Unverified Commit 98aeef2d authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Build xcarchive command (#67598)

parent 98d1ad01
......@@ -166,6 +166,29 @@ Future<void> main() async {
if (!await localNetworkUsageFound(outputAppPath)) {
throw TaskResult.failure('Debug bundle is missing NSLocalNetworkUsageDescription');
}
section('Clean build');
await inDirectory(flutterProject.rootPath, () async {
await flutter('clean');
});
section('Archive');
await inDirectory(flutterProject.rootPath, () async {
await flutter('build', options: <String>[
'xcarchive',
]);
});
checkDirectoryExists(path.join(
flutterProject.rootPath,
'build',
'ios',
'archive',
'Runner.xcarchive',
'Products',
));
});
return TaskResult.success(null);
......
......@@ -379,6 +379,12 @@ class BuildableIOSApp extends IOSApp {
@override
String get deviceBundlePath => _buildAppPath('iphoneos');
// Xcode uses this path for the final archive bundle location,
// not a top-level output directory.
// Specifying `build/ios/archive/Runner` will result in `build/ios/archive/Runner.xcarchive`.
String get archiveBundlePath
=> globals.fs.path.join(getIosBuildDirectory(), 'archive', globals.fs.path.withoutExtension(_hostAppBundleName));
String _buildAppPath(String type) {
return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName);
}
......
......@@ -28,6 +28,7 @@ class BuildCommand extends FlutterCommand {
buildSystem: globals.buildSystem,
verboseHelp: verboseHelp,
));
addSubcommand(BuildIOSArchiveCommand(verboseHelp: verboseHelp));
addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
addSubcommand(BuildWebCommand(verboseHelp: verboseHelp));
addSubcommand(BuildMacosCommand(verboseHelp: verboseHelp));
......
......@@ -19,24 +19,8 @@ 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({ @required bool verboseHelp }) {
addTreeShakeIconsFlag();
addSplitDebugInfoOption();
addBuildModeFlags(defaultToRelease: true);
usesTargetOption();
usesFlavorOption();
usesPubOption();
usesBuildNumberOption();
usesBuildNameOption();
addDartObfuscationOption();
usesDartDefineOption();
usesExtraFrontendOptions();
addEnableExperimentation(hide: !verboseHelp);
addBuildPerformanceFile(hide: !verboseHelp);
addBundleSkSLPathOption(hide: !verboseHelp);
addNullSafetyModeOptions(hide: !verboseHelp);
usesAnalyzeSizeFlag();
class BuildIOSCommand extends _BuildIOSSubCommand {
BuildIOSCommand({ @required bool verboseHelp }) : super(verboseHelp: verboseHelp) {
argParser
..addFlag('config-only',
help: 'Update the project configuration without performing a build. '
......@@ -59,16 +43,76 @@ class BuildIOSCommand extends BuildSubCommand {
@override
final String description = 'Build an iOS application bundle (Mac OS X host only).';
@override
final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.build;
@override
bool get forSimulator => boolArg('simulator');
@override
bool get configOnly => boolArg('config-only');
@override
bool get shouldCodesign => boolArg('codesign');
}
/// Builds an .xcarchive for an iOS app to be generated for App Store submission.
/// Can only be run on a macOS host.
/// For producing deployment .ipas, see https://flutter.dev/docs/deployment/ios.
class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
BuildIOSArchiveCommand({ @required bool verboseHelp }) : super(verboseHelp: verboseHelp);
@override
final String name = 'xcarchive';
@override
final String description = 'Build an iOS archive bundle (Mac OS X host only).';
@override
final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.archive;
@override
final bool forSimulator = false;
@override
final bool configOnly = false;
@override
final bool shouldCodesign = true;
}
abstract class _BuildIOSSubCommand extends BuildSubCommand {
_BuildIOSSubCommand({ @required bool verboseHelp }) {
addTreeShakeIconsFlag();
addSplitDebugInfoOption();
addBuildModeFlags(defaultToRelease: true);
usesTargetOption();
usesFlavorOption();
usesPubOption();
usesBuildNumberOption();
usesBuildNameOption();
addDartObfuscationOption();
usesDartDefineOption();
usesExtraFrontendOptions();
addEnableExperimentation(hide: !verboseHelp);
addBuildPerformanceFile(hide: !verboseHelp);
addBundleSkSLPathOption(hide: !verboseHelp);
addNullSafetyModeOptions(hide: !verboseHelp);
usesAnalyzeSizeFlag();
}
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
DevelopmentArtifact.iOS,
};
XcodeBuildAction get xcodeBuildAction;
bool get forSimulator;
bool get configOnly;
bool get shouldCodesign;
@override
Future<FlutterCommandResult> runCommand() async {
final bool forSimulator = boolArg('simulator');
final bool configOnly = boolArg('config-only');
final bool shouldCodesign = boolArg('codesign');
defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release;
final BuildInfo buildInfo = getBuildInfo();
......@@ -99,7 +143,11 @@ class BuildIOSCommand extends BuildSubCommand {
final String logTarget = forSimulator ? 'simulator' : 'device';
final String typeName = globals.artifacts.getEngineType(TargetPlatform.ios, buildInfo.mode);
globals.printStatus('Building $app for $logTarget ($typeName)...');
if (xcodeBuildAction == XcodeBuildAction.build) {
globals.printStatus('Building $app for $logTarget ($typeName)...');
} else {
globals.printStatus('Archiving $app...');
}
final XcodeBuildResult result = await buildXcodeProject(
app: app,
buildInfo: buildInfo,
......@@ -107,11 +155,12 @@ class BuildIOSCommand extends BuildSubCommand {
buildForDevice: !forSimulator,
codesign: shouldCodesign,
configOnly: configOnly,
buildAction: xcodeBuildAction,
);
if (!result.success) {
await diagnoseXcodeBuildFailure(result, globals.flutterUsage, globals.logger);
throwToolExit('Encountered error while building for $logTarget.');
throwToolExit('Encountered error while ${xcodeBuildAction.name}ing for $logTarget.');
}
if (buildInfo.codeSizeDirectory != null) {
......
......@@ -96,6 +96,7 @@ Future<XcodeBuildResult> buildXcodeProject({
bool codesign = true,
String deviceID,
bool configOnly = false,
XcodeBuildAction buildAction = XcodeBuildAction.build,
}) async {
if (!upgradePbxProjWithFlutterAssets(app.project, globals.logger)) {
return XcodeBuildResult(success: false);
......@@ -321,6 +322,14 @@ Future<XcodeBuildResult> buildXcodeProject({
buildCommands.add('COMPILER_INDEX_STORE_ENABLE=NO');
buildCommands.addAll(environmentVariablesAsXcodeBuildSettings(globals.platform));
if (buildAction == XcodeBuildAction.archive) {
buildCommands.addAll(<String>[
'-archivePath',
globals.fs.path.absolute(app.archiveBundlePath),
'archive',
]);
}
final Stopwatch sw = Stopwatch()..start();
initialBuildStatus = globals.logger.startProgress('Running Xcode build...', timeout: timeoutConfiguration.slowOperation);
......@@ -333,13 +342,13 @@ Future<XcodeBuildResult> buildXcodeProject({
initialBuildStatus?.cancel();
initialBuildStatus = null;
globals.printStatus(
'Xcode build done.'.padRight(kDefaultStatusPadding + 1)
'Xcode ${buildAction.name} done.'.padRight(kDefaultStatusPadding + 1)
+ getElapsedAsSeconds(sw.elapsed).padLeft(5),
);
globals.flutterUsage.sendTiming('build', 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds));
globals.flutterUsage.sendTiming(buildAction.name, 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds));
// Run -showBuildSettings again but with the exact same parameters as the
// build. showBuildSettings is reported to ocassionally timeout. Here, we give
// build. showBuildSettings is reported to occasionally timeout. Here, we give
// it a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
// When there is a timeout, we retry once. See issue #35988.
final List<String> showBuildSettingsCommand = (List<String>
......@@ -398,36 +407,42 @@ Future<XcodeBuildResult> buildXcodeProject({
),
);
} else {
// If the app contains a watch companion target, the sdk argument of xcodebuild has to be omitted.
// For some reason this leads to TARGET_BUILD_DIR always ending in 'iphoneos' even though the
// actual directory will end with 'iphonesimulator' for simulator builds.
// The value of TARGET_BUILD_DIR is adjusted to accommodate for this effect.
String targetBuildDir = buildSettings['TARGET_BUILD_DIR'];
if (hasWatchCompanion && !buildForDevice) {
globals.printTrace('Replacing iphoneos with iphonesimulator in TARGET_BUILD_DIR.');
targetBuildDir = targetBuildDir.replaceFirst('iphoneos', 'iphonesimulator');
}
final String expectedOutputDirectory = globals.fs.path.join(
targetBuildDir,
buildSettings['WRAPPER_NAME'],
);
String outputDir;
if (globals.fs.isDirectorySync(expectedOutputDirectory)) {
// Copy app folder to a place where other tools can find it without knowing
// the BuildInfo.
outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
if (globals.fs.isDirectorySync(outputDir)) {
// Previous output directory might have incompatible artifacts
// (for example, kernel binary files produced from previous run).
globals.fs.directory(outputDir).deleteSync(recursive: true);
if (buildAction == XcodeBuildAction.build) {
// If the app contains a watch companion target, the sdk argument of xcodebuild has to be omitted.
// For some reason this leads to TARGET_BUILD_DIR always ending in 'iphoneos' even though the
// actual directory will end with 'iphonesimulator' for simulator builds.
// The value of TARGET_BUILD_DIR is adjusted to accommodate for this effect.
String targetBuildDir = buildSettings['TARGET_BUILD_DIR'];
if (hasWatchCompanion && !buildForDevice) {
globals.printTrace('Replacing iphoneos with iphonesimulator in TARGET_BUILD_DIR.');
targetBuildDir = targetBuildDir.replaceFirst('iphoneos', 'iphonesimulator');
}
globals.fsUtils.copyDirectorySync(
globals.fs.directory(expectedOutputDirectory),
globals.fs.directory(outputDir),
final String expectedOutputDirectory = globals.fs.path.join(
targetBuildDir,
buildSettings['WRAPPER_NAME'],
);
if (globals.fs.isDirectorySync(expectedOutputDirectory)) {
// Copy app folder to a place where other tools can find it without knowing
// the BuildInfo.
outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
if (globals.fs.isDirectorySync(outputDir)) {
// Previous output directory might have incompatible artifacts
// (for example, kernel binary files produced from previous run).
globals.fs.directory(outputDir).deleteSync(recursive: true);
}
globals.fsUtils.copyDirectorySync(
globals.fs.directory(expectedOutputDirectory),
globals.fs.directory(outputDir),
);
} else {
globals.printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
}
} else {
globals.printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
outputDir = '${globals.fs.path.absolute(app.archiveBundlePath)}.xcarchive';
if (!globals.fs.isDirectorySync(outputDir)) {
globals.printError('Archive succeeded but the expected xcarchive at $outputDir not found');
}
}
return XcodeBuildResult(
success: true,
......@@ -568,6 +583,24 @@ Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result, Usage flutterUsa
}
}
/// xcodebuild <buildaction> parameter (see man xcodebuild for details).
///
/// `clean`, `test`, `analyze`, and `install` are not supported.
enum XcodeBuildAction { build, archive }
extension XcodeBuildActionExtension on XcodeBuildAction {
String get name {
switch (this) {
case XcodeBuildAction.build:
return 'build';
case XcodeBuildAction.archive:
return 'archive';
default:
throw UnsupportedError('Unknown Xcode build action');
}
}
}
class XcodeBuildResult {
XcodeBuildResult({
@required this.success,
......
// 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:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:process/process.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/testbed.dart';
class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInterpreter {
@override
Future<Map<String, String>> getBuildSettings(
String projectPath, {
String scheme,
Duration timeout = const Duration(minutes: 1),
}) async {
return <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
'DEVELOPMENT_TEAM': 'abc',
};
}
}
final Platform macosPlatform = FakePlatform(
operatingSystem: 'macos',
environment: <String, String>{
'FLUTTER_ROOT': '/',
}
);
final Platform notMacosPlatform = FakePlatform(
operatingSystem: 'linux',
environment: <String, String>{
'FLUTTER_ROOT': '/',
}
);
void main() {
FileSystem fileSystem;
Usage usage;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
fileSystem = MemoryFileSystem.test();
usage = Usage.test();
});
// Sets up the minimal mock project files necessary to look like a Flutter project.
void createCoreMockProjectFiles() {
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true);
}
// Sets up the minimal mock project files necessary for iOS builds to succeed.
void createMinimalMockProjectFiles() {
fileSystem.directory(fileSystem.path.join('ios', 'Runner.xcodeproj')).createSync(recursive: true);
fileSystem.directory(fileSystem.path.join('ios', 'Runner.xcworkspace')).createSync(recursive: true);
fileSystem.file(fileSystem.path.join('ios', 'Runner.xcodeproj', 'project.pbxproj')).createSync();
createCoreMockProjectFiles();
}
const FakeCommand xattrCommand = FakeCommand(command: <String>[
'xattr', '-r', '-d', 'com.apple.FinderInfo', '/ios'
]);
// Creates a FakeCommand for the xcodebuild call to build the app
// in the given configuration.
FakeCommand setUpMockXcodeBuildHandler({ bool verbose = false, bool showBuildSettings = false, void Function() onRun }) {
return FakeCommand(
command: <String>[
'/usr/bin/env',
'xcrun',
'xcodebuild',
'-configuration', 'Release',
if (verbose)
'VERBOSE_SCRIPT_LOGGING=YES'
else
'-quiet',
'-workspace', 'Runner.xcworkspace',
'-scheme', 'Runner',
'BUILD_DIR=/build/ios',
'-sdk', 'iphoneos',
'FLUTTER_SUPPRESS_ANALYTICS=true',
'COMPILER_INDEX_STORE_ENABLE=NO',
'-archivePath', '/build/ios/archive/Runner',
'archive',
if (showBuildSettings)
'-showBuildSettings',
],
stdout: 'STDOUT STUFF',
onRun: onRun,
);
}
testUsingContext('xcarchive build fails when there is no ios project', () async {
final BuildCommand command = BuildCommand();
createCoreMockProjectFiles();
expect(createTestCommandRunner(command).run(
const <String>['build', 'xcarchive', '--no-pub']
), throwsToolExit(message: 'Application not configured for iOS'));
}, overrides: <Type, Generator>{
Platform: () => macosPlatform,
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
});
testUsingContext('xcarchive build fails on non-macOS platform', () async {
final BuildCommand command = BuildCommand();
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
fileSystem.file(fileSystem.path.join('lib', 'main.dart'))
.createSync(recursive: true);
expect(createTestCommandRunner(command).run(
const <String>['build', 'xcarchive', '--no-pub']
), throwsToolExit());
}, overrides: <Type, Generator>{
Platform: () => notMacosPlatform,
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
});
testUsingContext('xcarchive build invokes xcode build', () async {
final BuildCommand command = BuildCommand();
createMinimalMockProjectFiles();
await createTestCommandRunner(command).run(
const <String>['build', 'xcarchive', '--no-pub']
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
xattrCommand,
setUpMockXcodeBuildHandler(),
setUpMockXcodeBuildHandler(showBuildSettings: true),
]),
Platform: () => macosPlatform,
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
});
testUsingContext('xcarchive build invokes xcode build with verbosity', () async {
final BuildCommand command = BuildCommand();
createMinimalMockProjectFiles();
await createTestCommandRunner(command).run(
const <String>['build', 'xcarchive', '--no-pub', '-v']
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
xattrCommand,
setUpMockXcodeBuildHandler(verbose: true),
setUpMockXcodeBuildHandler(verbose: true, showBuildSettings: true),
]),
Platform: () => macosPlatform,
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
});
testUsingContext('Performs code size analysis and sends analytics', () async {
final BuildCommand command = BuildCommand();
createMinimalMockProjectFiles();
fileSystem.file('build/ios/Release-iphoneos/Runner.app/Frameworks/App.framework/App')
..createSync(recursive: true)
..writeAsBytesSync(List<int>.generate(10000, (int index) => 0));
// Capture Usage.test() events.
final StringBuffer buffer = await capturedConsolePrint(() =>
createTestCommandRunner(command).run(
const <String>['build', 'xcarchive', '--no-pub', '--analyze-size']
)
);
expect(testLogger.statusText, contains('A summary of your iOS bundle analysis can be found at'));
expect(buffer.toString(), contains('event {category: code-size-analysis, action: ios, label: null, value: null, cd33: '));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
xattrCommand,
setUpMockXcodeBuildHandler(onRun: () {
fileSystem.file('build/flutter_size_01/snapshot.arm64.json')
..createSync(recursive: true)
..writeAsStringSync('''[
{
"l": "dart:_internal",
"c": "SubListIterable",
"n": "[Optimized] skip",
"s": 2400
}
]''');
fileSystem.file('build/flutter_size_01/trace.arm64.json')
..createSync(recursive: true)
..writeAsStringSync('{}');
}),
setUpMockXcodeBuildHandler(showBuildSettings: true),
]),
Platform: () => macosPlatform,
FileSystemUtils: () => FileSystemUtils(fileSystem: fileSystem, platform: macosPlatform),
Usage: () => usage,
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
});
}
......@@ -29,6 +29,7 @@ void main() {
BuildWebCommand(verboseHelp: false),
BuildApkCommand(verboseHelp: false),
BuildIOSCommand(verboseHelp: false),
BuildIOSArchiveCommand(verboseHelp: false),
BuildAppBundleCommand(verboseHelp: false),
BuildFuchsiaCommand(verboseHelp: false),
BuildAarCommand(verboseHelp: false),
......
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