mirror of
https://github.com/flutter/flutter
synced 2024-07-16 10:29:14 +00:00
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:
parent
15f8ef71d9
commit
4e814a5f3c
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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)';
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue