From d2e87a5d2cf15cfdccbba68076a6b21f0c5fc523 Mon Sep 17 00:00:00 2001 From: xster Date: Wed, 6 Nov 2019 14:54:15 -0800 Subject: [PATCH] Build ios framework (#44065) --- dev/bots/test.dart | 1 + .../build_ios_framework_module_test.dart | 143 +++++++ dev/devicelab/lib/framework/utils.dart | 7 + packages/flutter_tools/lib/src/aot.dart | 239 +++++++++++ .../flutter_tools/lib/src/base/build.dart | 7 +- .../lib/src/base/file_system.dart | 2 +- .../lib/src/build_system/targets/ios.dart | 61 ++- .../flutter_tools/lib/src/commands/build.dart | 2 + .../lib/src/commands/build_aar.dart | 8 +- .../lib/src/commands/build_aot.dart | 287 +------------ .../lib/src/commands/build_ios.dart | 6 +- .../lib/src/commands/build_ios_framework.dart | 400 ++++++++++++++++++ .../flutter_tools/lib/src/ios/bitcode.dart | 67 +++ .../commands/build_aot_test.dart | 2 +- 14 files changed, 950 insertions(+), 282 deletions(-) create mode 100644 dev/devicelab/bin/tasks/build_ios_framework_module_test.dart create mode 100644 packages/flutter_tools/lib/src/aot.dart create mode 100644 packages/flutter_tools/lib/src/commands/build_ios_framework.dart create mode 100644 packages/flutter_tools/lib/src/ios/bitcode.dart diff --git a/dev/bots/test.dart b/dev/bots/test.dart index b2377215fb8..a8aec472362 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -761,6 +761,7 @@ Future _runHostOnlyDeviceLabTests() async { // TODO(jmagman): Re-enable once flakiness is resolved, https://github.com/flutter/flutter/issues/37525 // if (Platform.isMacOS) () => _runDevicelabTest('module_test_ios'), + if (Platform.isMacOS) () => _runDevicelabTest('build_ios_framework_module_test'), if (Platform.isMacOS) () => _runDevicelabTest('plugin_lint_mac'), () => _runDevicelabTest('plugin_test', environment: gradleEnvironment), ]..shuffle(math.Random(0)); diff --git a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart new file mode 100644 index 00000000000..c143ad43bf6 --- /dev/null +++ b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart @@ -0,0 +1,143 @@ +// Copyright (c) 2019 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 'dart:io'; + +import 'package:flutter_devicelab/framework/apk_utils.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:path/path.dart' as path; + +/// Tests that iOS .frameworks can be built on module projects. +Future main() async { + await task(() async { + + section('Create module project'); + + final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.'); + final Directory projectDir = Directory(path.join(tempDir.path, 'hello')); + try { + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: ['--org', 'io.flutter.devicelab', '--template', 'module', 'hello'], + ); + }); + + section('Add plugins'); + + final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml')); + String content = pubspec.readAsStringSync(); + content = content.replaceFirst( + '\ndependencies:\n', + '\ndependencies:\n device_info: 0.4.1\n package_info: 0.4.0+9\n', + ); + pubspec.writeAsStringSync(content, flush: true); + await inDirectory(projectDir, () async { + await flutter( + 'packages', + options: ['get'], + ); + }); + + // This builds all build modes' frameworks by default + section('Build frameworks'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: ['ios-framework'], + ); + }); + + final String outputPath = path.join( + projectDir.path, + 'build', + 'ios', + 'framework', + ); + + section('Check debug build has Dart snapshot as asset'); + + checkFileExists(path.join( + outputPath, + 'Debug', + 'App.framework', + 'flutter_assets', + 'vm_snapshot_data', + )); + + section('Check profile, release builds has Dart dylib'); + + for (String mode in ['Profile', 'Release']) { + checkFileExists(path.join( + outputPath, + mode, + 'App.framework', + 'App', + )); + + checkFileNotExists(path.join( + outputPath, + mode, + 'App.framework', + 'flutter_assets', + 'vm_snapshot_data', + )); + } + + section("Check all modes' engine dylib"); + + for (String mode in ['Debug', 'Profile', 'Release']) { + checkFileExists(path.join( + outputPath, + mode, + 'Flutter.framework', + 'Flutter', + )); + } + + section("Check all modes' engine header"); + + for (String mode in ['Debug', 'Profile', 'Release']) { + checkFileContains( + ['#include "FlutterEngine.h"'], + path.join(outputPath, mode, 'Flutter.framework', 'Headers', 'Flutter.h'), + ); + } + + section("Check all modes' have plugin dylib"); + + for (String mode in ['Debug', 'Profile', 'Release']) { + checkFileExists(path.join( + outputPath, + mode, + 'device_info.framework', + 'device_info', + )); + } + + section("Check all modes' have generated plugin registrant"); + + for (String mode in ['Debug', 'Profile', 'Release']) { + checkFileExists(path.join( + outputPath, + mode, + 'FlutterPluginRegistrant.framework', + 'Headers', + 'GeneratedPluginRegistrant.h', + )); + } + + return TaskResult.success(null); + } on TaskResult catch (taskResult) { + return taskResult; + } catch (e) { + return TaskResult.failure(e.toString()); + } finally { + rmTree(tempDir); + } + }); +} diff --git a/dev/devicelab/lib/framework/utils.dart b/dev/devicelab/lib/framework/utils.dart index 32ca1bad94a..0d9b18e8583 100644 --- a/dev/devicelab/lib/framework/utils.dart +++ b/dev/devicelab/lib/framework/utils.dart @@ -625,6 +625,13 @@ void checkFileExists(String file) { } } +/// Checks that the file does not exists, otherwise throws a [FileSystemException]. +void checkFileNotExists(String file) { + if (exists(File(file))) { + throw FileSystemException('Expected file to exit.', file); + } +} + void _checkExitCode(int code) { if (code != 0) { throw Exception( diff --git a/packages/flutter_tools/lib/src/aot.dart b/packages/flutter_tools/lib/src/aot.dart new file mode 100644 index 00000000000..71e15587ec7 --- /dev/null +++ b/packages/flutter_tools/lib/src/aot.dart @@ -0,0 +1,239 @@ +// Copyright 2019 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 'package:meta/meta.dart'; + +import 'base/build.dart'; +import 'base/common.dart'; +import 'base/file_system.dart'; +import 'base/io.dart'; +import 'base/logger.dart'; +import 'base/process.dart'; +import 'build_info.dart'; +import 'build_system/build_system.dart'; +import 'build_system/targets/dart.dart'; +import 'dart/package_map.dart'; +import 'globals.dart'; +import 'ios/bitcode.dart'; +import 'project.dart'; + +/// Builds AOT snapshots given a platform, build mode and a path to a Dart +/// library. +class AotBuilder { + Future build({ + @required TargetPlatform platform, + @required String outputPath, + @required BuildMode buildMode, + @required String mainDartFile, + bool bitcode = kBitcodeEnabledDefault, + bool quiet = true, + bool reportTimings = false, + Iterable iosBuildArchs = defaultIOSArchs, + List extraFrontEndOptions, + List extraGenSnapshotOptions, + }) async { + if (platform == null) { + throwToolExit('No AOT build platform specified'); + } + if (_canUseAssemble(platform) + && extraGenSnapshotOptions?.isEmpty != false + && extraFrontEndOptions?.isEmpty != false) { + await _buildWithAssemble( + targetFile: mainDartFile, + outputDir: outputPath, + targetPlatform: platform, + buildMode: buildMode, + quiet: quiet, + ); + return; + } + + if (bitcode) { + if (platform != TargetPlatform.ios) { + throwToolExit('Bitcode is only supported on iOS (TargetPlatform is $platform).'); + } + await validateBitcode(buildMode, platform); + } + + Status status; + if (!quiet) { + final String typeName = artifacts.getEngineType(platform, buildMode); + status = logger.startProgress( + 'Building AOT snapshot in ${getFriendlyModeName(buildMode)} mode ($typeName)...', + timeout: timeoutConfiguration.slowOperation, + ); + } + try { + final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: reportTimings); + + // Compile to kernel. + final String kernelOut = await snapshotter.compileKernel( + platform: platform, + buildMode: buildMode, + mainPath: mainDartFile, + packagesPath: PackageMap.globalPackagesPath, + trackWidgetCreation: false, + outputPath: outputPath, + extraFrontEndOptions: extraFrontEndOptions, + ); + if (kernelOut == null) { + throwToolExit('Compiler terminated unexpectedly.'); + return; + } + + // Build AOT snapshot. + if (platform == TargetPlatform.ios) { + // Determine which iOS architectures to build for. + final Map iosBuilds = {}; + for (DarwinArch arch in iosBuildArchs) { + iosBuilds[arch] = fs.path.join(outputPath, getNameForDarwinArch(arch)); + } + + // Generate AOT snapshot and compile to arch-specific App.framework. + final Map> exitCodes = >{}; + iosBuilds.forEach((DarwinArch iosArch, String outputPath) { + exitCodes[iosArch] = snapshotter.build( + platform: platform, + darwinArch: iosArch, + buildMode: buildMode, + mainPath: kernelOut, + packagesPath: PackageMap.globalPackagesPath, + outputPath: outputPath, + extraGenSnapshotOptions: extraGenSnapshotOptions, + bitcode: bitcode, + quiet: quiet, + ).then((int buildExitCode) { + return buildExitCode; + }); + }); + + // Merge arch-specific App.frameworks into a multi-arch App.framework. + if ((await Future.wait(exitCodes.values)).every((int buildExitCode) => buildExitCode == 0)) { + final Iterable dylibs = iosBuilds.values.map( + (String outputDir) => fs.path.join(outputDir, 'App.framework', 'App')); + fs.directory(fs.path.join(outputPath, 'App.framework'))..createSync(); + await processUtils.run( + [ + 'lipo', + ...dylibs, + '-create', + '-output', fs.path.join(outputPath, 'App.framework', 'App'), + ], + throwOnError: true, + ); + } else { + status?.cancel(); + exitCodes.forEach((DarwinArch iosArch, Future 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: kernelOut, + packagesPath: PackageMap.globalPackagesPath, + outputPath: outputPath, + extraGenSnapshotOptions: extraGenSnapshotOptions, + 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; + } + status?.stop(); + + if (outputPath == null) { + throwToolExit(null); + } + + final String builtMessage = 'Built to $outputPath${fs.path.separator}.'; + if (quiet) { + printTrace(builtMessage); + } else { + printStatus(builtMessage); + } + return; + } + + bool _canUseAssemble(TargetPlatform targetPlatform) { + switch (targetPlatform) { + case TargetPlatform.android_arm: + case TargetPlatform.android_arm64: + case TargetPlatform.android_x86: + case TargetPlatform.darwin_x64: + return true; + case TargetPlatform.android_x64: + case TargetPlatform.ios: + case TargetPlatform.linux_x64: + case TargetPlatform.windows_x64: + case TargetPlatform.fuchsia_arm64: + case TargetPlatform.fuchsia_x64: + case TargetPlatform.tester: + case TargetPlatform.web_javascript: + default: + return false; + } + } + + Future _buildWithAssemble({ + TargetPlatform targetPlatform, + BuildMode buildMode, + String targetFile, + String outputDir, + bool quiet + }) async { + Status status; + if (!quiet) { + final String typeName = artifacts.getEngineType(targetPlatform, buildMode); + status = logger.startProgress( + 'Building AOT snapshot in ${getFriendlyModeName(buildMode)} mode ($typeName)...', + timeout: timeoutConfiguration.slowOperation, + ); + } + final FlutterProject flutterProject = FlutterProject.current(); + // Currently this only supports android, per the check above. + final Target target = buildMode == BuildMode.profile + ? const ProfileCopyFlutterAotBundle() + : const ReleaseCopyFlutterAotBundle(); + + final BuildResult result = await buildSystem.build(target, Environment( + projectDir: flutterProject.directory, + outputDir: fs.directory(outputDir), + buildDir: flutterProject.directory + .childDirectory('.dart_tool') + .childDirectory('flutter_build'), + defines: { + kBuildMode: getNameForBuildMode(buildMode), + kTargetPlatform: getNameForTargetPlatform(targetPlatform), + kTargetFile: targetFile, + } + )); + status?.stop(); + if (!result.success) { + for (ExceptionMeasurement measurement in result.exceptions.values) { + printError(measurement.exception.toString()); + printError(measurement.stackTrace.toString()); + } + throwToolExit('Failed to build aot.'); + } + final String builtMessage = 'Built to $outputDir${fs.path.separator}.'; + if (quiet) { + printTrace(builtMessage); + } else { + printStatus(builtMessage); + } + } +} diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 18a8683d37c..d0f16424fc0 100644 --- a/packages/flutter_tools/lib/src/base/build.dart +++ b/packages/flutter_tools/lib/src/base/build.dart @@ -94,6 +94,7 @@ class AOTSnapshotter { DarwinArch darwinArch, List extraGenSnapshotOptions = const [], @required bool bitcode, + bool quiet = false, }) async { if (bitcode && platform != TargetPlatform.ios) { printError('Bitcode is only supported for iOS.'); @@ -208,6 +209,7 @@ class AOTSnapshotter { assemblyPath: stripSymbols ? '$assembly.stripped.S' : assembly, outputPath: outputDir.path, bitcode: bitcode, + quiet: quiet, ); if (result.exitCode != 0) { return result.exitCode; @@ -224,9 +226,12 @@ class AOTSnapshotter { @required String assemblyPath, @required String outputPath, @required bool bitcode, + @required bool quiet }) async { final String targetArch = getNameForDarwinArch(appleArch); - printStatus('Building App.framework for $targetArch...'); + if (!quiet) { + printStatus('Building App.framework for $targetArch...'); + } final List commonBuildOptions = [ '-arch', targetArch, diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart index 46f34567f81..f10b3e0902e 100644 --- a/packages/flutter_tools/lib/src/base/file_system.dart +++ b/packages/flutter_tools/lib/src/base/file_system.dart @@ -173,7 +173,7 @@ bool isOlderThanReference({ @required FileSystemEntity entity, @required File re return true; } return referenceFile.existsSync() - && referenceFile.lastModifiedSync().isAfter(entity.statSync().modified); + && referenceFile.statSync().modified.isAfter(entity.statSync().modified); } /// Exception indicating that a file that was expected to exist was not found. diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 5b9510bdaa0..3f521662dd1 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -4,10 +4,13 @@ import '../../artifacts.dart'; import '../../base/build.dart'; +import '../../base/common.dart'; import '../../base/file_system.dart'; import '../../base/io.dart'; +import '../../base/process.dart'; import '../../base/process_manager.dart'; import '../../build_info.dart'; +import '../../macos/xcode.dart'; import '../build_system.dart'; import '../exceptions.dart'; import 'dart.dart'; @@ -22,7 +25,7 @@ abstract class AotAssemblyBase extends Target { @override Future build(Environment environment) async { final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false); - final String outputPath = environment.buildDir.path; + final String buildOutputPath = environment.buildDir.path; if (environment.defines[kBuildMode] == null) { throw MissingDefineException(kBuildMode, 'aot_assembly'); } @@ -45,7 +48,7 @@ abstract class AotAssemblyBase extends Target { buildMode: buildMode, mainPath: environment.buildDir.childFile('app.dill').path, packagesPath: environment.projectDir.childFile('.packages').path, - outputPath: outputPath, + outputPath: environment.outputDir.path, darwinArch: iosArchs.single, bitcode: bitcode, ); @@ -62,7 +65,7 @@ abstract class AotAssemblyBase extends Target { buildMode: buildMode, mainPath: environment.buildDir.childFile('app.dill').path, packagesPath: environment.projectDir.childFile('.packages').path, - outputPath: fs.path.join(outputPath, getNameForDarwinArch(iosArch)), + outputPath: fs.path.join(buildOutputPath, getNameForDarwinArch(iosArch)), darwinArch: iosArch, bitcode: bitcode, )); @@ -74,10 +77,10 @@ abstract class AotAssemblyBase extends Target { final ProcessResult result = await processManager.run([ 'lipo', ...iosArchs.map((DarwinArch iosArch) => - fs.path.join(outputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')), + fs.path.join(buildOutputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')), '-create', '-output', - fs.path.join(outputPath, 'App.framework', 'App'), + fs.path.join(environment.outputDir.path, 'App.framework', 'App'), ]); if (result.exitCode != 0) { throw Exception('lipo exited with code ${result.exitCode}'); @@ -108,7 +111,7 @@ class AotAssemblyRelease extends AotAssemblyBase { @override List get outputs => const [ - Source.pattern('{BUILD_DIR}/App.framework/App'), + Source.pattern('{OUTPUT_DIR}/App.framework/App'), ]; @override @@ -140,7 +143,7 @@ class AotAssemblyProfile extends AotAssemblyBase { @override List get outputs => const [ - Source.pattern('{BUILD_DIR}/App.framework/App'), + Source.pattern('{OUTPUT_DIR}/App.framework/App'), ]; @override @@ -148,3 +151,47 @@ class AotAssemblyProfile extends AotAssemblyBase { KernelSnapshot(), ]; } + +/// Create an App.framework for debug iOS targets. +/// +/// This framework needs to exist for the Xcode project to link/bundle, +/// but it isn't actually executed. To generate something valid, we compile a trivial +/// constant. +Future createStubAppFramework(Directory appFrameworkDirectory) async { + File outputFile; + try { + if (!appFrameworkDirectory.existsSync()) { + appFrameworkDirectory.createSync(recursive: true); + } + + outputFile = appFrameworkDirectory.childFile('App'); + outputFile.createSync(recursive: true); + } catch (e) { + throwToolExit('Failed to create App.framework stub at ${appFrameworkDirectory.path}'); + } + + final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_stub_source.'); + try { + final File stubSource = tempDir.childFile('debug_app.cc') + ..writeAsStringSync(r''' + static const int Moo = 88; + '''); + + return await xcode.clang([ + '-x', + 'c', + stubSource.path, + '-dynamiclib', + '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks', + '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks', + '-install_name', '@rpath/App.framework/App', + '-o', outputFile.path, + ]); + } finally { + try { + tempDir.deleteSync(recursive: true); + } on FileSystemException catch (_) { + // Best effort. Sometimes we can't delete things from system temp. + } + } +} diff --git a/packages/flutter_tools/lib/src/commands/build.dart b/packages/flutter_tools/lib/src/commands/build.dart index 9a703604fbc..22401621075 100644 --- a/packages/flutter_tools/lib/src/commands/build.dart +++ b/packages/flutter_tools/lib/src/commands/build.dart @@ -16,6 +16,7 @@ import 'build_appbundle.dart'; import 'build_bundle.dart'; import 'build_fuchsia.dart'; import 'build_ios.dart'; +import 'build_ios_framework.dart'; import 'build_web.dart'; class BuildCommand extends FlutterCommand { @@ -25,6 +26,7 @@ class BuildCommand extends FlutterCommand { addSubcommand(BuildAppBundleCommand(verboseHelp: verboseHelp)); addSubcommand(BuildAotCommand(verboseHelp: verboseHelp)); addSubcommand(BuildIOSCommand()); + addSubcommand(BuildIOSFrameworkCommand()); addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp)); addSubcommand(BuildWebCommand()); addSubcommand(BuildMacosCommand(verboseHelp: verboseHelp)); diff --git a/packages/flutter_tools/lib/src/commands/build_aar.dart b/packages/flutter_tools/lib/src/commands/build_aar.dart index 7eeb7abf685..3a3043d0a57 100644 --- a/packages/flutter_tools/lib/src/commands/build_aar.dart +++ b/packages/flutter_tools/lib/src/commands/build_aar.dart @@ -36,13 +36,13 @@ class BuildAarCommand extends BuildSubCommand { @override Future> get usageValues async { final Map usage = {}; - final FlutterProject futterProject = _getProject(); - if (futterProject == null) { + final FlutterProject flutterProject = _getProject(); + if (flutterProject == null) { return usage; } - if (futterProject.manifest.isModule) { + if (flutterProject.manifest.isModule) { usage[CustomDimensions.commandBuildAarProjectType] = 'module'; - } else if (futterProject.manifest.isPlugin) { + } else if (flutterProject.manifest.isPlugin) { usage[CustomDimensions.commandBuildAarProjectType] = 'plugin'; } else { usage[CustomDimensions.commandBuildAarProjectType] = 'app'; diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart index 18567feb130..b69b1bc5179 100644 --- a/packages/flutter_tools/lib/src/commands/build_aot.dart +++ b/packages/flutter_tools/lib/src/commands/build_aot.dart @@ -4,29 +4,17 @@ import 'dart:async'; -import '../artifacts.dart'; -import '../base/build.dart'; +import '../aot.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 '../build_system/build_system.dart'; -import '../build_system/targets/dart.dart'; -import '../dart/package_map.dart'; -import '../globals.dart'; -import '../ios/plist_parser.dart'; -import '../macos/xcode.dart'; -import '../project.dart'; +import '../ios/bitcode.dart'; import '../resident_runner.dart'; import '../runner/flutter_command.dart'; import 'build.dart'; +/// Builds AOT snapshots into platform specific library containers. class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmentArtifacts { - BuildAotCommand({bool verboseHelp = false}) { + BuildAotCommand({bool verboseHelp = false, this.aotBuilder}) { usesTargetOption(); addBuildModeFlags(); usesPubOption(); @@ -57,7 +45,7 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen hide: true, ) ..addFlag('bitcode', - defaultsTo: false, + defaultsTo: kBitcodeEnabledDefault, help: 'Build the AOT bundle with bitcode. Requires a compatible bitcode engine.', hide: true, ); @@ -67,6 +55,8 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen usesTrackWidgetCreation(hasEffect: false, verboseHelp: verboseHelp); } + AotBuilder aotBuilder; + @override final String name = 'aot'; @@ -82,258 +72,21 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen if (platform == null) { throwToolExit('Unknown platform: $targetPlatform'); } - if (_canUseAssemble(platform)) { - await _buildWithAssemble( - targetFile: findMainDartFile(targetFile), - outputDir: outputPath, - targetPlatform: platform, - buildMode: buildMode, - ); - return null; - } - final bool bitcode = argResults['bitcode']; + aotBuilder ??= AotBuilder(); - 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 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 buildArchs = argResults['ios-arch'].map(getIOSArchForName); - final Map iosBuilds = {}; - 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> exitCodes = >{}; - 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 buildExitCode) { - return buildExitCode; - }); - }); - - // Merge arch-specific App.frameworks into a multi-arch App.framework. - if ((await Future.wait(exitCodes.values)).every((int buildExitCode) => buildExitCode == 0)) { - final Iterable dylibs = iosBuilds.values.map( - (String outputDir) => fs.path.join(outputDir, 'App.framework', 'App')); - fs.directory(fs.path.join(outputPath, 'App.framework'))..createSync(); - await processUtils.run( - [ - 'lipo', - ...dylibs, - '-create', - '-output', fs.path.join(outputPath, 'App.framework', 'App'), - ], - throwOnError: true, - ); - } else { - status?.cancel(); - exitCodes.forEach((DarwinArch iosArch, Future 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); - } + await aotBuilder.build( + platform: platform, + outputPath: outputPath, + buildMode: buildMode, + mainDartFile: findMainDartFile(targetFile), + bitcode: argResults['bitcode'], + quiet: argResults['quiet'], + reportTimings: argResults['report-timings'], + iosBuildArchs: argResults['ios-arch'].map(getIOSArchForName), + extraFrontEndOptions: argResults[FlutterOptions.kExtraFrontEndOptions], + extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], + ); return null; } - - bool _canUseAssemble(TargetPlatform targetPlatform) { - if (argResults.wasParsed(FlutterOptions.kExtraFrontEndOptions) || - argResults.wasParsed(FlutterOptions.kExtraGenSnapshotOptions)) { - return false; - } - switch (targetPlatform) { - case TargetPlatform.android_arm: - case TargetPlatform.android_arm64: - case TargetPlatform.android_x86: - case TargetPlatform.darwin_x64: - return true; - case TargetPlatform.android_x64: - case TargetPlatform.ios: - case TargetPlatform.linux_x64: - case TargetPlatform.windows_x64: - case TargetPlatform.fuchsia_arm64: - case TargetPlatform.fuchsia_x64: - case TargetPlatform.tester: - case TargetPlatform.web_javascript: - default: - return false; - } - } - - Future _buildWithAssemble({ - TargetPlatform targetPlatform, - BuildMode buildMode, - String targetFile, - String outputDir, - }) async { - Status status; - if (!argResults['quiet']) { - final String typeName = artifacts.getEngineType(targetPlatform, buildMode); - status = logger.startProgress( - 'Building AOT snapshot in ${getFriendlyModeName(getBuildMode())} mode ($typeName)...', - timeout: timeoutConfiguration.slowOperation, - ); - } - final FlutterProject flutterProject = FlutterProject.current(); - // Currently this only supports android, per the check above. - final Target target = buildMode == BuildMode.profile - ? const ProfileCopyFlutterAotBundle() - : const ReleaseCopyFlutterAotBundle(); - - final BuildResult result = await buildSystem.build(target, Environment( - projectDir: flutterProject.directory, - outputDir: fs.directory(outputDir), - buildDir: flutterProject.directory - .childDirectory('.dart_tool') - .childDirectory('flutter_build'), - defines: { - kBuildMode: getNameForBuildMode(buildMode), - kTargetPlatform: getNameForTargetPlatform(targetPlatform), - kTargetFile: targetFile, - } - )); - status?.stop(); - if (!result.success) { - for (ExceptionMeasurement measurement in result.exceptions.values) { - printError(measurement.exception.toString()); - printError(measurement.stackTrace.toString()); - } - throwToolExit('Failed to build aot.'); - } - final String builtMessage = 'Built to $outputDir${fs.path.separator}.'; - if (argResults['quiet']) { - printTrace(builtMessage); - } else { - printStatus(builtMessage); - } - } -} - -Future 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(); - - final RunResult clangResult = await xcode.clang(['--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; } diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index c32e1f3508b..b49a9381b37 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -6,6 +6,7 @@ import 'dart:async'; import '../application_package.dart'; import '../base/common.dart'; +import '../base/platform.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../globals.dart'; @@ -13,6 +14,9 @@ import '../ios/mac.dart'; import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult; import 'build.dart'; +/// Builds an .app for an iOS app to be used for local testing on an iOS device +/// or simulator. Can only be run on a macOS host. For producing deployment +/// .ipas, see https://flutter.dev/docs/deployment/ios. class BuildIOSCommand extends BuildSubCommand { BuildIOSCommand() { usesTargetOption(); @@ -59,7 +63,7 @@ class BuildIOSCommand extends BuildSubCommand { final bool forSimulator = argResults['simulator']; defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release; - if (getCurrentHostPlatform() != HostPlatform.darwin_x64) { + if (!platform.isMacOS) { throwToolExit('Building for iOS is only supported on the Mac.'); } diff --git a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart new file mode 100644 index 00000000000..12d9f3619c6 --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart @@ -0,0 +1,400 @@ +// Copyright 2019 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 'package:file/file.dart'; + +import '../aot.dart'; +import '../application_package.dart'; +import '../artifacts.dart'; +import '../base/common.dart'; +import '../base/file_system.dart'; +import '../base/logger.dart'; +import '../base/platform.dart'; +import '../base/process.dart'; +import '../base/utils.dart'; +import '../build_info.dart'; +import '../build_system/targets/ios.dart'; +import '../bundle.dart'; +import '../globals.dart'; +import '../macos/cocoapod_utils.dart'; +import '../macos/xcode.dart'; +import '../plugins.dart'; +import '../project.dart'; +import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult; +import 'build.dart'; + +/// Produces a .framework for integration into a host iOS app. The .framework +/// contains the Flutter engine and framework code as well as plugins. It can +/// be integrated into plain Xcode projects without using or other package +/// managers. +class BuildIOSFrameworkCommand extends BuildSubCommand { + BuildIOSFrameworkCommand({this.aotBuilder, this.bundleBuilder}) { + usesTargetOption(); + usesFlavorOption(); + usesPubOption(); + argParser + ..addFlag('debug', + negatable: true, + defaultsTo: true, + help: 'Whether to produce a framework for the debug build configuration. ' + 'By default, all build configurations are built.' + ) + ..addFlag('profile', + negatable: true, + defaultsTo: true, + help: 'Whether to produce a framework for the profile build configuration. ' + 'By default, all build configurations are built.' + ) + ..addFlag('release', + negatable: true, + defaultsTo: true, + help: 'Whether to produce a framework for the release build configuration. ' + 'By default, all build configurations are built.' + ) + ..addFlag('universal', + help: 'Produce universal frameworks that include all valid architectures. ' + 'This is true by default.', + defaultsTo: true, + negatable: true + ) + ..addFlag('xcframework', + help: 'Produce xcframeworks that include all valid architectures (Xcode 11 or later).', + ) + ..addOption('output', + abbr: 'o', + valueHelp: 'path/to/directory/', + help: 'Location to write the frameworks.', + ); + } + + AotBuilder aotBuilder; + BundleBuilder bundleBuilder; + + @override + final String name = 'ios-framework'; + + @override + final String description = 'Produces a .framework directory for a Flutter module ' + 'and its plugins for integration into existing, plain Xcode projects.\n' + 'This can only be run on macOS hosts.'; + + @override + Future> get requiredArtifacts async => const { + DevelopmentArtifact.universal, + DevelopmentArtifact.iOS, + }; + + FlutterProject _project; + + List get buildModes { + final List buildModes = []; + + if (argResults['debug']) { + buildModes.add(BuildMode.debug); + } + if (argResults['profile']) { + buildModes.add(BuildMode.profile); + } + if (argResults['release']) { + buildModes.add(BuildMode.release); + } + + return buildModes; + } + + @override + Future validateCommand() async { + await super.validateCommand(); + _project = FlutterProject.current(); + if (!_project.isModule) { + throwToolExit('Building frameworks for iOS is only supported from a module.'); + } + + if (!platform.isMacOS) { + throwToolExit('Building frameworks for iOS is only supported on the Mac.'); + } + + if (!argResults['universal'] && !argResults['xcframework']) { + throwToolExit('--universal or --xcframework is required.'); + } + if (argResults['xcframework'] && xcode.majorVersion < 11) { + throwToolExit('--xcframework requires Xcode 11.'); + } + if (buildModes.isEmpty) { + throwToolExit('At least one of "--debug" or "--profile", or "--release" is required.'); + } + } + + @override + Future runCommand() async { + final String outputArgument = argResults['output'] + ?? fs.path.join(fs.currentDirectory.path, 'build', 'ios', 'framework'); + + if (outputArgument.isEmpty) { + throwToolExit('--output is required.'); + } + + final BuildableIOSApp iosProject = await applicationPackages.getPackageForPlatform(TargetPlatform.ios); + + if (iosProject == null) { + throwToolExit("Module's iOS folder missing"); + } + + final Directory outputDirectory = fs.directory(fs.path.normalize(outputArgument)); + + if (outputDirectory.existsSync()) { + outputDirectory.deleteSync(recursive: true); + } + + aotBuilder ??= AotBuilder(); + bundleBuilder ??= BundleBuilder(); + + for (BuildMode mode in buildModes) { + printStatus('Building framework for $iosProject in ${getNameForBuildMode(mode)} mode...'); + final String xcodeBuildConfiguration = toTitleCase(getNameForBuildMode(mode)); + final Directory modeDirectory = outputDirectory.childDirectory(xcodeBuildConfiguration); + final Directory iPhoneBuildOutput = modeDirectory.childDirectory('iphoneos'); + final Directory simulatorBuildOutput = modeDirectory.childDirectory('iphonesimulator'); + + // Copy Flutter.framework. + await _produceFlutterFramework(outputDirectory, mode, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory); + + // Build aot, create module.framework and copy. + await _produceAppFramework(mode, iPhoneBuildOutput, modeDirectory); + + // Build and copy plugins. + await processPodsIfNeeded(_project.ios, getIosBuildDirectory(), mode); + if (hasPlugins(_project)) { + await _producePlugins(xcodeBuildConfiguration, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory, outputDirectory); + } + + final Status status = logger.startProgress(' └─Moving to ${fs.path.relative(modeDirectory.path)}', timeout: timeoutConfiguration.slowOperation); + // Delete the intermediaries since they would have been copied into our + // output frameworks. + if (iPhoneBuildOutput.existsSync()) { + iPhoneBuildOutput.deleteSync(recursive: true); + } + if (simulatorBuildOutput.existsSync()) { + simulatorBuildOutput.deleteSync(recursive: true); + } + status.stop(); + } + + printStatus('Frameworks written to ${outputDirectory.path}.'); + + return null; + } + + Future _produceFlutterFramework(Directory outputDirectory, BuildMode mode, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory modeDirectory) async { + final Status status = logger.startProgress(' ├─Populating Flutter.framework...', timeout: timeoutConfiguration.fastOperation); + final String engineCacheFlutterFrameworkDirectory = artifacts.getArtifactPath(Artifact.flutterFramework, platform: TargetPlatform.ios, mode: mode); + + // Copy universal engine cache framework to mode directory. + final String flutterFrameworkFileName = fs.path.basename(engineCacheFlutterFrameworkDirectory); + final Directory fatFlutterFrameworkCopy = modeDirectory.childDirectory(flutterFrameworkFileName); + copyDirectorySync(fs.directory(engineCacheFlutterFrameworkDirectory), fatFlutterFrameworkCopy); + + if (argResults['xcframework']) { + // Copy universal framework to variant directory. + final Directory armFlutterFrameworkDirectory = iPhoneBuildOutput.childDirectory(flutterFrameworkFileName); + final File armFlutterFrameworkBinary = armFlutterFrameworkDirectory.childFile('Flutter'); + final File fatFlutterFrameworkBinary = fatFlutterFrameworkCopy.childFile('Flutter'); + copyDirectorySync(fatFlutterFrameworkCopy, armFlutterFrameworkDirectory); + + // Create iOS framework. + List lipoCommand = ['xcrun', 'lipo', fatFlutterFrameworkBinary.path, '-remove', 'x86_64', '-output', armFlutterFrameworkBinary.path]; + + await processUtils.run( + lipoCommand, + workingDirectory: outputDirectory.path, + allowReentrantFlutter: false, + ); + + // Create simulator framework. + final Directory simulatorFlutterFrameworkDirectory = simulatorBuildOutput.childDirectory(flutterFrameworkFileName); + final File simulatorFlutterFrameworkBinary = simulatorFlutterFrameworkDirectory.childFile('Flutter'); + copyDirectorySync(fatFlutterFrameworkCopy, simulatorFlutterFrameworkDirectory); + + lipoCommand = ['xcrun', 'lipo', fatFlutterFrameworkBinary.path, '-thin', 'x86_64', '-output', simulatorFlutterFrameworkBinary.path]; + + await processUtils.run( + lipoCommand, + workingDirectory: outputDirectory.path, + allowReentrantFlutter: false, + ); + + // Create XCFramework from iOS and simulator frameworks. + final List xcframeworkCommand = [ + 'xcrun', + 'xcodebuild', + '-create-xcframework', + '-framework', armFlutterFrameworkDirectory.path, + '-framework', simulatorFlutterFrameworkDirectory.path, + '-output', modeDirectory + .childFile('Flutter.xcframework') + .path + ]; + + await processUtils.run( + xcframeworkCommand, + workingDirectory: outputDirectory.path, + allowReentrantFlutter: false, + ); + } + + if (!argResults['universal']) { + fatFlutterFrameworkCopy.deleteSync(recursive: true); + } + status.stop(); + } + + Future _produceAppFramework(BuildMode mode, Directory iPhoneBuildOutput, Directory modeDirectory) async { + const String appFrameworkName = 'App.framework'; + final Directory destinationAppFrameworkDirectory = modeDirectory.childDirectory(appFrameworkName); + + if (mode == BuildMode.debug) { + final Status status = logger.startProgress(' ├─Add placeholder App.framework for debug...', timeout: timeoutConfiguration.fastOperation); + await createStubAppFramework(destinationAppFrameworkDirectory); + status.stop(); + } else { + await _produceAotAppFrameworkIfNeeded(mode, iPhoneBuildOutput, destinationAppFrameworkDirectory); + } + + final File sourceInfoPlist = _project.ios.hostAppRoot.childDirectory('Flutter').childFile('AppFrameworkInfo.plist'); + final File destinationInfoPlist = destinationAppFrameworkDirectory.childFile('Info.plist')..createSync(recursive: true); + + destinationInfoPlist.writeAsBytesSync(sourceInfoPlist.readAsBytesSync()); + + final Status status = logger.startProgress(' ├─Assembling Flutter resources for App.framework...', timeout: timeoutConfiguration.slowOperation); + await bundleBuilder.build( + platform: TargetPlatform.ios, + buildMode: mode, + // Relative paths show noise in the compiler https://github.com/dart-lang/sdk/issues/37978. + mainPath: fs.path.absolute(targetFile), + assetDirPath: destinationAppFrameworkDirectory.childDirectory('flutter_assets').path, + precompiledSnapshot: mode != BuildMode.debug, + ); + status.stop(); + } + + Future _produceAotAppFrameworkIfNeeded(BuildMode mode, Directory iPhoneBuildOutput, Directory destinationAppFrameworkDirectory) async { + if (mode == BuildMode.debug) { + return; + } + final Status status = logger.startProgress(' ├─Building Dart AOT for App.framework...', timeout: timeoutConfiguration.slowOperation); + await aotBuilder.build( + platform: TargetPlatform.ios, + outputPath: iPhoneBuildOutput.path, + buildMode: mode, + // Relative paths show noise in the compiler https://github.com/dart-lang/sdk/issues/37978. + mainDartFile: fs.path.absolute(targetFile), + quiet: true, + reportTimings: false, + iosBuildArchs: [DarwinArch.armv7, DarwinArch.arm64], + ); + + const String appFrameworkName = 'App.framework'; + copyDirectorySync(iPhoneBuildOutput.childDirectory(appFrameworkName), destinationAppFrameworkDirectory); + status.stop(); + } + + Future _producePlugins( + String xcodeBuildConfiguration, + Directory iPhoneBuildOutput, + Directory simulatorBuildOutput, + Directory modeDirectory, + Directory outputDirectory, + ) async { + final Status status = logger.startProgress(' ├─Building plugins...', timeout: timeoutConfiguration.slowOperation); + List pluginsBuildCommand = [ + 'xcrun', + 'xcodebuild', + '-alltargets', + '-sdk', + 'iphoneos', + '-configuration', + xcodeBuildConfiguration, + 'SYMROOT=${iPhoneBuildOutput.path}', + 'ONLY_ACTIVE_ARCH=NO' // No device targeted, so build all valid architectures. + ]; + + await processUtils.run( + pluginsBuildCommand, + workingDirectory: _project.ios.hostAppRoot.childDirectory('Pods').path, + allowReentrantFlutter: false, + ); + + pluginsBuildCommand = [ + 'xcrun', + 'xcodebuild', + '-alltargets', + '-sdk', + 'iphonesimulator', + '-configuration', + xcodeBuildConfiguration, + 'SYMROOT=${simulatorBuildOutput.path}', + 'ARCHS=x86_64', + 'ONLY_ACTIVE_ARCH=NO' // No device targeted, so build all valid architectures. + ]; + + await processUtils.run( + pluginsBuildCommand, + workingDirectory: _project.ios.hostAppRoot.childDirectory('Pods').path, + allowReentrantFlutter: false, + ); + + final Directory iPhoneBuildConfiguration = iPhoneBuildOutput.childDirectory('$xcodeBuildConfiguration-iphoneos'); + final Directory simulatorBuildConfiguration = simulatorBuildOutput.childDirectory('$xcodeBuildConfiguration-iphonesimulator'); + + for (Directory builtProduct in iPhoneBuildConfiguration.listSync(followLinks: false).whereType()) { + for (FileSystemEntity podProduct in builtProduct.listSync(followLinks: false)) { + final String podFrameworkName = podProduct.basename; + if (fs.path.extension(podFrameworkName) == '.framework') { + final String binaryName = fs.path.basenameWithoutExtension(podFrameworkName); + if (argResults['universal']) { + copyDirectorySync(podProduct, modeDirectory.childDirectory(podFrameworkName)); + final List lipoCommand = [ + 'xcrun', + 'lipo', + '-create', + fs.path.join(podProduct.path, binaryName), + simulatorBuildConfiguration.childDirectory(binaryName).childDirectory(podFrameworkName).childFile(binaryName).path, + '-output', + modeDirectory.childDirectory(podFrameworkName).childFile(binaryName).path + ]; + + await processUtils.run( + lipoCommand, + workingDirectory: outputDirectory.path, + allowReentrantFlutter: false, + ); + } + + if (argResults['xcframework']) { + final List xcframeworkCommand = [ + 'xcrun', + 'xcodebuild', + '-create-xcframework', + '-framework', + podProduct.path, + '-framework', + simulatorBuildConfiguration.childDirectory(binaryName).childDirectory(podFrameworkName).path, + '-output', + modeDirectory.childFile('$binaryName.xcframework').path + ]; + + await processUtils.run( + xcframeworkCommand, + workingDirectory: outputDirectory.path, + allowReentrantFlutter: false, + ); + } + } + } + } + status.stop(); + } +} diff --git a/packages/flutter_tools/lib/src/ios/bitcode.dart b/packages/flutter_tools/lib/src/ios/bitcode.dart new file mode 100644 index 00000000000..ec66eafd06e --- /dev/null +++ b/packages/flutter_tools/lib/src/ios/bitcode.dart @@ -0,0 +1,67 @@ +// Copyright 2019 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 '../artifacts.dart'; +import '../base/common.dart'; +import '../base/context.dart'; +import '../base/file_system.dart'; +import '../base/process.dart'; +import '../base/version.dart'; +import '../build_info.dart'; +import '../ios/plist_parser.dart'; +import '../macos/xcode.dart'; + +const bool kBitcodeEnabledDefault = false; + +Future 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(); + + final RunResult clangResult = await xcode.clang(['--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; +} diff --git a/packages/flutter_tools/test/general.shard/commands/build_aot_test.dart b/packages/flutter_tools/test/general.shard/commands/build_aot_test.dart index cba25106419..79b89291b3d 100644 --- a/packages/flutter_tools/test/general.shard/commands/build_aot_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/build_aot_test.dart @@ -7,8 +7,8 @@ import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/build_info.dart'; -import 'package:flutter_tools/src/commands/build_aot.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/ios/bitcode.dart'; import 'package:flutter_tools/src/ios/plist_parser.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:mockito/mockito.dart';