| // 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'), |
| ); |
| } |
| } |