// Copyright 2016 The Chromium 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 'dart:async'; import '../artifacts.dart'; import '../base/build.dart'; import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/process.dart'; import '../base/version.dart'; import '../build_info.dart'; import '../dart/package_map.dart'; import '../globals.dart'; import '../ios/plist_parser.dart'; import '../macos/xcode.dart'; import '../resident_runner.dart'; import '../runner/flutter_command.dart'; import 'build.dart'; class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmentArtifacts { BuildAotCommand({bool verboseHelp = false}) { usesTargetOption(); addBuildModeFlags(); usesPubOption(); argParser ..addOption('output-dir', defaultsTo: getAotBuildDirectory()) ..addOption('target-platform', defaultsTo: 'android-arm', allowed: <String>['android-arm', 'android-arm64', 'ios'], ) ..addFlag('quiet', defaultsTo: false) ..addFlag('report-timings', negatable: false, defaultsTo: false, help: 'Report timing information about build steps in machine readable form,', ) ..addMultiOption('ios-arch', splitCommas: true, defaultsTo: defaultIOSArchs.map<String>(getNameForDarwinArch), allowed: DarwinArch.values.map<String>(getNameForDarwinArch), help: 'iOS architectures to build.', ) ..addMultiOption(FlutterOptions.kExtraFrontEndOptions, splitCommas: true, hide: true, ) ..addMultiOption(FlutterOptions.kExtraGenSnapshotOptions, splitCommas: true, hide: true, ) ..addFlag('bitcode', defaultsTo: false, help: 'Build the AOT bundle with bitcode. Requires a compatible bitcode engine.', hide: true, ); // --track-widget-creation is exposed as a flag here to deal with build // invalidation issues, but it is ignored -- there are no plans to support // it for AOT mode. usesTrackWidgetCreation(hasEffect: false, verboseHelp: verboseHelp); } @override final String name = 'aot'; @override final String description = "Build an ahead-of-time compiled snapshot of your app's Dart code."; @override Future<FlutterCommandResult> runCommand() async { final String targetPlatform = argResults['target-platform']; final TargetPlatform platform = getTargetPlatformForName(targetPlatform); if (platform == null) { throwToolExit('Unknown platform: $targetPlatform'); } final bool bitcode = argResults['bitcode']; final BuildMode buildMode = getBuildMode(); if (bitcode) { if (platform != TargetPlatform.ios) { throwToolExit('Bitcode is only supported on iOS (TargetPlatform is $targetPlatform).'); } await validateBitcode(buildMode, platform); } Status status; if (!argResults['quiet']) { final String typeName = artifacts.getEngineType(platform, buildMode); status = logger.startProgress( 'Building AOT snapshot in ${getFriendlyModeName(getBuildMode())} mode ($typeName)...', timeout: timeoutConfiguration.slowOperation, ); } final String outputPath = argResults['output-dir'] ?? getAotBuildDirectory(); final bool reportTimings = argResults['report-timings']; try { String mainPath = findMainDartFile(targetFile); final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: reportTimings); // Compile to kernel. mainPath = await snapshotter.compileKernel( platform: platform, buildMode: buildMode, mainPath: mainPath, packagesPath: PackageMap.globalPackagesPath, trackWidgetCreation: false, outputPath: outputPath, extraFrontEndOptions: argResults[FlutterOptions.kExtraFrontEndOptions], ); if (mainPath == null) { throwToolExit('Compiler terminated unexpectedly.'); return null; } // Build AOT snapshot. if (platform == TargetPlatform.ios) { // Determine which iOS architectures to build for. final Iterable<DarwinArch> buildArchs = argResults['ios-arch'].map<DarwinArch>(getIOSArchForName); final Map<DarwinArch, String> iosBuilds = <DarwinArch, String>{}; for (DarwinArch arch in buildArchs) { iosBuilds[arch] = fs.path.join(outputPath, getNameForDarwinArch(arch)); } // Generate AOT snapshot and compile to arch-specific App.framework. final Map<DarwinArch, Future<int>> exitCodes = <DarwinArch, Future<int>>{}; iosBuilds.forEach((DarwinArch iosArch, String outputPath) { exitCodes[iosArch] = snapshotter.build( platform: platform, darwinArch: iosArch, buildMode: buildMode, mainPath: mainPath, packagesPath: PackageMap.globalPackagesPath, outputPath: outputPath, extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], bitcode: bitcode, ).then<int>((int buildExitCode) { return buildExitCode; }); }); // Merge arch-specific App.frameworks into a multi-arch App.framework. if ((await Future.wait<int>(exitCodes.values)).every((int buildExitCode) => buildExitCode == 0)) { final Iterable<String> dylibs = iosBuilds.values.map<String>( (String outputDir) => fs.path.join(outputDir, 'App.framework', 'App')); fs.directory(fs.path.join(outputPath, 'App.framework'))..createSync(); await processUtils.run( <String>[ 'lipo', ...dylibs, '-create', '-output', fs.path.join(outputPath, 'App.framework', 'App'), ], throwOnError: true, ); } else { status?.cancel(); exitCodes.forEach((DarwinArch iosArch, Future<int> exitCodeFuture) async { final int buildExitCode = await exitCodeFuture; printError('Snapshotting ($iosArch) exited with non-zero exit code: $buildExitCode'); }); } } else { // Android AOT snapshot. final int snapshotExitCode = await snapshotter.build( platform: platform, buildMode: buildMode, mainPath: mainPath, packagesPath: PackageMap.globalPackagesPath, outputPath: outputPath, extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], bitcode: false, ); if (snapshotExitCode != 0) { status?.cancel(); throwToolExit('Snapshotting exited with non-zero exit code: $snapshotExitCode'); } } } on ProcessException catch (error) { // Catch the String exceptions thrown from the `runSync` methods below. status?.cancel(); printError(error.toString()); return null; } status?.stop(); if (outputPath == null) { throwToolExit(null); } final String builtMessage = 'Built to $outputPath${fs.path.separator}.'; if (argResults['quiet']) { printTrace(builtMessage); } else { printStatus(builtMessage); } return null; } } Future<void> validateBitcode(BuildMode buildMode, TargetPlatform targetPlatform) async { final Artifacts artifacts = Artifacts.instance; final String flutterFrameworkPath = artifacts.getArtifactPath( Artifact.flutterFramework, mode: buildMode, platform: targetPlatform, ); if (!fs.isDirectorySync(flutterFrameworkPath)) { throwToolExit('Flutter.framework not found at $flutterFrameworkPath'); } final Xcode xcode = context.get<Xcode>(); final RunResult clangResult = await xcode.clang(<String>['--version']); final String clangVersion = clangResult.stdout.split('\n').first; final String engineClangVersion = PlistParser.instance.getValueFromFile( fs.path.join(flutterFrameworkPath, 'Info.plist'), 'ClangVersion', ); final Version engineClangSemVer = _parseVersionFromClang(engineClangVersion); final Version clangSemVer = _parseVersionFromClang(clangVersion); if (engineClangSemVer > clangSemVer) { throwToolExit( 'The Flutter.framework at $flutterFrameworkPath was built ' 'with "${engineClangVersion ?? 'unknown'}", but the current version ' 'of clang is "$clangVersion". This will result in failures when trying to' 'archive an IPA. To resolve this issue, update your version of Xcode to ' 'at least $engineClangSemVer.', ); } } Version _parseVersionFromClang(String clangVersion) { final RegExp pattern = RegExp(r'Apple (LLVM|clang) version (\d+\.\d+\.\d+) '); void _invalid() { throwToolExit('Unable to parse Clang version from "$clangVersion". ' 'Expected a string like "Apple (LLVM|clang) #.#.# (clang-####.#.##.#)".'); } if (clangVersion == null || clangVersion.isEmpty) { _invalid(); } final RegExpMatch match = pattern.firstMatch(clangVersion); if (match == null || match.groupCount != 2) { _invalid(); } final Version version = Version.parse(match.group(2)); if (version == null) { _invalid(); } return version; }