// 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'; import 'shader_compiler.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, required ShaderTarget shaderTarget, }) 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 ShaderCompiler shaderCompiler = ShaderCompiler( 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, }; final Map<String, AssetKind> entryKinds = <String, AssetKind>{ ...assetBundle.entryKinds, }; 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)); final AssetKind assetKind = entryKinds[entry.key] ?? AssetKind.regular; 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); bool doCopy = true; switch (assetKind) { case AssetKind.regular: break; case AssetKind.font: doCopy = !await iconTreeShaker.subsetFont( input: content.file as File, outputPath: file.path, relativePath: entry.key, ); break; case AssetKind.shader: doCopy = !await shaderCompiler.compileShader( input: content.file as File, outputPath: file.path, target: shaderTarget, ); break; } if (doCopy) { 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, ...ShaderCompiler.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, shaderTarget: ShaderTarget.sksl, ); final DepfileService depfileService = DepfileService( fileSystem: environment.fileSystem, logger: environment.logger, ); depfileService.writeToFile( depfile, environment.buildDir.childFile('flutter_assets.d'), ); } }