Unverified Commit d2e87a5d authored by xster's avatar xster Committed by GitHub

Build ios framework (#44065)

parent 028ed712
......@@ -761,6 +761,7 @@ Future<void> _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));
......
// 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<void> 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: <String>['--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: <String>['get'],
);
});
// This builds all build modes' frameworks by default
section('Build frameworks');
await inDirectory(projectDir, () async {
await flutter(
'build',
options: <String>['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 <String>['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 <String>['Debug', 'Profile', 'Release']) {
checkFileExists(path.join(
outputPath,
mode,
'Flutter.framework',
'Flutter',
));
}
section("Check all modes' engine header");
for (String mode in <String>['Debug', 'Profile', 'Release']) {
checkFileContains(
<String>['#include "FlutterEngine.h"'],
path.join(outputPath, mode, 'Flutter.framework', 'Headers', 'Flutter.h'),
);
}
section("Check all modes' have plugin dylib");
for (String mode in <String>['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 <String>['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);
}
});
}
......@@ -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(
......
// 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<void> build({
@required TargetPlatform platform,
@required String outputPath,
@required BuildMode buildMode,
@required String mainDartFile,
bool bitcode = kBitcodeEnabledDefault,
bool quiet = true,
bool reportTimings = false,
Iterable<DarwinArch> iosBuildArchs = defaultIOSArchs,
List<String> extraFrontEndOptions,
List<String> 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<DarwinArch, String> iosBuilds = <DarwinArch, String>{};
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<DarwinArch, Future<int>> exitCodes = <DarwinArch, Future<int>>{};
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>((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: 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<void> _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: <String, String>{
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);
}
}
}
......@@ -94,6 +94,7 @@ class AOTSnapshotter {
DarwinArch darwinArch,
List<String> extraGenSnapshotOptions = const <String>[],
@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);
if (!quiet) {
printStatus('Building App.framework for $targetArch...');
}
final List<String> commonBuildOptions = <String>[
'-arch', targetArch,
......
......@@ -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.
......
......@@ -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<void> 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(<String>[
'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<Source> get outputs => const <Source>[
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<Source> get outputs => const <Source>[
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<RunResult> 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(<String>[
'-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.
}
}
}
......@@ -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));
......
......@@ -36,13 +36,13 @@ class BuildAarCommand extends BuildSubCommand {
@override
Future<Map<CustomDimensions, String>> get usageValues async {
final Map<CustomDimensions, String> usage = <CustomDimensions, String>{};
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';
......
......@@ -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.');
}
......
// 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<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;
}
......@@ -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';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment