Enable asset transformation feature in hot reload workflow (excluding Web) (#144161)

Partial implementation of https://github.com/flutter/flutter/issues/143348

This enables asset transformation during hot reload (except for web, because that has its own implementation of `DevFS` 🙃). Asset transformers will be reapplied after changing any asset and performing a hot reload during `flutter run`.
This commit is contained in:
Andrew Kolos 2024-03-05 13:54:06 -08:00 committed by GitHub
parent 15e8d324f5
commit ff3b6dc02c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 357 additions and 16 deletions

View file

@ -218,8 +218,8 @@ class ManifestAssetBundle implements AssetBundle {
return true; return true;
} }
final FileStat stat = _fileSystem.file(manifestPath).statSync(); final FileStat manifestStat = _fileSystem.file(manifestPath).statSync();
if (stat.type == FileSystemEntityType.notFound) { if (manifestStat.type == FileSystemEntityType.notFound) {
return true; return true;
} }
@ -235,7 +235,7 @@ class ManifestAssetBundle implements AssetBundle {
} }
} }
return stat.modified.isAfter(lastBuildTimestamp); return manifestStat.modified.isAfter(lastBuildTimestamp);
} }
@override @override

View file

@ -3,11 +3,16 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:typed_data';
import 'package:pool/pool.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import '../../base/error_handling_io.dart'; import '../../base/error_handling_io.dart';
import '../../base/file_system.dart'; import '../../base/file_system.dart';
import '../../base/io.dart'; import '../../base/io.dart';
import '../../base/logger.dart';
import '../../devfs.dart';
import '../../flutter_manifest.dart'; import '../../flutter_manifest.dart';
import '../build_system.dart'; import '../build_system.dart';
@ -52,6 +57,7 @@ final class AssetTransformer {
File tempInputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(0)); File tempInputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(0));
await asset.copy(tempInputFile.path); await asset.copy(tempInputFile.path);
File tempOutputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(1)); File tempOutputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(1));
ErrorHandlingFileSystem.deleteIfExists(tempOutputFile);
try { try {
for (final (int i, AssetTransformerEntry transformer) in transformerEntries.indexed) { for (final (int i, AssetTransformerEntry transformer) in transformerEntries.indexed) {
@ -71,10 +77,12 @@ final class AssetTransformer {
ErrorHandlingFileSystem.deleteIfExists(tempInputFile); ErrorHandlingFileSystem.deleteIfExists(tempInputFile);
if (i == transformerEntries.length - 1) { if (i == transformerEntries.length - 1) {
await _fileSystem.file(outputPath).create(recursive: true);
await tempOutputFile.copy(outputPath); await tempOutputFile.copy(outputPath);
} else { } else {
tempInputFile = tempOutputFile; tempInputFile = tempOutputFile;
tempOutputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(i+2)); tempOutputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(i+2));
ErrorHandlingFileSystem.deleteIfExists(tempOutputFile);
} }
} }
} finally { } finally {
@ -136,6 +144,69 @@ final class AssetTransformer {
} }
} }
// A wrapper around [AssetTransformer] to support hot reload of transformed assets.
final class DevelopmentAssetTransformer {
DevelopmentAssetTransformer({
required FileSystem fileSystem,
required AssetTransformer transformer,
required Logger logger,
}) : _fileSystem = fileSystem,
_transformer = transformer,
_logger = logger;
final AssetTransformer _transformer;
final FileSystem _fileSystem;
final Pool _transformationPool = Pool(4);
final Logger _logger;
/// Re-transforms an asset and returns a [DevFSContent] that should be synced
/// to the attached device in its place.
///
/// Returns `null` if any of the transformation subprocesses failed.
Future<DevFSContent?> retransformAsset({
required String inputAssetKey,
required DevFSContent inputAssetContent,
required List<AssetTransformerEntry> transformerEntries,
required String workingDirectory,
}) async {
final File output = _fileSystem.systemTempDirectory.childFile('retransformerInput-$inputAssetKey');
ErrorHandlingFileSystem.deleteIfExists(output);
File? inputFile;
bool cleanupInput = false;
Uint8List result;
PoolResource? resource;
try {
resource = await _transformationPool.request();
if (inputAssetContent is DevFSFileContent) {
inputFile = inputAssetContent.file as File;
} else {
inputFile = _fileSystem.systemTempDirectory.childFile('retransformerInput-$inputAssetKey');
inputFile.writeAsBytesSync(await inputAssetContent.contentsAsBytes());
cleanupInput = true;
}
final AssetTransformationFailure? failure = await _transformer.transformAsset(
asset: inputFile,
outputPath: output.path,
transformerEntries: transformerEntries,
workingDirectory: workingDirectory,
);
if (failure != null) {
_logger.printError(failure.message);
return null;
}
result = output.readAsBytesSync();
} finally {
resource?.release();
ErrorHandlingFileSystem.deleteIfExists(output);
if (cleanupInput && inputFile != null) {
ErrorHandlingFileSystem.deleteIfExists(inputFile);
}
}
return DevFSByteContent(result);
}
}
final class AssetTransformationFailure { final class AssetTransformationFailure {
const AssetTransformationFailure(this.message); const AssetTransformationFailure(this.message);

View file

@ -5,8 +5,10 @@
import 'dart:async'; import 'dart:async';
import 'package:package_config/package_config.dart'; import 'package:package_config/package_config.dart';
import 'package:process/process.dart';
import 'package:vm_service/vm_service.dart' as vm_service; import 'package:vm_service/vm_service.dart' as vm_service;
import 'artifacts.dart';
import 'asset.dart'; import 'asset.dart';
import 'base/config.dart'; import 'base/config.dart';
import 'base/context.dart'; import 'base/context.dart';
@ -16,6 +18,7 @@ import 'base/logger.dart';
import 'base/net.dart'; import 'base/net.dart';
import 'base/os.dart'; import 'base/os.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'build_system/tools/asset_transformer.dart';
import 'build_system/tools/scene_importer.dart'; import 'build_system/tools/scene_importer.dart';
import 'build_system/tools/shader_compiler.dart'; import 'build_system/tools/shader_compiler.dart';
import 'compile.dart'; import 'compile.dart';
@ -454,6 +457,8 @@ class DevFS {
required OperatingSystemUtils osUtils, required OperatingSystemUtils osUtils,
required Logger logger, required Logger logger,
required FileSystem fileSystem, required FileSystem fileSystem,
required ProcessManager processManager,
required Artifacts artifacts,
HttpClient? httpClient, HttpClient? httpClient,
Duration? uploadRetryThrottle, Duration? uploadRetryThrottle,
StopwatchFactory stopwatchFactory = const StopwatchFactory(), StopwatchFactory stopwatchFactory = const StopwatchFactory(),
@ -471,7 +476,16 @@ class DevFS {
? HttpClient() ? HttpClient()
: context.get<HttpClientFactory>()!())), : context.get<HttpClientFactory>()!())),
_stopwatchFactory = stopwatchFactory, _stopwatchFactory = stopwatchFactory,
_config = config; _config = config,
_assetTransformer = DevelopmentAssetTransformer(
transformer: AssetTransformer(
processManager: processManager,
fileSystem: fileSystem,
dartBinaryPath: artifacts.getArtifactPath(Artifact.engineDartBinary),
),
fileSystem: fileSystem,
logger: logger,
);
final FlutterVmService _vmService; final FlutterVmService _vmService;
final _DevFSHttpWriter _httpWriter; final _DevFSHttpWriter _httpWriter;
@ -479,9 +493,10 @@ class DevFS {
final FileSystem _fileSystem; final FileSystem _fileSystem;
final StopwatchFactory _stopwatchFactory; final StopwatchFactory _stopwatchFactory;
final Config? _config; final Config? _config;
final DevelopmentAssetTransformer _assetTransformer;
final String fsName; final String fsName;
final Directory? rootDirectory; final Directory rootDirectory;
final Set<String> assetPathsToEvict = <String>{}; final Set<String> assetPathsToEvict = <String>{};
final Set<String> shaderPathsToEvict = <String>{}; final Set<String> shaderPathsToEvict = <String>{};
final Set<String> scenePathsToEvict = <String>{}; final Set<String> scenePathsToEvict = <String>{};
@ -505,7 +520,7 @@ class DevFS {
final String baseUriString = baseUri.toString(); final String baseUriString = baseUri.toString();
if (deviceUriString.startsWith(baseUriString)) { if (deviceUriString.startsWith(baseUriString)) {
final String deviceUriSuffix = deviceUriString.substring(baseUriString.length); final String deviceUriSuffix = deviceUriString.substring(baseUriString.length);
return rootDirectory!.uri.resolve(deviceUriSuffix); return rootDirectory.uri.resolve(deviceUriSuffix);
} }
return deviceUri; return deviceUri;
} }
@ -600,7 +615,7 @@ class DevFS {
invalidatedFiles, invalidatedFiles,
outputPath: dillOutputPath, outputPath: dillOutputPath,
fs: _fileSystem, fs: _fileSystem,
projectRootPath: rootDirectory?.path, projectRootPath: rootDirectory.path,
packageConfig: packageConfig, packageConfig: packageConfig,
checkDartPluginRegistry: true, // The entry point is assumed not to have changed. checkDartPluginRegistry: true, // The entry point is assumed not to have changed.
dartPluginRegistrant: dartPluginRegistrant, dartPluginRegistrant: dartPluginRegistrant,
@ -636,8 +651,8 @@ class DevFS {
if (archivePath == _kFontManifest) { if (archivePath == _kFontManifest) {
didUpdateFontManifest = true; didUpdateFontManifest = true;
} }
final AssetKind? kind = bundle.entries[archivePath]?.kind;
switch (bundle.entries[archivePath]?.kind) { switch (kind) {
case AssetKind.shader: case AssetKind.shader:
final Future<DevFSContent?> pending = shaderCompiler.recompileShader(entry.content); final Future<DevFSContent?> pending = shaderCompiler.recompileShader(entry.content);
pendingAssetBuilds.add(pending); pendingAssetBuilds.add(pending);
@ -672,11 +687,30 @@ class DevFS {
case AssetKind.regular: case AssetKind.regular:
case AssetKind.font: case AssetKind.font:
case null: case null:
dirtyEntries[deviceUri] = entry.content; final Future<DevFSContent?> pending = (() async {
syncedBytes += entry.content.size; if (entry.transformers.isEmpty || kind != AssetKind.regular) {
if (!bundleFirstUpload) { return entry.content;
assetPathsToEvict.add(archivePath); }
} return _assetTransformer.retransformAsset(
inputAssetKey: archivePath,
inputAssetContent: entry.content,
transformerEntries: entry.transformers,
workingDirectory: rootDirectory.path,
);
})();
pendingAssetBuilds.add(pending);
pending.then((DevFSContent? content) {
if (content == null) {
assetBuildFailed = true;
return;
}
dirtyEntries[deviceUri] = content;
syncedBytes += content.size;
if (!bundleFirstUpload) {
assetPathsToEvict.add(archivePath);
}
});
} }
}); });

