// 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:meta/meta.dart'; import 'package:pool/pool.dart'; import 'asset.dart' hide defaultManifestPath; import 'base/common.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'build_info.dart'; import 'build_system/build_system.dart'; import 'build_system/depfile.dart'; import 'build_system/targets/common.dart'; import 'build_system/targets/scene_importer.dart'; import 'build_system/targets/shader_compiler.dart'; import 'bundle.dart'; import 'cache.dart'; import 'devfs.dart'; import 'globals.dart' as globals; import 'project.dart'; /// Provides a `build` method that builds the bundle. class BundleBuilder { /// Builds the bundle for the given target platform. /// /// The default `mainPath` is `lib/main.dart`. /// The default `manifestPath` is `pubspec.yaml` Future<void> build({ required TargetPlatform platform, required BuildInfo buildInfo, FlutterProject? project, String? mainPath, String manifestPath = defaultManifestPath, String? applicationKernelFilePath, String? depfilePath, String? assetDirPath, @visibleForTesting BuildSystem? buildSystem, }) async { project ??= FlutterProject.current(); mainPath ??= defaultMainPath; depfilePath ??= defaultDepfilePath; assetDirPath ??= getAssetBuildDirectory(); buildSystem ??= globals.buildSystem; // If the precompiled flag was not passed, force us into debug mode. final Environment environment = Environment( projectDir: project.directory, outputDir: globals.fs.directory(assetDirPath), buildDir: project.dartTool.childDirectory('flutter_build'), cacheDir: globals.cache.getRoot(), flutterRootDir: globals.fs.directory(Cache.flutterRoot), engineVersion: globals.artifacts!.isLocalEngine ? null : globals.flutterVersion.engineRevision, defines: <String, String>{ // used by the KernelSnapshot target kTargetPlatform: getNameForTargetPlatform(platform), kTargetFile: mainPath, kDeferredComponents: 'false', ...buildInfo.toBuildSystemEnvironment(), }, artifacts: globals.artifacts!, fileSystem: globals.fs, logger: globals.logger, processManager: globals.processManager, usage: globals.flutterUsage, platform: globals.platform, generateDartPluginRegistry: true, ); final Target target = buildInfo.mode == BuildMode.debug ? const CopyFlutterBundle() : const ReleaseCopyFlutterBundle(); final BuildResult result = await buildSystem.build(target, environment); if (!result.success) { for (final ExceptionMeasurement measurement in result.exceptions.values) { globals.printError('Target ${measurement.target} failed: ${measurement.exception}', stackTrace: measurement.fatal ? measurement.stackTrace : null, ); } throwToolExit('Failed to build bundle.'); } final Depfile depfile = Depfile(result.inputFiles, result.outputFiles); final File outputDepfile = globals.fs.file(depfilePath); if (!outputDepfile.parent.existsSync()) { outputDepfile.parent.createSync(recursive: true); } environment.depFileService.writeToFile(depfile, outputDepfile); // Work around for flutter_tester placing kernel artifacts in odd places. if (applicationKernelFilePath != null) { final File outputDill = globals.fs.directory(assetDirPath).childFile('kernel_blob.bin'); if (outputDill.existsSync()) { outputDill.copySync(applicationKernelFilePath); } } return; } } Future<AssetBundle?> buildAssets({ required String manifestPath, String? assetDirPath, String? packagesPath, TargetPlatform? targetPlatform, }) async { assetDirPath ??= getAssetBuildDirectory(); packagesPath ??= globals.fs.path.absolute('.packages'); // Build the asset bundle. final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle(); final int result = await assetBundle.build( manifestPath: manifestPath, packagesPath: packagesPath, targetPlatform: targetPlatform, ); if (result != 0) { return null; } return assetBundle; } Future<void> writeBundle( Directory bundleDir, Map<String, DevFSContent> assetEntries, Map<String, AssetKind> entryKinds, { Logger? loggerOverride, required TargetPlatform targetPlatform, }) async { loggerOverride ??= globals.logger; if (bundleDir.existsSync()) { try { bundleDir.deleteSync(recursive: true); } on FileSystemException catch (err) { loggerOverride.printWarning( 'Failed to clean up asset directory ${bundleDir.path}: $err\n' 'To clean build artifacts, use the command "flutter clean".' ); } } bundleDir.createSync(recursive: true); final ShaderCompiler shaderCompiler = ShaderCompiler( processManager: globals.processManager, logger: globals.logger, fileSystem: globals.fs, artifacts: globals.artifacts!, ); final SceneImporter sceneImporter = SceneImporter( processManager: globals.processManager, logger: globals.logger, fileSystem: globals.fs, artifacts: globals.artifacts!, ); // Limit number of open files to avoid running out of file descriptors. final Pool pool = Pool(64); 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 = globals.fs.file(globals.fs.path.join(bundleDir.path, entry.key)); final AssetKind assetKind = entryKinds[entry.key] ?? AssetKind.regular; file.parent.createSync(recursive: true); final DevFSContent devFSContent = entry.value; if (devFSContent is DevFSFileContent) { final File input = devFSContent.file as File; bool doCopy = true; switch (assetKind) { case AssetKind.regular: break; case AssetKind.font: break; case AssetKind.shader: doCopy = !await shaderCompiler.compileShader( input: input, outputPath: file.path, target: ShaderTarget.sksl, // TODO(zanderso): configure impeller target when enabled. json: targetPlatform == TargetPlatform.web_javascript, ); case AssetKind.model: doCopy = !await sceneImporter.importScene( input: input, outputPath: file.path, ); } if (doCopy) { input.copySync(file.path); } } else { await file.writeAsBytes(await entry.value.contentsAsBytes()); } } finally { resource.release(); } })); }