mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Fix asset transformation in the presence of resolution-aware asset variants (#151932)
For the necessary background knowledge, see the flutter.dev content on [Resolution-aware image assets](https://docs.flutter.dev/ui/assets/assets-and-images#resolution-aware) and [Conditional bundling of assets based on app flavor](https://docs.flutter.dev/ui/assets/assets-and-images#conditional-bundling-of-assets-based-on-app-flavor) if you don't have a basic understanding of these features. Fixes https://github.com/flutter/flutter/issues/151813 by using unique temporary directories, per asset file, for transformations. Currently, only a single directory is used and the name of the temporary files was based only on the basename of files. This means that `assets/image.png` and `assets/2x/image.png` would share an output path (`<temp dir path>/image.png`), causing a race. If this quick and rough explanation is a bit confusing, the original issueâ#151813âprovides a full repro and correct identification of the exact cause of the failure that can occur in the asset transformation process.
This commit is contained in:
parent
a8d11d2134
commit
ebe53d570a
@ -154,7 +154,8 @@ Future<Depfile> copyAssets(
|
||||
);
|
||||
doCopy = false;
|
||||
if (failure != null) {
|
||||
throwToolExit(failure.message);
|
||||
throwToolExit('User-defined transformation of asset "${entry.key}" failed.\n'
|
||||
'${failure.message}');
|
||||
}
|
||||
}
|
||||
case AssetKind.font:
|
||||
|
@ -55,16 +55,21 @@ final class AssetTransformer {
|
||||
required Logger logger,
|
||||
}) async {
|
||||
|
||||
String getTempFilePath(int transformStep) {
|
||||
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync();
|
||||
|
||||
int transformStep = 0;
|
||||
File nextTempFile() {
|
||||
final String basename = _fileSystem.path.basename(asset.path);
|
||||
final String ext = _fileSystem.path.extension(asset.path);
|
||||
return '$basename-transformOutput$transformStep$ext';
|
||||
|
||||
final File result = tempDirectory.childFile('$basename-transformOutput$transformStep$ext');
|
||||
transformStep++;
|
||||
return result;
|
||||
}
|
||||
|
||||
File tempInputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(0));
|
||||
File tempInputFile = nextTempFile();
|
||||
await asset.copy(tempInputFile.path);
|
||||
File tempOutputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(1));
|
||||
ErrorHandlingFileSystem.deleteIfExists(tempOutputFile);
|
||||
File tempOutputFile = nextTempFile();
|
||||
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
@ -78,10 +83,7 @@ final class AssetTransformer {
|
||||
);
|
||||
|
||||
if (transformerFailure != null) {
|
||||
return AssetTransformationFailure(
|
||||
'User-defined transformation of asset "${asset.path}" failed.\n'
|
||||
'${transformerFailure.message}',
|
||||
);
|
||||
return AssetTransformationFailure(transformerFailure.message);
|
||||
}
|
||||
|
||||
ErrorHandlingFileSystem.deleteIfExists(tempInputFile);
|
||||
@ -90,15 +92,13 @@ final class AssetTransformer {
|
||||
await tempOutputFile.copy(outputPath);
|
||||
} else {
|
||||
tempInputFile = tempOutputFile;
|
||||
tempOutputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(i+2));
|
||||
ErrorHandlingFileSystem.deleteIfExists(tempOutputFile);
|
||||
tempOutputFile = nextTempFile();
|
||||
}
|
||||
}
|
||||
|
||||
logger.printTrace("Finished transforming asset at path '${asset.path}' (${stopwatch.elapsedMilliseconds}ms)");
|
||||
} finally {
|
||||
ErrorHandlingFileSystem.deleteIfExists(tempInputFile);
|
||||
ErrorHandlingFileSystem.deleteIfExists(tempOutputFile);
|
||||
ErrorHandlingFileSystem.deleteIfExists(tempDirectory, recursive: true);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -124,6 +124,11 @@ final class AssetTransformer {
|
||||
...transformerArguments,
|
||||
];
|
||||
|
||||
// Delete the output file if it already exists for whatever reason.
|
||||
// With this, we can check for the existence of the file after transformation
|
||||
// to make sure the transformer produced an output file.
|
||||
ErrorHandlingFileSystem.deleteIfExists(output);
|
||||
|
||||
logger.printTrace("Transforming asset using command '${command.join(' ')}'");
|
||||
final ProcessResult result = await _processManager.run(
|
||||
command,
|
||||
|
@ -213,7 +213,8 @@ Future<void> writeBundle(
|
||||
);
|
||||
doCopy = false;
|
||||
if (failure != null) {
|
||||
throwToolExit(failure.message);
|
||||
throwToolExit('User-defined transformation of asset "${entry.key}" failed.\n'
|
||||
'${failure.message}');
|
||||
}
|
||||
case AssetKind.font:
|
||||
break;
|
||||
|
@ -31,8 +31,8 @@ void main() {
|
||||
artifacts.getArtifactPath(Artifact.engineDartBinary),
|
||||
'run',
|
||||
'my_copy_transformer',
|
||||
'--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
|
||||
'--input=/.tmp_rand0/rand0/asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/rand0/asset.txt-transformOutput1.txt',
|
||||
'-f',
|
||||
'--my_option',
|
||||
'my_option_value',
|
||||
@ -95,8 +95,8 @@ void main() {
|
||||
dartBinaryPath,
|
||||
'run',
|
||||
'my_copy_transformer',
|
||||
'--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
|
||||
'--input=/.tmp_rand0/rand0/asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/rand0/asset.txt-transformOutput1.txt',
|
||||
],
|
||||
onRun: (List<String> args) {
|
||||
final ArgResults parsedArgs = (ArgParser()
|
||||
@ -136,10 +136,9 @@ void main() {
|
||||
expect(failure, isNotNull);
|
||||
expect(failure!.message,
|
||||
'''
|
||||
User-defined transformation of asset "asset.txt" failed.
|
||||
Transformer process terminated with non-zero exit code: 1
|
||||
Transformer package: my_copy_transformer
|
||||
Full command: $dartBinaryPath run my_copy_transformer --input=/.tmp_rand0/asset.txt-transformOutput0.txt --output=/.tmp_rand0/asset.txt-transformOutput1.txt
|
||||
Full command: $dartBinaryPath run my_copy_transformer --input=/.tmp_rand0/rand0/asset.txt-transformOutput0.txt --output=/.tmp_rand0/rand0/asset.txt-transformOutput1.txt
|
||||
stdout:
|
||||
Beginning transformation
|
||||
stderr:
|
||||
@ -162,8 +161,8 @@ Something went wrong''');
|
||||
dartBinaryPath,
|
||||
'run',
|
||||
'my_transformer',
|
||||
'--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
|
||||
'--input=/.tmp_rand0/rand0/asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/rand0/asset.txt-transformOutput1.txt',
|
||||
],
|
||||
onRun: (_) {
|
||||
// Do nothing.
|
||||
@ -196,11 +195,10 @@ Something went wrong''');
|
||||
expect(failure, isNotNull);
|
||||
expect(failure!.message,
|
||||
'''
|
||||
User-defined transformation of asset "asset.txt" failed.
|
||||
Asset transformer my_transformer did not produce an output file.
|
||||
Input file provided to transformer: "/.tmp_rand0/asset.txt-transformOutput0.txt"
|
||||
Expected output file at: "/.tmp_rand0/asset.txt-transformOutput1.txt"
|
||||
Full command: $dartBinaryPath run my_transformer --input=/.tmp_rand0/asset.txt-transformOutput0.txt --output=/.tmp_rand0/asset.txt-transformOutput1.txt
|
||||
Input file provided to transformer: "/.tmp_rand0/rand0/asset.txt-transformOutput0.txt"
|
||||
Expected output file at: "/.tmp_rand0/rand0/asset.txt-transformOutput1.txt"
|
||||
Full command: $dartBinaryPath run my_transformer --input=/.tmp_rand0/rand0/asset.txt-transformOutput0.txt --output=/.tmp_rand0/rand0/asset.txt-transformOutput1.txt
|
||||
stdout:
|
||||
|
||||
stderr:
|
||||
@ -225,8 +223,8 @@ Transformation failed, but I forgot to exit with a non-zero code.'''
|
||||
dartBinaryPath,
|
||||
'run',
|
||||
'my_lowercase_transformer',
|
||||
'--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
|
||||
'--input=/.tmp_rand0/rand0/asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/rand0/asset.txt-transformOutput1.txt',
|
||||
],
|
||||
onRun: (List<String> args) {
|
||||
final ArgResults parsedArgs = (ArgParser()
|
||||
@ -245,8 +243,8 @@ Transformation failed, but I forgot to exit with a non-zero code.'''
|
||||
dartBinaryPath,
|
||||
'run',
|
||||
'my_distance_from_ascii_a_transformer',
|
||||
'--input=/.tmp_rand0/asset.txt-transformOutput1.txt',
|
||||
'--output=/.tmp_rand0/asset.txt-transformOutput2.txt',
|
||||
'--input=/.tmp_rand0/rand0/asset.txt-transformOutput1.txt',
|
||||
'--output=/.tmp_rand0/rand0/asset.txt-transformOutput2.txt',
|
||||
],
|
||||
onRun: (List<String> args) {
|
||||
final ArgResults parsedArgs = (ArgParser()
|
||||
@ -314,8 +312,8 @@ Transformation failed, but I forgot to exit with a non-zero code.'''
|
||||
dartBinaryPath,
|
||||
'run',
|
||||
'my_lowercase_transformer',
|
||||
'--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
|
||||
'--input=/.tmp_rand0/rand0/asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/rand0/asset.txt-transformOutput1.txt',
|
||||
],
|
||||
onRun: (List<String> args) {
|
||||
final ArgResults parsedArgs = (ArgParser()
|
||||
@ -334,8 +332,8 @@ Transformation failed, but I forgot to exit with a non-zero code.'''
|
||||
dartBinaryPath,
|
||||
'run',
|
||||
'my_distance_from_ascii_a_transformer',
|
||||
'--input=/.tmp_rand0/asset.txt-transformOutput1.txt',
|
||||
'--output=/.tmp_rand0/asset.txt-transformOutput2.txt',
|
||||
'--input=/.tmp_rand0/rand0/asset.txt-transformOutput1.txt',
|
||||
'--output=/.tmp_rand0/rand0/asset.txt-transformOutput2.txt',
|
||||
],
|
||||
onRun: (List<String> args) {
|
||||
// Do nothing.
|
||||
@ -374,11 +372,10 @@ Transformation failed, but I forgot to exit with a non-zero code.'''
|
||||
expect(failure, isNotNull);
|
||||
expect(failure!.message,
|
||||
'''
|
||||
User-defined transformation of asset "asset.txt" failed.
|
||||
Asset transformer my_distance_from_ascii_a_transformer did not produce an output file.
|
||||
Input file provided to transformer: "/.tmp_rand0/asset.txt-transformOutput1.txt"
|
||||
Expected output file at: "/.tmp_rand0/asset.txt-transformOutput2.txt"
|
||||
Full command: Artifact.engineDartBinary run my_distance_from_ascii_a_transformer --input=/.tmp_rand0/asset.txt-transformOutput1.txt --output=/.tmp_rand0/asset.txt-transformOutput2.txt
|
||||
Input file provided to transformer: "/.tmp_rand0/rand0/asset.txt-transformOutput1.txt"
|
||||
Expected output file at: "/.tmp_rand0/rand0/asset.txt-transformOutput2.txt"
|
||||
Full command: Artifact.engineDartBinary run my_distance_from_ascii_a_transformer --input=/.tmp_rand0/rand0/asset.txt-transformOutput1.txt --output=/.tmp_rand0/rand0/asset.txt-transformOutput2.txt
|
||||
stdout:
|
||||
|
||||
stderr:
|
||||
|
@ -252,6 +252,7 @@ flutter:
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
testUsingContext('exits tool if an asset transformation fails', () async {
|
||||
Cache.flutterRoot = Cache.defaultFlutterRoot(
|
||||
platform: globals.platform,
|
||||
@ -289,7 +290,7 @@ flutter:
|
||||
|
||||
await expectToolExitLater(
|
||||
const CopyAssets().build(environment),
|
||||
startsWith('User-defined transformation of asset "/input.txt" failed.\n'),
|
||||
startsWith('User-defined transformation of asset "input.txt" failed.\n'),
|
||||
);
|
||||
expect(globals.processManager, hasNoRemainingExpectations);
|
||||
}, overrides: <Type, Generator> {
|
||||
@ -316,6 +317,90 @@ flutter:
|
||||
),
|
||||
});
|
||||
|
||||
testUsingContext('asset transformation, per each asset, uses unique paths for temporary files', () async {
|
||||
final List<String> inputFilePaths = <String>[];
|
||||
final List<String> outputFilePaths = <String>[];
|
||||
|
||||
final FakeCommand transformerCommand = FakeCommand(
|
||||
command: <Pattern>[
|
||||
Artifacts.test().getArtifactPath(Artifact.engineDartBinary),
|
||||
'run',
|
||||
'my_capitalizer_transformer',
|
||||
RegExp('--input=.*'),
|
||||
RegExp('--output=.*'),
|
||||
],
|
||||
onRun: (List<String> args) {
|
||||
final ArgResults parsedArgs = (ArgParser()
|
||||
..addOption('input')
|
||||
..addOption('output'))
|
||||
.parse(args);
|
||||
|
||||
final String input = parsedArgs['input'] as String;
|
||||
final String output = parsedArgs['output'] as String;
|
||||
|
||||
inputFilePaths.add(input);
|
||||
outputFilePaths.add(output);
|
||||
|
||||
fileSystem.file(output)
|
||||
..createSync()
|
||||
..writeAsStringSync('foo');
|
||||
},
|
||||
);
|
||||
|
||||
Cache.flutterRoot = Cache.defaultFlutterRoot(
|
||||
platform: globals.platform,
|
||||
fileSystem: fileSystem,
|
||||
userMessages: UserMessages(),
|
||||
);
|
||||
|
||||
final Environment environment = Environment.test(
|
||||
fileSystem.currentDirectory,
|
||||
processManager: FakeProcessManager.list(
|
||||
<FakeCommand>[
|
||||
transformerCommand,
|
||||
transformerCommand,
|
||||
],
|
||||
),
|
||||
artifacts: Artifacts.test(),
|
||||
fileSystem: fileSystem,
|
||||
logger: logger,
|
||||
platform: globals.platform,
|
||||
defines: <String, String>{
|
||||
kBuildMode: BuildMode.debug.cliName,
|
||||
},
|
||||
);
|
||||
|
||||
await fileSystem.file('.packages').create();
|
||||
|
||||
fileSystem.file('pubspec.yaml')
|
||||
..createSync()
|
||||
..writeAsStringSync('''
|
||||
name: example
|
||||
flutter:
|
||||
assets:
|
||||
- path: input.txt
|
||||
transformers:
|
||||
- package: my_capitalizer_transformer
|
||||
''');
|
||||
|
||||
fileSystem.file('input.txt')
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('abc');
|
||||
|
||||
fileSystem.directory('2x').childFile('input.txt')
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('def');
|
||||
|
||||
await const CopyAssets().build(environment);
|
||||
|
||||
expect(inputFilePaths.toSet(), hasLength(inputFilePaths.length));
|
||||
expect(outputFilePaths.toSet(), hasLength(outputFilePaths.length));
|
||||
}, overrides: <Type, Generator>{
|
||||
Logger: () => logger,
|
||||
FileSystem: () => fileSystem,
|
||||
Platform: () => FakePlatform(),
|
||||
ProcessManager: () => FakeProcessManager.empty(),
|
||||
});
|
||||
|
||||
testUsingContext('Throws exception if pubspec contains missing files', () async {
|
||||
fileSystem.file('pubspec.yaml')
|
||||
|
@ -71,8 +71,8 @@ void main() {
|
||||
artifacts.getArtifactPath(Artifact.engineDartBinary),
|
||||
'run',
|
||||
'increment',
|
||||
'--input=/.tmp_rand0/my-asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/my-asset.txt-transformOutput1.txt'
|
||||
'--input=/.tmp_rand0/rand0/my-asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/rand0/my-asset.txt-transformOutput1.txt'
|
||||
],
|
||||
onRun: (List<String> command) {
|
||||
final ArgResults argParseResults = (ArgParser()
|
||||
|
@ -733,8 +733,8 @@ void main() {
|
||||
artifacts.getArtifactPath(Artifact.engineDartBinary),
|
||||
'run',
|
||||
'increment',
|
||||
'--input=/.tmp_rand0/retransformerInput-asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/retransformerInput-asset.txt-transformOutput1.txt',
|
||||
'--input=/.tmp_rand0/rand0/retransformerInput-asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/rand0/retransformerInput-asset.txt-transformOutput1.txt',
|
||||
],
|
||||
onRun: (List<String> command) {
|
||||
final ArgResults argParseResults = (ArgParser()
|
||||
@ -831,8 +831,8 @@ void main() {
|
||||
artifacts.getArtifactPath(Artifact.engineDartBinary),
|
||||
'run',
|
||||
'increment',
|
||||
'--input=/.tmp_rand0/retransformerInput-asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/retransformerInput-asset.txt-transformOutput1.txt',
|
||||
'--input=/.tmp_rand0/rand0/retransformerInput-asset.txt-transformOutput0.txt',
|
||||
'--output=/.tmp_rand0/rand0/retransformerInput-asset.txt-transformOutput1.txt',
|
||||
],
|
||||
exitCode: 1,
|
||||
),
|
||||
@ -895,10 +895,9 @@ void main() {
|
||||
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'
|
||||
'Full command: Artifact.engineDartBinary run increment --input=/.tmp_rand0/rand0/retransformerInput-asset.txt-transformOutput0.txt --output=/.tmp_rand0/rand0/retransformerInput-asset.txt-transformOutput1.txt\n'
|
||||
'stdout:\n'
|
||||
'\n'
|
||||
'stderr:\n'
|
||||
|
Loading…
Reference in New Issue
Block a user