View file

@ -701,6 +701,7 @@ class WebDevFS implements DevFS {
required this.nullSafetyMode, required this.nullSafetyMode,
required this.ddcModuleSystem, required this.ddcModuleSystem,
required this.webRenderer, required this.webRenderer,
required this.rootDirectory,
this.testMode = false, this.testMode = false,
}) : _port = port; }) : _port = port;
@ -857,7 +858,7 @@ class WebDevFS implements DevFS {
String get fsName => 'web_asset'; String get fsName => 'web_asset';
@override @override
Directory? get rootDirectory => null; final Directory rootDirectory;
@override @override
Future<UpdateFSReport> update({ Future<UpdateFSReport> update({

View file

@ -311,6 +311,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
nativeNullAssertions: debuggingOptions.nativeNullAssertions, nativeNullAssertions: debuggingOptions.nativeNullAssertions,
ddcModuleSystem: debuggingOptions.buildInfo.ddcModuleFormat == DdcModuleFormat.ddc, ddcModuleSystem: debuggingOptions.buildInfo.ddcModuleFormat == DdcModuleFormat.ddc,
webRenderer: debuggingOptions.webRenderer, webRenderer: debuggingOptions.webRenderer,
rootDirectory: fileSystem.directory(projectRootPath),
); );
Uri url = await device!.devFS!.create(); Uri url = await device!.devFS!.create();
if (debuggingOptions.tlsCertKeyPath != null && debuggingOptions.tlsCertPath != null) { if (debuggingOptions.tlsCertKeyPath != null && debuggingOptions.tlsCertPath != null) {

View file

@ -385,6 +385,8 @@ class FlutterDevice {
osUtils: globals.os, osUtils: globals.os,
fileSystem: globals.fs, fileSystem: globals.fs,
logger: globals.logger, logger: globals.logger,
processManager: globals.processManager,
artifacts: globals.artifacts!,
); );
return devFS!.create(); return devFS!.create();
} }

View file

@ -74,6 +74,7 @@ void main() {
expect(transformationFailure, isNull, reason: logger.errorText); expect(transformationFailure, isNull, reason: logger.errorText);
expect(processManager, hasNoRemainingExpectations); expect(processManager, hasNoRemainingExpectations);
expect(fileSystem.file(outputPath).readAsStringSync(), 'hello world'); expect(fileSystem.file(outputPath).readAsStringSync(), 'hello world');
expect(fileSystem.directory('.tmp_rand0').listSync(), isEmpty, reason: 'Transformer did not clean up after itself.');
}); });
testWithoutContext('logs useful error information when transformation process returns a nonzero exit code', () async { testWithoutContext('logs useful error information when transformation process returns a nonzero exit code', () async {
@ -138,6 +139,7 @@ stdout:
Beginning transformation Beginning transformation
stderr: stderr:
Something went wrong'''); Something went wrong''');
expect(fileSystem.directory('.tmp_rand0').listSync(), isEmpty, reason: 'Transformer did not clean up after itself.');
}); });
testWithoutContext('prints error message when the transformer does not produce an output file', () async { testWithoutContext('prints error message when the transformer does not produce an output file', () async {
@ -197,6 +199,7 @@ stdout:
stderr: stderr:
Transformation failed, but I forgot to exit with a non-zero code.''' Transformation failed, but I forgot to exit with a non-zero code.'''
); );
expect(fileSystem.directory('.tmp_rand0').listSync(), isEmpty, reason: 'Transformer did not clean up after itself.');
}); });
testWithoutContext('correctly chains transformations when there are multiple of them', () async { testWithoutContext('correctly chains transformations when there are multiple of them', () async {
@ -283,6 +286,7 @@ Transformation failed, but I forgot to exit with a non-zero code.'''
expect(processManager, hasNoRemainingExpectations); expect(processManager, hasNoRemainingExpectations);
expect(failure, isNull); expect(failure, isNull);
expect(fileSystem.file(outputPath).readAsStringSync(), '012'); expect(fileSystem.file(outputPath).readAsStringSync(), '012');
expect(fileSystem.directory('.tmp_rand0').listSync(), isEmpty, reason: 'Transformer did not clean up after itself.');
}); });
testWithoutContext('prints an error when a transformer in a chain (thats not the first) does not produce an output', () async { testWithoutContext('prints an error when a transformer in a chain (thats not the first) does not produce an output', () async {
@ -368,6 +372,6 @@ Transformation failed, but I forgot to exit with a non-zero code.'''
); );
expect(processManager, hasNoRemainingExpectations); expect(processManager, hasNoRemainingExpectations);
expect(fileSystem.file(outputPath), isNot(exists)); expect(fileSystem.file(outputPath), isNot(exists));
expect(fileSystem.systemTempDirectory.listSync(), isEmpty); expect(fileSystem.directory('.tmp_rand0').listSync(), isEmpty, reason: 'Transformer did not clean up after itself.');
}); });
} }

