blob: 68a4cd02570b130826c12f49b81bb16a6decf6dd [file] [log] [blame]
// 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:pool/pool.dart';
import '../../asset.dart';
import '../../base/file_system.dart';
import '../../base/logger.dart';
import '../../build_info.dart';
import '../../convert.dart';
import '../../devfs.dart';
import '../build_system.dart';
import '../depfile.dart';
import 'common.dart';
import 'icon_tree_shaker.dart';
/// A helper function to copy an asset bundle into an [environment]'s output
/// directory.
///
/// Throws [Exception] if [AssetBundle.build] returns a non-zero exit code.
///
/// [additionalContent] may contain additional DevFS entries that will be
/// included in the final bundle, but not the AssetManifest.json file.
///
/// Returns a [Depfile] containing all assets used in the build.
Future<Depfile> copyAssets(Environment environment, Directory outputDirectory, {
Map<String, DevFSContent>? additionalContent,
required TargetPlatform targetPlatform,
BuildMode? buildMode,
}) async {
// Check for an SkSL bundle.
final String? shaderBundlePath = environment.defines[kBundleSkSLPath] ?? environment.inputs[kBundleSkSLPath];
final DevFSContent? skslBundle = processSkSLBundle(
shaderBundlePath,
engineVersion: environment.engineVersion,
fileSystem: environment.fileSystem,
logger: environment.logger,
targetPlatform: targetPlatform,
);
final File pubspecFile = environment.projectDir.childFile('pubspec.yaml');
// Only the default asset bundle style is supported in assemble.
final AssetBundle assetBundle = AssetBundleFactory.defaultInstance(
logger: environment.logger,
fileSystem: environment.fileSystem,
platform: environment.platform,
splitDeferredAssets: buildMode != BuildMode.debug && buildMode != BuildMode.jitRelease,
).createBundle();
final int resultCode = await assetBundle.build(
manifestPath: pubspecFile.path,
packagesPath: environment.projectDir.childFile('.packages').path,
deferredComponentsEnabled: environment.defines[kDeferredComponents] == 'true',
targetPlatform: targetPlatform,
);
if (resultCode != 0) {
throw Exception('Failed to bundle asset files.');
}
final Pool pool = Pool(kMaxOpenFiles);
final List<File> inputs = <File>[
// An asset manifest with no assets would have zero inputs if not
// for this pubspec file.
pubspecFile,
];
final List<File> outputs = <File>[];
final IconTreeShaker iconTreeShaker = IconTreeShaker(
environment,
assetBundle.entries[kFontManifestJson] as DevFSStringContent?,
processManager: environment.processManager,
logger: environment.logger,
fileSystem: environment.fileSystem,
artifacts: environment.artifacts,
);
final Map<String, DevFSContent> assetEntries = <String, DevFSContent>{
...assetBundle.entries,
...?additionalContent,
if (skslBundle != null)
kSkSLShaderBundlePath: skslBundle,
};
await Future.wait<void>(
assetEntries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
final PoolResource resource = await pool.request();
try {
// This will result in strange looking files, for example files with `/`
// on Windows or files that end up getting URI encoded such as `#.ext`
// to `%23.ext`. However, we have to keep it this way since the
// platform channels in the framework will URI encode these values,
// and the native APIs will look for files this way.
final File file = environment.fileSystem.file(
environment.fileSystem.path.join(outputDirectory.path, entry.key));
outputs.add(file);
file.parent.createSync(recursive: true);
final DevFSContent content = entry.value;
if (content is DevFSFileContent && content.file is File) {
inputs.add(content.file as File);
if (!await iconTreeShaker.subsetFont(
input: content.file as File,
outputPath: file.path,
relativePath: entry.key,
)) {
await (content.file as File).copy(file.path);
}
} else {
await file.writeAsBytes(await entry.value.contentsAsBytes());
}
} finally {
resource.release();
}
}));
// Copy deferred components assets only for release or profile builds.
// The assets are included in assetBundle.entries as a normal asset when
// building as debug.
if (environment.defines[kDeferredComponents] == 'true' && buildMode != null) {
await Future.wait<void>(
assetBundle.deferredComponentsEntries.entries.map<Future<void>>((MapEntry<String, Map<String, DevFSContent>> componentEntries) async {
final Directory componentOutputDir =
environment.projectDir
.childDirectory('build')
.childDirectory(componentEntries.key)
.childDirectory('intermediates')
.childDirectory('flutter');
await Future.wait<void>(
componentEntries.value.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
final PoolResource resource = await pool.request();
try {
// This will result in strange looking files, for example files with `/`
// on Windows or files that end up getting URI encoded such as `#.ext`
// to `%23.ext`. However, we have to keep it this way since the
// platform channels in the framework will URI encode these values,
// and the native APIs will look for files this way.
// If deferred components are disabled, then copy assets to regular location.
final File file = environment.defines[kDeferredComponents] == 'true'
? environment.fileSystem.file(
environment.fileSystem.path.join(componentOutputDir.path, buildMode.name, 'deferred_assets', 'flutter_assets', entry.key))
: environment.fileSystem.file(
environment.fileSystem.path.join(outputDirectory.path, entry.key));
outputs.add(file);
file.parent.createSync(recursive: true);
final DevFSContent content = entry.value;
if (content is DevFSFileContent && content.file is File) {
inputs.add(content.file as File);
if (!await iconTreeShaker.subsetFont(
input: content.file as File,
outputPath: file.path,
relativePath: entry.key,
)) {
await (content.file as File).copy(file.path);
}
} else {
await file.writeAsBytes(await entry.value.contentsAsBytes());
}
} finally {
resource.release();
}
}));
}));
}
final Depfile depfile = Depfile(inputs + assetBundle.additionalDependencies, outputs);
if (shaderBundlePath != null) {
final File skSLBundleFile = environment.fileSystem
.file(shaderBundlePath).absolute;
depfile.inputs.add(skSLBundleFile);
}
return depfile;
}
/// The path of the SkSL JSON bundle included in flutter_assets.
const String kSkSLShaderBundlePath = 'io.flutter.shaders.json';
/// Validate and process an SkSL asset bundle in a [DevFSContent].
///
/// Returns `null` if the bundle was not provided, otherwise attempts to
/// validate the bundle.
///
/// Throws [Exception] if the bundle is invalid due to formatting issues.
///
/// If the current target platform is different than the platform constructed
/// for the bundle, a warning will be printed.
DevFSContent? processSkSLBundle(String? bundlePath, {
required TargetPlatform targetPlatform,
required FileSystem fileSystem,
required Logger logger,
String? engineVersion,
}) {
if (bundlePath == null) {
return null;
}
// Step 1: check that file exists.
final File skSLBundleFile = fileSystem.file(bundlePath);
if (!skSLBundleFile.existsSync()) {
logger.printError('$bundlePath does not exist.');
throw Exception('SkSL bundle was invalid.');
}
// Step 2: validate top level bundle structure.
Map<String, Object>? bundle;
try {
final Object? rawBundle = json.decode(skSLBundleFile.readAsStringSync());
if (rawBundle is Map<String, Object>) {
bundle = rawBundle;
} else {
logger.printError('"$bundle" was not a JSON object: $rawBundle');
throw Exception('SkSL bundle was invalid.');
}
} on FormatException catch (err) {
logger.printError('"$bundle" was not a JSON object: $err');
throw Exception('SkSL bundle was invalid.');
}
// Step 3: Validate that:
// * The engine revision the bundle was compiled with
// is the same as the current revision.
// * The target platform is the same (this one is a warning only).
final String? bundleEngineRevision = bundle['engineRevision'] as String?;
if (bundleEngineRevision != engineVersion) {
logger.printError(
'Expected Flutter $bundleEngineRevision, but found $engineVersion\n'
'The SkSL bundle was produced with a different engine version. It must '
'be recreated for the current Flutter version.'
);
throw Exception('SkSL bundle was invalid');
}
final String? parsedPlatform = bundle['platform'] as String?;
TargetPlatform? bundleTargetPlatform;
if (parsedPlatform != null) {
bundleTargetPlatform = getTargetPlatformForName(parsedPlatform);
}
if (bundleTargetPlatform == null || bundleTargetPlatform != targetPlatform) {
logger.printError(
'The SkSL bundle was created for $bundleTargetPlatform, but the current '
'platform is $targetPlatform. This may lead to less efficient shader '
'caching.'
);
}
return DevFSStringContent(json.encode(<String, Object?>{
'data': bundle['data'],
}));
}
/// Copy the assets defined in the flutter manifest into a build directory.
class CopyAssets extends Target {
const CopyAssets();
@override
String get name => 'copy_assets';
@override
List<Target> get dependencies => const <Target>[
KernelSnapshot(),
];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/assets.dart'),
...IconTreeShaker.inputs,
];
@override
List<Source> get outputs => const <Source>[];
@override
List<String> get depfiles => const <String>[
'flutter_assets.d'
];
@override
Future<void> build(Environment environment) async {
final Directory output = environment
.buildDir
.childDirectory('flutter_assets');
output.createSync(recursive: true);
final Depfile depfile = await copyAssets(
environment,
output,
targetPlatform: TargetPlatform.android,
);
final DepfileService depfileService = DepfileService(
fileSystem: environment.fileSystem,
logger: environment.logger,
);
depfileService.writeToFile(
depfile,
environment.buildDir.childFile('flutter_assets.d'),
);
}
}