Enable asset transformation for flutter build for iOS, Android, Windows, MacOS, Linux, and web (also flutter run without hot reload support) (#143815)

See title. These are are the platforms that use the `CopyAssets` `Target` as part of their build target.

Partial implementation of https://github.com/flutter/flutter/issues/143348.
This commit is contained in:
Andrew Kolos 2024-02-23 14:48:08 -08:00 committed by GitHub
parent 15f8ef71d9
commit 4e814a5f3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 767 additions and 10 deletions

View file

@ -88,10 +88,12 @@ enum AssetKind {
final class AssetBundleEntry {
const AssetBundleEntry(this.content, {
required this.kind,
required this.transformers,
});
final DevFSContent content;
final AssetKind kind;
final List<AssetTransformerEntry> transformers;
Future<List<int>> contentsAsBytes() => content.contentsAsBytes();
}
@ -264,6 +266,7 @@ class ManifestAssetBundle implements AssetBundle {
entries[_kAssetManifestJsonFilename] = AssetBundleEntry(
DevFSStringContent('{}'),
kind: AssetKind.regular,
transformers: const <AssetTransformerEntry>[],
);
final ByteData emptyAssetManifest =
const StandardMessageCodec().encodeMessage(<dynamic, dynamic>{})!;
@ -272,12 +275,14 @@ class ManifestAssetBundle implements AssetBundle {
emptyAssetManifest.buffer.asUint8List(0, emptyAssetManifest.lengthInBytes),
),
kind: AssetKind.regular,
transformers: const <AssetTransformerEntry>[],
);
// Create .bin.json on web builds.
if (targetPlatform == TargetPlatform.web_javascript) {
entries[_kAssetManifestBinJsonFilename] = AssetBundleEntry(
DevFSStringContent('""'),
kind: AssetKind.regular,
transformers: const <AssetTransformerEntry>[],
);
}
return 0;
@ -423,6 +428,7 @@ class ManifestAssetBundle implements AssetBundle {
entries[variant.entryUri.path] ??= AssetBundleEntry(
DevFSFileContent(variantFile),
kind: variant.kind,
transformers: variant.transformers,
);
}
}
@ -456,6 +462,7 @@ class ManifestAssetBundle implements AssetBundle {
deferredComponentsEntries[componentName]![variant.entryUri.path] ??= AssetBundleEntry(
DevFSFileContent(variantFile),
kind: AssetKind.regular,
transformers: variant.transformers,
);
}
}
@ -471,7 +478,11 @@ class ManifestAssetBundle implements AssetBundle {
for (final _Asset asset in materialAssets) {
final File assetFile = asset.lookupAssetFile(_fileSystem);
assert(assetFile.existsSync(), 'Missing ${assetFile.path}');
entries[asset.entryUri.path] ??= AssetBundleEntry(DevFSFileContent(assetFile), kind: asset.kind);
entries[asset.entryUri.path] ??= AssetBundleEntry(
DevFSFileContent(assetFile),
kind: asset.kind,
transformers: const <AssetTransformerEntry>[],
);
}
// Update wildcard directories we can detect changes in them.
@ -534,6 +545,7 @@ class ManifestAssetBundle implements AssetBundle {
entries[key] = AssetBundleEntry(
content,
kind: assetKind,
transformers: const <AssetTransformerEntry>[],
);
}
@ -579,6 +591,7 @@ class ManifestAssetBundle implements AssetBundle {
hintString: 'copyrightsoftwaretothisinandorofthe',
),
kind: AssetKind.regular,
transformers: const<AssetTransformerEntry>[],
);
}
}
@ -684,6 +697,8 @@ class ManifestAssetBundle implements AssetBundle {
cache,
componentAssets,
assetsEntry.uri,
flavors: assetsEntry.flavors,
transformers: assetsEntry.transformers,
);
} else {
_parseAssetFromFile(
@ -693,6 +708,8 @@ class ManifestAssetBundle implements AssetBundle {
cache,
componentAssets,
assetsEntry.uri,
flavors: assetsEntry.flavors,
transformers: assetsEntry.transformers,
);
}
}
@ -863,6 +880,7 @@ class ManifestAssetBundle implements AssetBundle {
packageName: packageName,
attributedPackage: attributedPackage,
flavors: assetsEntry.flavors,
transformers: assetsEntry.transformers,
);
} else {
_parseAssetFromFile(
@ -875,6 +893,7 @@ class ManifestAssetBundle implements AssetBundle {
packageName: packageName,
attributedPackage: attributedPackage,
flavors: assetsEntry.flavors,
transformers: assetsEntry.transformers,
);
}
}
@ -900,6 +919,8 @@ class ManifestAssetBundle implements AssetBundle {
packageName: packageName,
attributedPackage: attributedPackage,
assetKind: AssetKind.shader,
flavors: <String>{},
transformers: <AssetTransformerEntry>[],
);
}
@ -914,6 +935,8 @@ class ManifestAssetBundle implements AssetBundle {
packageName: packageName,
attributedPackage: attributedPackage,
assetKind: AssetKind.model,
flavors: <String>{},
transformers: <AssetTransformerEntry>[],
);
}
@ -927,6 +950,8 @@ class ManifestAssetBundle implements AssetBundle {
packageName,
attributedPackage,
assetKind: AssetKind.font,
flavors: <String>{},
transformers: <AssetTransformerEntry>[],
);
final File baseAssetFile = baseAsset.lookupAssetFile(_fileSystem);
if (!baseAssetFile.existsSync()) {
@ -949,7 +974,8 @@ class ManifestAssetBundle implements AssetBundle {
Uri assetUri, {
String? packageName,
Package? attributedPackage,
Set<String>? flavors,
required Set<String> flavors,
required List<AssetTransformerEntry> transformers,
}) {
final String directoryPath;
try {
@ -985,6 +1011,7 @@ class ManifestAssetBundle implements AssetBundle {
attributedPackage: attributedPackage,
originUri: assetUri,
flavors: flavors,
transformers: transformers,
);
}
}
@ -1000,7 +1027,8 @@ class ManifestAssetBundle implements AssetBundle {
String? packageName,
Package? attributedPackage,
AssetKind assetKind = AssetKind.regular,
Set<String>? flavors,
required Set<String> flavors,
required List<AssetTransformerEntry> transformers,
}) {
final _Asset asset = _resolveAsset(
packageConfig,
@ -1011,6 +1039,7 @@ class ManifestAssetBundle implements AssetBundle {
assetKind: assetKind,
originUri: originUri,
flavors: flavors,
transformers: transformers,
);
_checkForFlavorConflicts(asset, result.keys.toList());
@ -1032,6 +1061,8 @@ class ManifestAssetBundle implements AssetBundle {
relativeUri: relativeUri,
package: attributedPackage,
kind: assetKind,
flavors: flavors,
transformers: transformers,
),
);
}
@ -1116,7 +1147,8 @@ class ManifestAssetBundle implements AssetBundle {
Package? attributedPackage, {
Uri? originUri,
AssetKind assetKind = AssetKind.regular,
Set<String>? flavors,
required Set<String> flavors,
required List<AssetTransformerEntry> transformers,
}) {
final String assetPath = _fileSystem.path.fromUri(assetUri);
if (assetUri.pathSegments.first == 'packages'
@ -1130,6 +1162,7 @@ class ManifestAssetBundle implements AssetBundle {
assetKind: assetKind,
originUri: originUri,
flavors: flavors,
transformers: transformers,
);
if (packageAsset != null) {
return packageAsset;
@ -1146,6 +1179,7 @@ class ManifestAssetBundle implements AssetBundle {
originUri: originUri,
kind: assetKind,
flavors: flavors,
transformers: transformers,
);
}
@ -1156,6 +1190,7 @@ class ManifestAssetBundle implements AssetBundle {
AssetKind assetKind = AssetKind.regular,
Uri? originUri,
Set<String>? flavors,
List<AssetTransformerEntry>? transformers,
}) {
assert(assetUri.pathSegments.first == 'packages');
if (assetUri.pathSegments.length > 1) {
@ -1171,6 +1206,7 @@ class ManifestAssetBundle implements AssetBundle {
kind: assetKind,
originUri: originUri,
flavors: flavors,
transformers: transformers,
);
}
}
@ -1193,7 +1229,10 @@ class _Asset {
required this.package,
this.kind = AssetKind.regular,
Set<String>? flavors,
}): originUri = originUri ?? entryUri, flavors = flavors ?? const <String>{};
List<AssetTransformerEntry>? transformers,
}) : originUri = originUri ?? entryUri,
flavors = flavors ?? const <String>{},
transformers = transformers ?? const <AssetTransformerEntry>[];
final String baseDir;
@ -1214,6 +1253,8 @@ class _Asset {
final Set<String> flavors;
final List<AssetTransformerEntry> transformers;
File lookupAssetFile(FileSystem fileSystem) {
return fileSystem.file(fileSystem.path.join(baseDir, fileSystem.path.fromUri(relativeUri)));
}

View file

@ -498,3 +498,19 @@ bool setEquals<T>(Set<T>? a, Set<T>? b) {
}
return true;
}
/// Tests for shallow equality on two lists.
bool listEquals<T>(List<T> a, List<T> b) {
if (identical(a, b)) {
return true;
}
if (a.length != b.length) {
return false;
}
for (int index = 0; index < a.length; index++) {
if (a[index] != b[index]) {
return false;
}
}
return true;
}

View file

@ -4,14 +4,18 @@
import 'package:pool/pool.dart';
import '../../artifacts.dart';
import '../../asset.dart';
import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/logger.dart';
import '../../build_info.dart';
import '../../convert.dart';
import '../../devfs.dart';
import '../../flutter_manifest.dart';
import '../build_system.dart';
import '../depfile.dart';
import '../tools/asset_transformer.dart';
import '../tools/scene_importer.dart';
import '../tools/shader_compiler.dart';
import 'common.dart';
@ -93,19 +97,29 @@ Future<Depfile> copyAssets(
fileSystem: environment.fileSystem,
artifacts: environment.artifacts,
);
final AssetTransformer assetTransformer = AssetTransformer(
processManager: environment.processManager,
fileSystem: environment.fileSystem,
dartBinaryPath: environment.artifacts.getArtifactPath(Artifact.engineDartBinary),
);
final Map<String, AssetBundleEntry> assetEntries = <String, AssetBundleEntry>{
...assetBundle.entries,
...additionalContent.map((String key, DevFSContent value) {
return MapEntry<String, AssetBundleEntry>(
key,
AssetBundleEntry(value, kind: AssetKind.regular),
AssetBundleEntry(
value,
kind: AssetKind.regular,
transformers: const <AssetTransformerEntry>[],
),
);
}),
if (skslBundle != null)
kSkSLShaderBundlePath: AssetBundleEntry(
skslBundle,
kind: AssetKind.regular,
transformers: const <AssetTransformerEntry>[],
),
};
@ -128,7 +142,18 @@ Future<Depfile> copyAssets(
bool doCopy = true;
switch (entry.value.kind) {
case AssetKind.regular:
break;
if (entry.value.transformers.isNotEmpty) {
final AssetTransformationFailure? failure = await assetTransformer.transformAsset(
asset: content.file as File,
outputPath: file.path,
workingDirectory: environment.projectDir.path,
transformerEntries: entry.value.transformers,
);
doCopy = false;
if (failure != null) {
throwToolExit(failure.message);
}
}
case AssetKind.font:
doCopy = !await iconTreeShaker.subsetFont(
input: content.file as File,

View file

@ -0,0 +1,143 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:process/process.dart';
import '../../base/error_handling_io.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../flutter_manifest.dart';
import '../build_system.dart';
/// Applies a series of user-specified asset-transforming packages to an asset file.
final class AssetTransformer {
AssetTransformer({
required ProcessManager processManager,
required FileSystem fileSystem,
required String dartBinaryPath,
}) : _processManager = processManager,
_fileSystem = fileSystem,
_dartBinaryPath = dartBinaryPath;
final ProcessManager _processManager;
final FileSystem _fileSystem;
final String _dartBinaryPath;
/// The [Source] inputs that targets using this should depend on.
///
/// See [Target.inputs].
static const List<Source> inputs = <Source>[
Source.pattern(
'{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/asset_transformer.dart',
),
];
/// Applies, in sequence, a list of transformers to an [asset] and then copies
/// the output to [outputPath].
Future<AssetTransformationFailure?> transformAsset({
required File asset,
required String outputPath,
required String workingDirectory,
required List<AssetTransformerEntry> transformerEntries,
}) async {
String getTempFilePath(int transformStep) {
final String basename = _fileSystem.path.basename(asset.path);
final String ext = _fileSystem.path.extension(asset.path);
return '$basename-transformOutput$transformStep$ext';
}
File tempInputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(0));
await asset.copy(tempInputFile.path);
File tempOutputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(1));
try {
for (final (int i, AssetTransformerEntry transformer) in transformerEntries.indexed) {
final AssetTransformationFailure? transformerFailure = await _applyTransformer(
asset: tempInputFile,
output: tempOutputFile,
transformer: transformer,
workingDirectory: workingDirectory,
);
if (transformerFailure != null) {
return AssetTransformationFailure(
'User-defined transformation of asset "${asset.path}" failed.\n'
'${transformerFailure.message}',
);
}
ErrorHandlingFileSystem.deleteIfExists(tempInputFile);
if (i == transformerEntries.length - 1) {
await tempOutputFile.copy(outputPath);
} else {
tempInputFile = tempOutputFile;
tempOutputFile = _fileSystem.systemTempDirectory.childFile(getTempFilePath(i+2));
}
}
} finally {
ErrorHandlingFileSystem.deleteIfExists(tempInputFile);
ErrorHandlingFileSystem.deleteIfExists(tempOutputFile);
}
return null;
}
Future<AssetTransformationFailure?> _applyTransformer({
required File asset,
required File output,
required AssetTransformerEntry transformer,
required String workingDirectory,
}) async {
final List<String> transformerArguments = <String>[
'--input=${asset.absolute.path}',
'--output=${output.absolute.path}',
...?transformer.args,
];
final List<String> command = <String>[
_dartBinaryPath,
'run',
transformer.package,
...transformerArguments,
];
final ProcessResult result = await _processManager.run(
command,
workingDirectory: workingDirectory,
);
final String stdout = result.stdout as String;
final String stderr = result.stderr as String;
if (result.exitCode != 0) {
return AssetTransformationFailure(
'Transformer process terminated with non-zero exit code: ${result.exitCode}\n'
'Transformer package: ${transformer.package}\n'
'Full command: ${command.join(' ')}\n'
'stdout:\n$stdout\n'
'stderr:\n$stderr'
);
}
if (!_fileSystem.file(output).existsSync()) {
return AssetTransformationFailure(
'Asset transformer ${transformer.package} did not produce an output file.\n'
'Input file provided to transformer: "${asset.path}"\n'
'Expected output file at: "${output.absolute.path}"\n'
'Full command: ${command.join(' ')}\n'
'stdout:\n$stdout\n'
'stderr:\n$stderr',
);
}
return null;
}
}
final class AssetTransformationFailure {
const AssetTransformationFailure(this.message);
final String message;
}

View file

@ -835,10 +835,11 @@ class AssetsEntry {
int get hashCode => Object.hashAll(<Object?>[
uri.hashCode,
Object.hashAllUnordered(flavors),
Object.hashAll(transformers),
]);
@override
String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors)';
String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors, transformers: $transformers)';
}

View file

@ -0,0 +1,373 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:args/args.dart';
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_system/tools/asset_transformer.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import '../../../src/common.dart';
import '../../../src/fake_process_manager.dart';
void main() {
testWithoutContext('Invokes dart properly', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final BufferLogger logger = BufferLogger.test();
final Artifacts artifacts = Artifacts.test();
final File asset = fileSystem.file('asset.txt')..createSync()..writeAsStringSync('hello world');
const String outputPath = 'output.txt';
final FakeProcessManager processManager =
FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: <String>[
artifacts.getArtifactPath(Artifact.engineDartBinary),
'run',
'my_copy_transformer',
'--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
'--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
'-f',
'--my_option',
'my_option_value',
],
onRun: (List<String> args) {
final ArgResults parsedArgs = (ArgParser()
..addOption('input')
..addOption('output')
..addFlag('foo', abbr: 'f')
..addOption('my_option'))
.parse(args);
fileSystem.file(parsedArgs['input']).copySync(parsedArgs['output'] as String);
},
),
]);
final AssetTransformer transformer = AssetTransformer(
processManager: processManager,
fileSystem: fileSystem,
dartBinaryPath: artifacts.getArtifactPath(Artifact.engineDartBinary),
);
final AssetTransformationFailure? transformationFailure = await transformer.transformAsset(
asset: asset,
outputPath: outputPath,
workingDirectory: fileSystem.currentDirectory.path,
transformerEntries: <AssetTransformerEntry>[
const AssetTransformerEntry(
package: 'my_copy_transformer',
args: <String>[
'-f',
'--my_option',
'my_option_value',
],
)
],
);
expect(transformationFailure, isNull, reason: logger.errorText);
expect(processManager, hasNoRemainingExpectations);
expect(fileSystem.file(outputPath).readAsStringSync(), 'hello world');
});
testWithoutContext('logs useful error information when transformation process returns a nonzero exit code', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final Artifacts artifacts = Artifacts.test();
final File asset = fileSystem.file('asset.txt')..createSync();
const String outputPath = 'output.txt';
final String dartBinaryPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
final FakeProcessManager processManager =
FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: <String>[
dartBinaryPath,
'run',
'my_copy_transformer',
'--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
'--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
],
onRun: (List<String> args) {
final ArgResults parsedArgs = (ArgParser()
..addOption('input')
..addOption('output'))
.parse(args);
fileSystem.file(parsedArgs['input']).copySync(parsedArgs['output'] as String);
},
exitCode: 1,
stdout: 'Beginning transformation',
stderr: 'Something went wrong',
),
]);
final AssetTransformer transformer = AssetTransformer(
processManager: processManager,
fileSystem: fileSystem,
dartBinaryPath: dartBinaryPath,
);
final AssetTransformationFailure? failure = await transformer.transformAsset(
asset: asset,
outputPath: outputPath,
workingDirectory: fileSystem.currentDirectory.path,
transformerEntries: <AssetTransformerEntry>[
const AssetTransformerEntry(
package: 'my_copy_transformer',
args: <String>[],
)
],
);
expect(asset, exists);
expect(processManager, hasNoRemainingExpectations);
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
stdout:
Beginning transformation
stderr:
Something went wrong''');
});
testWithoutContext('prints error message when the transformer does not produce an output file', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final Artifacts artifacts = Artifacts.test();
final File asset = fileSystem.file('asset.txt')..createSync();
const String outputPath = 'output.txt';
final String dartBinaryPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
final FakeProcessManager processManager =
FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: <String>[
dartBinaryPath,
'run',
'my_transformer',
'--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
'--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
],
onRun: (_) {
// Do nothing.
},
stderr: 'Transformation failed, but I forgot to exit with a non-zero code.'
),
]);
final AssetTransformer transformer = AssetTransformer(
processManager: processManager,
fileSystem: fileSystem,
dartBinaryPath: dartBinaryPath,
);
final AssetTransformationFailure? failure = await transformer.transformAsset(
asset: asset,
outputPath: outputPath,
workingDirectory: fileSystem.currentDirectory.path,
transformerEntries: <AssetTransformerEntry>[
const AssetTransformerEntry(
package: 'my_transformer',
args: <String>[],
)
],
);
expect(processManager, hasNoRemainingExpectations);
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
stdout:
stderr:
Transformation failed, but I forgot to exit with a non-zero code.'''
);
});
testWithoutContext('correctly chains transformations when there are multiple of them', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final Artifacts artifacts = Artifacts.test();
final File asset = fileSystem.file('asset.txt')
..createSync()
..writeAsStringSync('ABC');
const String outputPath = 'output.txt';
final String dartBinaryPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: <String>[
dartBinaryPath,
'run',
'my_lowercase_transformer',
'--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
'--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
],
onRun: (List<String> args) {
final ArgResults parsedArgs = (ArgParser()
..addOption('input')
..addOption('output'))
.parse(args);
final String inputFileContents = fileSystem.file(parsedArgs['input']).readAsStringSync();
fileSystem.file(parsedArgs['output'])
..createSync()
..writeAsStringSync(inputFileContents.toLowerCase());
},
),
FakeCommand(
command: <String>[
dartBinaryPath,
'run',
'my_distance_from_ascii_a_transformer',
'--input=/.tmp_rand0/asset.txt-transformOutput1.txt',
'--output=/.tmp_rand0/asset.txt-transformOutput2.txt',
],
onRun: (List<String> args) {
final ArgResults parsedArgs = (ArgParser()
..addOption('input')
..addOption('output'))
.parse(args);
final String inputFileContents = fileSystem.file(parsedArgs['input']).readAsStringSync();
final StringBuffer outputContents = StringBuffer();
for (int i = 0; i < inputFileContents.length; i++) {
outputContents.write(inputFileContents.codeUnitAt(i) - 'a'.codeUnits.first);
}
fileSystem.file(parsedArgs['output'])
..createSync()
..writeAsStringSync(outputContents.toString());
},
),
]);
final AssetTransformer transformer = AssetTransformer(
processManager: processManager,
fileSystem: fileSystem,
dartBinaryPath: dartBinaryPath,
);
final AssetTransformationFailure? failure = await transformer.transformAsset(
asset: asset,
outputPath: outputPath,
workingDirectory: fileSystem.currentDirectory.path,
transformerEntries: <AssetTransformerEntry>[
const AssetTransformerEntry(
package: 'my_lowercase_transformer',
args: <String>[],
),
const AssetTransformerEntry(
package: 'my_distance_from_ascii_a_transformer',
args: <String>[],
),
],
);
expect(processManager, hasNoRemainingExpectations);
expect(failure, isNull);
expect(fileSystem.file(outputPath).readAsStringSync(), '012');
});
testWithoutContext('prints an error when a transformer in a chain (thats not the first) does not produce an output', () async {
final FileSystem fileSystem = MemoryFileSystem();
final Artifacts artifacts = Artifacts.test();
final File asset = fileSystem.file('asset.txt')
..createSync()
..writeAsStringSync('ABC');
const String outputPath = 'output.txt';
final String dartBinaryPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: <String>[
dartBinaryPath,
'run',
'my_lowercase_transformer',
'--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
'--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
],
onRun: (List<String> args) {
final ArgResults parsedArgs = (ArgParser()
..addOption('input')
..addOption('output'))
.parse(args);
final String inputFileContents = fileSystem.file(parsedArgs['input']).readAsStringSync();
fileSystem.file(parsedArgs['output'])
..createSync()
..writeAsStringSync(inputFileContents.toLowerCase());
},
),
FakeCommand(
command: <String>[
dartBinaryPath,
'run',
'my_distance_from_ascii_a_transformer',
'--input=/.tmp_rand0/asset.txt-transformOutput1.txt',
'--output=/.tmp_rand0/asset.txt-transformOutput2.txt',
],
onRun: (List<String> args) {
// Do nothing.
},
stderr: 'Transformation failed, but I forgot to exit with a non-zero code.'
),
]);
final AssetTransformer transformer = AssetTransformer(
processManager: processManager,
fileSystem: fileSystem,
dartBinaryPath: dartBinaryPath,
);
final AssetTransformationFailure? failure = await transformer.transformAsset(
asset: asset,
outputPath: outputPath,
workingDirectory: fileSystem.currentDirectory.path,
transformerEntries: <AssetTransformerEntry>[
const AssetTransformerEntry(
package: 'my_lowercase_transformer',
args: <String>[],
),
const AssetTransformerEntry(
package: 'my_distance_from_ascii_a_transformer',
args: <String>[],
),
],
);
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
stdout:
stderr:
Transformation failed, but I forgot to exit with a non-zero code.'''
);
expect(processManager, hasNoRemainingExpectations);
expect(fileSystem.file(outputPath), isNot(exists));
expect(fileSystem.systemTempDirectory.listSync(), isEmpty);
});
}

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:args/args.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
@ -9,19 +10,24 @@ import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/depfile.dart';
import 'package:flutter_tools/src/build_system/targets/assets.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import '../../../src/common.dart';
import '../../../src/context.dart';
import '../../../src/fake_process_manager.dart';
void main() {
late Environment environment;
late FileSystem fileSystem;
late BufferLogger logger;
setUp(() {
fileSystem = MemoryFileSystem.test();
@ -53,6 +59,7 @@ flutter:
- assets/foo/bar.png
- assets/wildcard/
''');
logger = BufferLogger.test();
});
testUsingContext('includes LICENSE file inputs in dependencies', () async {
@ -157,6 +164,153 @@ flutter:
});
});
testUsingContext('transforms assets declared with transformers', () async {
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: globals.platform,
fileSystem: fileSystem,
userMessages: UserMessages(),
);
final Environment environment = Environment.test(
fileSystem.currentDirectory,
processManager: globals.processManager,
artifacts: Artifacts.test(),
fileSystem: fileSystem,
logger: logger,
platform: globals.platform,
defines: <String, String>{},
);
await fileSystem.file('.packages').create();
fileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync('''
name: example
flutter:
assets:
- path: input.txt
transformers:
- package: my_capitalizer_transformer
args: ["-a", "-b", "--color", "green"]
''');
fileSystem.file('input.txt')
..createSync(recursive: true)
..writeAsStringSync('abc');
await const CopyAssets().build(environment);
expect(logger.errorText, isEmpty);
expect(globals.processManager, hasNoRemainingExpectations);
expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/input.txt'), exists);
}, overrides: <Type, Generator> {
Logger: () => logger,
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.list(
<FakeCommand>[
FakeCommand(
command: <Pattern>[
Artifacts.test().getArtifactPath(Artifact.engineDartBinary),
'run',
'my_capitalizer_transformer',
RegExp('--input=.*'),
RegExp('--output=.*'),
'-a',
'-b',
'--color',
'green',
],
onRun: (List<String> args) {
final ArgResults parsedArgs = (ArgParser()
..addOption('input')
..addOption('output')
..addOption('color')
..addFlag('aaa', abbr: 'a')
..addFlag('bbb', abbr: 'b'))
.parse(args);
expect(parsedArgs['aaa'], true);
expect(parsedArgs['bbb'], true);
expect(parsedArgs['color'], 'green');
final File input = fileSystem.file(parsedArgs['input'] as String);
expect(input, exists);
final String inputContents = input.readAsStringSync();
expect(inputContents, 'abc');
fileSystem.file(parsedArgs['output'])
..createSync()
..writeAsStringSync(inputContents.toUpperCase());
},
),
],
),
});
testUsingContext('exits tool if an asset transformation fails', () async {
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: globals.platform,
fileSystem: fileSystem,
userMessages: UserMessages(),
);
final Environment environment = Environment.test(
fileSystem.currentDirectory,
processManager: globals.processManager,
artifacts: Artifacts.test(),
fileSystem: fileSystem,
logger: logger,
platform: globals.platform,
defines: <String, String>{},
);
await fileSystem.file('.packages').create();
fileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync('''
name: example
flutter:
assets:
- path: input.txt
transformers:
- package: my_transformer
args: ["-a", "-b", "--color", "green"]
''');
await fileSystem.file('input.txt').create(recursive: true);
await expectToolExitLater(
const CopyAssets().build(environment),
startsWith('User-defined transformation of asset "/input.txt" failed.\n'),
);
expect(globals.processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator> {
Logger: () => logger,
FileSystem: () => fileSystem,
Platform: () => FakePlatform(),
ProcessManager: () => FakeProcessManager.list(
<FakeCommand>[
FakeCommand(
command: <Pattern>[
Artifacts.test().getArtifactPath(Artifact.engineDartBinary),
'run',
'my_transformer',
RegExp('--input=.*'),
RegExp('--output=.*'),
'-a',
'-b',
'--color',
'green',
],
exitCode: 1,
),
],
),
});
testUsingContext('Throws exception if pubspec contains missing files', () async {
fileSystem.file('pubspec.yaml')
..createSync()
@ -182,7 +336,7 @@ flutter:
null,
targetPlatform: TargetPlatform.android,
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
logger: logger,
), isNull);
});
@ -193,7 +347,7 @@ flutter:
'does_not_exist.sksl',
targetPlatform: TargetPlatform.android,
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
logger: logger,
), throwsException);
});

View file

@ -20,6 +20,7 @@ import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/tools/shader_compiler.dart';
import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:package_config/package_config.dart';
import 'package:test/fake.dart';
@ -623,10 +624,12 @@ void main() {
..entries['foo.frag'] = AssetBundleEntry(
DevFSByteContent(<int>[1, 2, 3, 4]),
kind: AssetKind.shader,
transformers: const <AssetTransformerEntry>[],
)
..entries['not.frag'] = AssetBundleEntry(
DevFSByteContent(<int>[1, 2, 3, 4]),
kind: AssetKind.regular,
transformers: const <AssetTransformerEntry>[],
);
final UpdateFSReport report = await devFS.update(
@ -680,6 +683,7 @@ void main() {
..entries['FontManifest.json'] = AssetBundleEntry(
DevFSByteContent(<int>[1, 2, 3, 4]),
kind: AssetKind.regular,
transformers: const <AssetTransformerEntry>[],
);
final UpdateFSReport report = await devFS.update(