View file

@ -5,7 +5,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io' as io show Process, ProcessSignal; import 'dart:io' as io show Process, ProcessSignal;
import 'dart:typed_data';
import 'package:args/args.dart';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart'; import 'package:file_testing/file_testing.dart';
@ -29,6 +31,7 @@ import 'package:test/fake.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
import '../src/fake_http_client.dart'; import '../src/fake_http_client.dart';
import '../src/fake_process_manager.dart';
import '../src/fake_vm_services.dart'; import '../src/fake_vm_services.dart';
import '../src/fakes.dart'; import '../src/fakes.dart';
import '../src/logging_logger.dart'; import '../src/logging_logger.dart';
@ -144,6 +147,8 @@ void main() {
fileSystem: fileSystem, fileSystem: fileSystem,
logger: BufferLogger.test(), logger: BufferLogger.test(),
httpClient: FakeHttpClient.any(), httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
expect(() async => devFS.create(), throwsA(isA<DevFSException>())); expect(() async => devFS.create(), throwsA(isA<DevFSException>()));
}); });
@ -167,6 +172,8 @@ void main() {
fileSystem: fileSystem, fileSystem: fileSystem,
logger: BufferLogger.test(), logger: BufferLogger.test(),
httpClient: FakeHttpClient.any(), httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
expect(await devFS.create(), isNotNull); expect(await devFS.create(), isNotNull);
@ -215,6 +222,8 @@ void main() {
FakeRequest(Uri.parse('http://localhost'), method: HttpMethod.put, body: <int>[for (final List<int> chunk in expectedEncoded) ...chunk]), FakeRequest(Uri.parse('http://localhost'), method: HttpMethod.put, body: <int>[for (final List<int> chunk in expectedEncoded) ...chunk]),
]), ]),
uploadRetryThrottle: Duration.zero, uploadRetryThrottle: Duration.zero,
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
await devFS.create(); await devFS.create();
@ -248,6 +257,8 @@ void main() {
logger: BufferLogger.test(), logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(), osUtils: FakeOperatingSystemUtils(),
httpClient: FakeHttpClient.any(), httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
await devFS.create(); await devFS.create();
@ -288,6 +299,8 @@ void main() {
logger: BufferLogger.test(), logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(), osUtils: FakeOperatingSystemUtils(),
httpClient: FakeHttpClient.any(), httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
await devFS.create(); await devFS.create();
@ -330,6 +343,8 @@ void main() {
logger: BufferLogger.test(), logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(), osUtils: FakeOperatingSystemUtils(),
httpClient: HttpClient(), httpClient: HttpClient(),
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
await devFS.create(); await devFS.create();
@ -379,6 +394,8 @@ void main() {
logger: BufferLogger.test(), logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(), osUtils: FakeOperatingSystemUtils(),
httpClient: FakeHttpClient.any(), httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
await devFS.create(); await devFS.create();
@ -456,6 +473,8 @@ void main() {
'compile': FakeStopwatch()..elapsed = const Duration(seconds: 3), 'compile': FakeStopwatch()..elapsed = const Duration(seconds: 3),
'transfer': FakeStopwatch()..elapsed = const Duration(seconds: 5), 'transfer': FakeStopwatch()..elapsed = const Duration(seconds: 5),
}), }),
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
await devFS.create(); await devFS.create();
@ -500,6 +519,8 @@ void main() {
logger: logger, logger: logger,
osUtils: FakeOperatingSystemUtils(), osUtils: FakeOperatingSystemUtils(),
httpClient: FakeHttpClient.any(), httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
await devFS.create(); await devFS.create();
@ -604,6 +625,8 @@ void main() {
osUtils: FakeOperatingSystemUtils(), osUtils: FakeOperatingSystemUtils(),
httpClient: FakeHttpClient.any(), httpClient: FakeHttpClient.any(),
config: Config.test(), config: Config.test(),
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
await devFS.create(); await devFS.create();
@ -660,6 +683,8 @@ void main() {
osUtils: FakeOperatingSystemUtils(), osUtils: FakeOperatingSystemUtils(),
httpClient: FakeHttpClient.any(), httpClient: FakeHttpClient.any(),
config: Config.test(), config: Config.test(),
processManager: FakeProcessManager.empty(),
artifacts: Artifacts.test(),
); );
await devFS.create(); await devFS.create();
@ -698,6 +723,191 @@ void main() {
expect(devFS.didUpdateFontManifest, true); expect(devFS.didUpdateFontManifest, true);
}); });
}); });
group('Asset transformation', () {
testWithoutContext('DevFS re-transforms assets with transformers during update', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final Artifacts artifacts = Artifacts.test();
final FakeDevFSWriter devFSWriter = FakeDevFSWriter();
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[
FakeCommand(
command: <Pattern>[
artifacts.getArtifactPath(Artifact.engineDartBinary),
'run',
'increment',
'--input=/.tmp_rand0/retransformerInput-asset.txt-transformOutput0.txt',
'--output=/.tmp_rand0/retransformerInput-asset.txt-transformOutput1.txt',
],
onRun: (List<String> command) {
final ArgResults argParseResults = (ArgParser()
..addOption('input', mandatory: true)
..addOption('output', mandatory: true))
.parse(command);
final File inputFile = fileSystem.file(argParseResults['input']);
final File outputFile = fileSystem.file(argParseResults['output']);
expect(inputFile, exists);
outputFile
..createSync()
..writeAsBytesSync(
Uint8List.fromList(
inputFile.readAsBytesSync().map((int b) => b + 1).toList(),
),
);
},
),
],
);
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[createDevFSRequest],
httpAddress: Uri.parse('http://localhost'),
);
final BufferLogger logger = BufferLogger.test();
final DevFS devFS = DevFS(
fakeVmServiceHost.vmService,
'test',
fileSystem.currentDirectory,
fileSystem: fileSystem,
logger: logger,
osUtils: FakeOperatingSystemUtils(),
httpClient: FakeHttpClient.any(),
config: Config.test(),
processManager: processManager,
artifacts: artifacts,
);
await devFS.create();
final FakeResidentCompiler residentCompiler = FakeResidentCompiler()
..onRecompile = (Uri mainUri, List<Uri>? invalidatedFiles) async {
fileSystem.file('lib/foo.dill')
..createSync(recursive: true)
..writeAsBytesSync(<int>[1, 2, 3, 4, 5]);
return const CompilerOutput('lib/foo.dill', 0, <Uri>[]);
};
final FakeBundle bundle = FakeBundle()
..entries['asset.txt'] = AssetBundleEntry(
DevFSByteContent(<int>[1, 2, 3, 4]),
kind: AssetKind.regular,
transformers: const <AssetTransformerEntry>[
AssetTransformerEntry(package: 'increment', args: <String>[]),
],
);
final UpdateFSReport report = await devFS.update(
mainUri: Uri.parse('lib/main.dart'),
generator: residentCompiler,
dillOutputPath: 'lib/foo.dill',
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
devFSWriter: devFSWriter,
shaderCompiler: const FakeShaderCompiler(),
bundle: bundle,
);
expect(processManager, hasNoRemainingExpectations);
expect(report.success, true);
expect(devFSWriter.entries, isNotNull);
final Uri assetUri = Uri(path: 'build/flutter_assets/asset.txt');
expect(devFSWriter.entries, contains(assetUri));
expect(
await devFSWriter.entries![assetUri]!.contentsAsBytes(),
containsAllInOrder(<int>[2, 3, 4, 5]),
);
});
testWithoutContext('DevFS reports failure when asset transformation fails', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final Artifacts artifacts = Artifacts.test();
final FakeDevFSWriter devFSWriter = FakeDevFSWriter();
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[
FakeCommand(
command: <Pattern>[
artifacts.getArtifactPath(Artifact.engineDartBinary),
'run',
'increment',
'--input=/.tmp_rand0/retransformerInput-asset.txt-transformOutput0.txt',
'--output=/.tmp_rand0/retransformerInput-asset.txt-transformOutput1.txt',
],
exitCode: 1,
),
],
);
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[createDevFSRequest],
httpAddress: Uri.parse('http://localhost'),
);
final BufferLogger logger = BufferLogger.test();
final DevFS devFS = DevFS(
fakeVmServiceHost.vmService,
'test',
fileSystem.currentDirectory,
fileSystem: fileSystem,
logger: logger,
osUtils: FakeOperatingSystemUtils(),
httpClient: FakeHttpClient.any(),
config: Config.test(),
processManager: processManager,
artifacts: artifacts,
);
await devFS.create();
final FakeResidentCompiler residentCompiler = FakeResidentCompiler()
..onRecompile = (Uri mainUri, List<Uri>? invalidatedFiles) async {
fileSystem.file('lib/foo.dill')
..createSync(recursive: true)
..writeAsBytesSync(<int>[1, 2, 3, 4, 5]);
return const CompilerOutput('lib/foo.dill', 0, <Uri>[]);
};
final FakeBundle bundle = FakeBundle()
..entries['asset.txt'] = AssetBundleEntry(
DevFSByteContent(<int>[1, 2, 3, 4]),
kind: AssetKind.regular,
transformers: const <AssetTransformerEntry>[
AssetTransformerEntry(package: 'increment', args: <String>[]),
],
);
final UpdateFSReport report = await devFS.update(
mainUri: Uri.parse('lib/main.dart'),
generator: residentCompiler,
dillOutputPath: 'lib/foo.dill',
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
devFSWriter: devFSWriter,
shaderCompiler: const FakeShaderCompiler(),
bundle: bundle,
);
expect(processManager, hasNoRemainingExpectations);
expect(report.success, false, reason: 'DevFS update should fail since asset transformation failed.');
expect(devFSWriter.entries, isNull, reason: 'DevFS should not have written anything since the update failed.');
expect(
logger.errorText,
'User-defined transformation of asset "/.tmp_rand0/retransformerInput-asset.txt" failed.\n'
'Transformer process terminated with non-zero exit code: 1\n'
'Transformer package: increment\n'
'Full command: Artifact.engineDartBinary run increment --input=/.tmp_rand0/retransformerInput-asset.txt-transformOutput0.txt --output=/.tmp_rand0/retransformerInput-asset.txt-transformOutput1.txt\n'
'stdout:\n'
'\n'
'stderr:\n'
'\n',
);
});
});
} }
class FakeResidentCompiler extends Fake implements ResidentCompiler { class FakeResidentCompiler extends Fake implements ResidentCompiler {
@ -723,10 +933,12 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
class FakeDevFSWriter implements DevFSWriter { class FakeDevFSWriter implements DevFSWriter {
bool written = false; bool written = false;
Map<Uri, DevFSContent>? entries;
@override @override
Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, DevFSWriter parent) async { Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, DevFSWriter parent) async {
written = true; written = true;
this.entries = entries;
} }
} }

View file

@ -909,6 +909,7 @@ void main() {
nullSafetyMode: NullSafetyMode.unsound, nullSafetyMode: NullSafetyMode.unsound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.html, webRenderer: WebRendererMode.html,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.ddcModuleLoaderJS.createSync(recursive: true);
webDevFS.flutterJs.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true);
@ -1044,6 +1045,7 @@ void main() {
nullSafetyMode: NullSafetyMode.sound, nullSafetyMode: NullSafetyMode.sound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.html, webRenderer: WebRendererMode.html,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.ddcModuleLoaderJS.createSync(recursive: true);
webDevFS.flutterJs.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true);
@ -1179,6 +1181,7 @@ void main() {
nullSafetyMode: NullSafetyMode.sound, nullSafetyMode: NullSafetyMode.sound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.ddcModuleLoaderJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
@ -1251,6 +1254,7 @@ void main() {
nullSafetyMode: NullSafetyMode.sound, nullSafetyMode: NullSafetyMode.sound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.ddcModuleLoaderJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
@ -1299,6 +1303,7 @@ void main() {
nullSafetyMode: NullSafetyMode.sound, nullSafetyMode: NullSafetyMode.sound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.ddcModuleLoaderJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
@ -1349,6 +1354,7 @@ void main() {
nullSafetyMode: NullSafetyMode.sound, nullSafetyMode: NullSafetyMode.sound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.auto, webRenderer: WebRendererMode.auto,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.ddcModuleLoaderJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
@ -1401,6 +1407,7 @@ void main() {
nullSafetyMode: NullSafetyMode.unsound, nullSafetyMode: NullSafetyMode.unsound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.ddcModuleLoaderJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
@ -1571,6 +1578,7 @@ void main() {
nullSafetyMode: NullSafetyMode.unsound, nullSafetyMode: NullSafetyMode.unsound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.ddcModuleLoaderJS.createSync(recursive: true); webDevFS.ddcModuleLoaderJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);

View file

@ -697,6 +697,7 @@ void main() {
nullSafetyMode: NullSafetyMode.unsound, nullSafetyMode: NullSafetyMode.unsound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.html, webRenderer: WebRendererMode.html,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.requireJS.createSync(recursive: true); webDevFS.requireJS.createSync(recursive: true);
webDevFS.flutterJs.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true);
@ -808,6 +809,7 @@ void main() {
nullSafetyMode: NullSafetyMode.sound, nullSafetyMode: NullSafetyMode.sound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.html, webRenderer: WebRendererMode.html,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.requireJS.createSync(recursive: true); webDevFS.requireJS.createSync(recursive: true);
webDevFS.flutterJs.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true);
@ -918,6 +920,7 @@ void main() {
nullSafetyMode: NullSafetyMode.sound, nullSafetyMode: NullSafetyMode.sound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.requireJS.createSync(recursive: true); webDevFS.requireJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
@ -981,6 +984,7 @@ void main() {
nullSafetyMode: NullSafetyMode.sound, nullSafetyMode: NullSafetyMode.sound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.requireJS.createSync(recursive: true); webDevFS.requireJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
@ -1028,6 +1032,7 @@ void main() {
nullSafetyMode: NullSafetyMode.sound, nullSafetyMode: NullSafetyMode.sound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.requireJS.createSync(recursive: true); webDevFS.requireJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
@ -1076,6 +1081,7 @@ void main() {
nullSafetyMode: NullSafetyMode.sound, nullSafetyMode: NullSafetyMode.sound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.auto, webRenderer: WebRendererMode.auto,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.requireJS.createSync(recursive: true); webDevFS.requireJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
@ -1125,6 +1131,7 @@ void main() {
nullSafetyMode: NullSafetyMode.unsound, nullSafetyMode: NullSafetyMode.unsound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.requireJS.createSync(recursive: true); webDevFS.requireJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
@ -1278,6 +1285,7 @@ void main() {
nullSafetyMode: NullSafetyMode.unsound, nullSafetyMode: NullSafetyMode.unsound,
ddcModuleSystem: usesDdcModuleSystem, ddcModuleSystem: usesDdcModuleSystem,
webRenderer: WebRendererMode.canvaskit, webRenderer: WebRendererMode.canvaskit,
rootDirectory: globals.fs.currentDirectory,
); );
webDevFS.requireJS.createSync(recursive: true); webDevFS.requireJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);