blob: a04ad60dbdcf8fa3b6e82b1d888c65508f1fc9c4 [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:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';
import 'package:pool/pool.dart';
import 'asset.dart';
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/icon_tree_shaker.dart';
import 'cache.dart';
import 'convert.dart';
import 'devfs.dart';
import 'globals.dart' as globals;
import 'project.dart';
String get defaultMainPath => globals.fs.path.join('lib', 'main.dart');
const String defaultAssetBasePath = '.';
const String defaultManifestPath = 'pubspec.yaml';
String get defaultDepfilePath => globals.fs.path.join(getBuildDirectory(), 'snapshot_blob.bin.d');
String getDefaultApplicationKernelPath({ @required bool trackWidgetCreation }) {
return getKernelPathForTransformerOptions(
globals.fs.path.join(getBuildDirectory(), 'app.dill'),
trackWidgetCreation: trackWidgetCreation,
);
}
String getDefaultCachedKernelPath({
@required bool trackWidgetCreation,
@required List<String> dartDefines,
@required List<String> extraFrontEndOptions,
}) {
final StringBuffer buffer = StringBuffer();
buffer.writeAll(dartDefines);
buffer.writeAll(extraFrontEndOptions ?? <String>[]);
String buildPrefix = '';
if (buffer.isNotEmpty) {
final String output = buffer.toString();
final Digest digest = md5.convert(utf8.encode(output));
buildPrefix = '${hex.encode(digest.bytes)}.';
}
return getKernelPathForTransformerOptions(
globals.fs.path.join(getBuildDirectory(), '${buildPrefix}cache.dill'),
trackWidgetCreation: trackWidgetCreation,
);
}
String getKernelPathForTransformerOptions(
String path, {
@required bool trackWidgetCreation,
}) {
if (trackWidgetCreation) {
path += '.track.dill';
}
return path;
}
const String defaultPrivateKeyPath = 'privatekey.der';
/// 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,
BuildInfo buildInfo,
String mainPath,
String manifestPath = defaultManifestPath,
String applicationKernelFilePath,
String depfilePath,
String privateKeyPath = defaultPrivateKeyPath,
String assetDirPath,
String packagesPath,
bool precompiledSnapshot = false,
bool reportLicensedPackages = false,
bool trackWidgetCreation = false,
List<String> extraFrontEndOptions = const <String>[],
List<String> extraGenSnapshotOptions = const <String>[],
List<String> fileSystemRoots,
String fileSystemScheme,
@required bool treeShakeIcons,
}) async {
mainPath ??= defaultMainPath;
depfilePath ??= defaultDepfilePath;
assetDirPath ??= getAssetBuildDirectory();
packagesPath ??= globals.fs.path.absolute('.packages');
final FlutterProject flutterProject = FlutterProject.current();
await buildWithAssemble(
buildMode: buildInfo.mode,
targetPlatform: platform,
mainPath: mainPath,
flutterProject: flutterProject,
outputDir: assetDirPath,
depfilePath: depfilePath,
precompiled: precompiledSnapshot,
trackWidgetCreation: trackWidgetCreation,
treeShakeIcons: treeShakeIcons,
dartDefines: buildInfo.dartDefines,
);
// 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;
}
}
/// Build an application bundle using flutter assemble.
///
/// This is a temporary shim to migrate the build implementations.
Future<void> buildWithAssemble({
@required FlutterProject flutterProject,
@required BuildMode buildMode,
@required TargetPlatform targetPlatform,
@required String mainPath,
@required String outputDir,
@required String depfilePath,
@required bool precompiled,
bool trackWidgetCreation,
@required bool treeShakeIcons,
List<String> dartDefines,
}) async {
// If the precompiled flag was not passed, force us into debug mode.
buildMode = precompiled ? buildMode : BuildMode.debug;
final Environment environment = Environment(
projectDir: flutterProject.directory,
outputDir: globals.fs.directory(outputDir),
buildDir: flutterProject.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>{
kTargetFile: mainPath,
kBuildMode: getNameForBuildMode(buildMode),
kTargetPlatform: getNameForTargetPlatform(targetPlatform),
kTrackWidgetCreation: trackWidgetCreation?.toString(),
kIconTreeShakerFlag: treeShakeIcons ? 'true' : null,
if (dartDefines != null && dartDefines.isNotEmpty)
kDartDefines: encodeDartDefines(dartDefines),
},
artifacts: globals.artifacts,
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
);
final Target target = buildMode == BuildMode.debug
? const CopyFlutterBundle()
: const ReleaseCopyFlutterBundle();
final BuildResult result = await globals.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.');
}
if (depfilePath != null) {
final Depfile depfile = Depfile(result.inputFiles, result.outputFiles);
final File outputDepfile = globals.fs.file(depfilePath);
if (!outputDepfile.parent.existsSync()) {
outputDepfile.parent.createSync(recursive: true);
}
final DepfileService depfileService = DepfileService(
fileSystem: globals.fs,
logger: globals.logger,
);
depfileService.writeToFile(depfile, outputDepfile);
}
}
Future<AssetBundle> buildAssets({
String manifestPath,
String assetDirPath,
@required String packagesPath,
bool includeDefaultFonts = true,
bool reportLicensedPackages = false,
}) async {
assetDirPath ??= getAssetBuildDirectory();
packagesPath ??= globals.fs.path.absolute(packagesPath);
// Build the asset bundle.
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
final int result = await assetBundle.build(
manifestPath: manifestPath,
assetDirPath: assetDirPath,
packagesPath: packagesPath,
includeDefaultFonts: includeDefaultFonts,
reportLicensedPackages: reportLicensedPackages,
);
if (result != 0) {
return null;
}
return assetBundle;
}
Future<void> writeBundle(
Directory bundleDir,
Map<String, DevFSContent> assetEntries,
{ Logger loggerOverride }
) async {
loggerOverride ??= globals.logger;
if (bundleDir.existsSync()) {
try {
bundleDir.deleteSync(recursive: true);
} on FileSystemException catch (err) {
loggerOverride.printError(
'Failed to clean up asset directory ${bundleDir.path}: $err\n'
'To clean build artifacts, use the command "flutter clean".'
);
}
}
bundleDir.createSync(recursive: true);
// 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));
file.parent.createSync(recursive: true);
await file.writeAsBytes(await entry.value.contentsAsBytes());
} finally {
resource.release();
}
}));
}