Skip to content

Commit 98aeef2

Browse files
authored
Build xcarchive command (flutter#67598)
1 parent 98d1ad0 commit 98aeef2

File tree

7 files changed

+381
-52
lines changed

7 files changed

+381
-52
lines changed

dev/devicelab/bin/tasks/ios_content_validation_test.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,29 @@ Future<void> main() async {
166166
if (!await localNetworkUsageFound(outputAppPath)) {
167167
throw TaskResult.failure('Debug bundle is missing NSLocalNetworkUsageDescription');
168168
}
169+
170+
section('Clean build');
171+
172+
await inDirectory(flutterProject.rootPath, () async {
173+
await flutter('clean');
174+
});
175+
176+
section('Archive');
177+
178+
await inDirectory(flutterProject.rootPath, () async {
179+
await flutter('build', options: <String>[
180+
'xcarchive',
181+
]);
182+
});
183+
184+
checkDirectoryExists(path.join(
185+
flutterProject.rootPath,
186+
'build',
187+
'ios',
188+
'archive',
189+
'Runner.xcarchive',
190+
'Products',
191+
));
169192
});
170193

171194
return TaskResult.success(null);

packages/flutter_tools/lib/src/application_package.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,12 @@ class BuildableIOSApp extends IOSApp {
379379
@override
380380
String get deviceBundlePath => _buildAppPath('iphoneos');
381381

382+
// Xcode uses this path for the final archive bundle location,
383+
// not a top-level output directory.
384+
// Specifying `build/ios/archive/Runner` will result in `build/ios/archive/Runner.xcarchive`.
385+
String get archiveBundlePath
386+
=> globals.fs.path.join(getIosBuildDirectory(), 'archive', globals.fs.path.withoutExtension(_hostAppBundleName));
387+
382388
String _buildAppPath(String type) {
383389
return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName);
384390
}

packages/flutter_tools/lib/src/commands/build.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class BuildCommand extends FlutterCommand {
2828
buildSystem: globals.buildSystem,
2929
verboseHelp: verboseHelp,
3030
));
31+
addSubcommand(BuildIOSArchiveCommand(verboseHelp: verboseHelp));
3132
addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
3233
addSubcommand(BuildWebCommand(verboseHelp: verboseHelp));
3334
addSubcommand(BuildMacosCommand(verboseHelp: verboseHelp));

packages/flutter_tools/lib/src/commands/build_ios.dart

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,8 @@ import 'build.dart';
1919
/// Builds an .app for an iOS app to be used for local testing on an iOS device
2020
/// or simulator. Can only be run on a macOS host. For producing deployment
2121
/// .ipas, see https://flutter.dev/docs/deployment/ios.
22-
class BuildIOSCommand extends BuildSubCommand {
23-
BuildIOSCommand({ @required bool verboseHelp }) {
24-
addTreeShakeIconsFlag();
25-
addSplitDebugInfoOption();
26-
addBuildModeFlags(defaultToRelease: true);
27-
usesTargetOption();
28-
usesFlavorOption();
29-
usesPubOption();
30-
usesBuildNumberOption();
31-
usesBuildNameOption();
32-
addDartObfuscationOption();
33-
usesDartDefineOption();
34-
usesExtraFrontendOptions();
35-
addEnableExperimentation(hide: !verboseHelp);
36-
addBuildPerformanceFile(hide: !verboseHelp);
37-
addBundleSkSLPathOption(hide: !verboseHelp);
38-
addNullSafetyModeOptions(hide: !verboseHelp);
39-
usesAnalyzeSizeFlag();
22+
class BuildIOSCommand extends _BuildIOSSubCommand {
23+
BuildIOSCommand({ @required bool verboseHelp }) : super(verboseHelp: verboseHelp) {
4024
argParser
4125
..addFlag('config-only',
4226
help: 'Update the project configuration without performing a build. '
@@ -59,16 +43,76 @@ class BuildIOSCommand extends BuildSubCommand {
5943
@override
6044
final String description = 'Build an iOS application bundle (Mac OS X host only).';
6145

46+
@override
47+
final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.build;
48+
49+
@override
50+
bool get forSimulator => boolArg('simulator');
51+
52+
@override
53+
bool get configOnly => boolArg('config-only');
54+
55+
@override
56+
bool get shouldCodesign => boolArg('codesign');
57+
}
58+
59+
/// Builds an .xcarchive for an iOS app to be generated for App Store submission.
60+
/// Can only be run on a macOS host.
61+
/// For producing deployment .ipas, see https://flutter.dev/docs/deployment/ios.
62+
class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
63+
BuildIOSArchiveCommand({ @required bool verboseHelp }) : super(verboseHelp: verboseHelp);
64+
65+
@override
66+
final String name = 'xcarchive';
67+
68+
@override
69+
final String description = 'Build an iOS archive bundle (Mac OS X host only).';
70+
71+
@override
72+
final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.archive;
73+
74+
@override
75+
final bool forSimulator = false;
76+
77+
@override
78+
final bool configOnly = false;
79+
80+
@override
81+
final bool shouldCodesign = true;
82+
}
83+
84+
abstract class _BuildIOSSubCommand extends BuildSubCommand {
85+
_BuildIOSSubCommand({ @required bool verboseHelp }) {
86+
addTreeShakeIconsFlag();
87+
addSplitDebugInfoOption();
88+
addBuildModeFlags(defaultToRelease: true);
89+
usesTargetOption();
90+
usesFlavorOption();
91+
usesPubOption();
92+
usesBuildNumberOption();
93+
usesBuildNameOption();
94+
addDartObfuscationOption();
95+
usesDartDefineOption();
96+
usesExtraFrontendOptions();
97+
addEnableExperimentation(hide: !verboseHelp);
98+
addBuildPerformanceFile(hide: !verboseHelp);
99+
addBundleSkSLPathOption(hide: !verboseHelp);
100+
addNullSafetyModeOptions(hide: !verboseHelp);
101+
usesAnalyzeSizeFlag();
102+
}
103+
62104
@override
63105
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
64106
DevelopmentArtifact.iOS,
65107
};
66108

109+
XcodeBuildAction get xcodeBuildAction;
110+
bool get forSimulator;
111+
bool get configOnly;
112+
bool get shouldCodesign;
113+
67114
@override
68115
Future<FlutterCommandResult> runCommand() async {
69-
final bool forSimulator = boolArg('simulator');
70-
final bool configOnly = boolArg('config-only');
71-
final bool shouldCodesign = boolArg('codesign');
72116
defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release;
73117
final BuildInfo buildInfo = getBuildInfo();
74118

@@ -99,19 +143,24 @@ class BuildIOSCommand extends BuildSubCommand {
99143

100144
final String logTarget = forSimulator ? 'simulator' : 'device';
101145
final String typeName = globals.artifacts.getEngineType(TargetPlatform.ios, buildInfo.mode);
102-
globals.printStatus('Building $app for $logTarget ($typeName)...');
146+
if (xcodeBuildAction == XcodeBuildAction.build) {
147+
globals.printStatus('Building $app for $logTarget ($typeName)...');
148+
} else {
149+
globals.printStatus('Archiving $app...');
150+
}
103151
final XcodeBuildResult result = await buildXcodeProject(
104152
app: app,
105153
buildInfo: buildInfo,
106154
targetOverride: targetFile,
107155
buildForDevice: !forSimulator,
108156
codesign: shouldCodesign,
109157
configOnly: configOnly,
158+
buildAction: xcodeBuildAction,
110159
);
111160

112161
if (!result.success) {
113162
await diagnoseXcodeBuildFailure(result, globals.flutterUsage, globals.logger);
114-
throwToolExit('Encountered error while building for $logTarget.');
163+
throwToolExit('Encountered error while ${xcodeBuildAction.name}ing for $logTarget.');
115164
}
116165

117166
if (buildInfo.codeSizeDirectory != null) {

packages/flutter_tools/lib/src/ios/mac.dart

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Future<XcodeBuildResult> buildXcodeProject({
9696
bool codesign = true,
9797
String deviceID,
9898
bool configOnly = false,
99+
XcodeBuildAction buildAction = XcodeBuildAction.build,
99100
}) async {
100101
if (!upgradePbxProjWithFlutterAssets(app.project, globals.logger)) {
101102
return XcodeBuildResult(success: false);
@@ -321,6 +322,14 @@ Future<XcodeBuildResult> buildXcodeProject({
321322
buildCommands.add('COMPILER_INDEX_STORE_ENABLE=NO');
322323
buildCommands.addAll(environmentVariablesAsXcodeBuildSettings(globals.platform));
323324

325+
if (buildAction == XcodeBuildAction.archive) {
326+
buildCommands.addAll(<String>[
327+
'-archivePath',
328+
globals.fs.path.absolute(app.archiveBundlePath),
329+
'archive',
330+
]);
331+
}
332+
324333
final Stopwatch sw = Stopwatch()..start();
325334
initialBuildStatus = globals.logger.startProgress('Running Xcode build...', timeout: timeoutConfiguration.slowOperation);
326335

@@ -333,13 +342,13 @@ Future<XcodeBuildResult> buildXcodeProject({
333342
initialBuildStatus?.cancel();
334343
initialBuildStatus = null;
335344
globals.printStatus(
336-
'Xcode build done.'.padRight(kDefaultStatusPadding + 1)
345+
'Xcode ${buildAction.name} done.'.padRight(kDefaultStatusPadding + 1)
337346
+ getElapsedAsSeconds(sw.elapsed).padLeft(5),
338347
);
339-
globals.flutterUsage.sendTiming('build', 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds));
348+
globals.flutterUsage.sendTiming(buildAction.name, 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds));
340349

341350
// Run -showBuildSettings again but with the exact same parameters as the
342-
// build. showBuildSettings is reported to ocassionally timeout. Here, we give
351+
// build. showBuildSettings is reported to occasionally timeout. Here, we give
343352
// it a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
344353
// When there is a timeout, we retry once. See issue #35988.
345354
final List<String> showBuildSettingsCommand = (List<String>
@@ -398,36 +407,42 @@ Future<XcodeBuildResult> buildXcodeProject({
398407
),
399408
);
400409
} else {
401-
// If the app contains a watch companion target, the sdk argument of xcodebuild has to be omitted.
402-
// For some reason this leads to TARGET_BUILD_DIR always ending in 'iphoneos' even though the
403-
// actual directory will end with 'iphonesimulator' for simulator builds.
404-
// The value of TARGET_BUILD_DIR is adjusted to accommodate for this effect.
405-
String targetBuildDir = buildSettings['TARGET_BUILD_DIR'];
406-
if (hasWatchCompanion && !buildForDevice) {
407-
globals.printTrace('Replacing iphoneos with iphonesimulator in TARGET_BUILD_DIR.');
408-
targetBuildDir = targetBuildDir.replaceFirst('iphoneos', 'iphonesimulator');
409-
}
410-
final String expectedOutputDirectory = globals.fs.path.join(
411-
targetBuildDir,
412-
buildSettings['WRAPPER_NAME'],
413-
);
414-
415410
String outputDir;
416-
if (globals.fs.isDirectorySync(expectedOutputDirectory)) {
417-
// Copy app folder to a place where other tools can find it without knowing
418-
// the BuildInfo.
419-
outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
420-
if (globals.fs.isDirectorySync(outputDir)) {
421-
// Previous output directory might have incompatible artifacts
422-
// (for example, kernel binary files produced from previous run).
423-
globals.fs.directory(outputDir).deleteSync(recursive: true);
411+
if (buildAction == XcodeBuildAction.build) {
412+
// If the app contains a watch companion target, the sdk argument of xcodebuild has to be omitted.
413+
// For some reason this leads to TARGET_BUILD_DIR always ending in 'iphoneos' even though the
414+
// actual directory will end with 'iphonesimulator' for simulator builds.
415+
// The value of TARGET_BUILD_DIR is adjusted to accommodate for this effect.
416+
String targetBuildDir = buildSettings['TARGET_BUILD_DIR'];
417+
if (hasWatchCompanion && !buildForDevice) {
418+
globals.printTrace('Replacing iphoneos with iphonesimulator in TARGET_BUILD_DIR.');
419+
targetBuildDir = targetBuildDir.replaceFirst('iphoneos', 'iphonesimulator');
424420
}
425-
globals.fsUtils.copyDirectorySync(
426-
globals.fs.directory(expectedOutputDirectory),
427-
globals.fs.directory(outputDir),
421+
final String expectedOutputDirectory = globals.fs.path.join(
422+
targetBuildDir,
423+
buildSettings['WRAPPER_NAME'],
428424
);
425+
if (globals.fs.isDirectorySync(expectedOutputDirectory)) {
426+
// Copy app folder to a place where other tools can find it without knowing
427+
// the BuildInfo.
428+
outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
429+
if (globals.fs.isDirectorySync(outputDir)) {
430+
// Previous output directory might have incompatible artifacts
431+
// (for example, kernel binary files produced from previous run).
432+
globals.fs.directory(outputDir).deleteSync(recursive: true);
433+
}
434+
globals.fsUtils.copyDirectorySync(
435+
globals.fs.directory(expectedOutputDirectory),
436+
globals.fs.directory(outputDir),
437+
);
438+
} else {
439+
globals.printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
440+
}
429441
} else {
430-
globals.printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
442+
outputDir = '${globals.fs.path.absolute(app.archiveBundlePath)}.xcarchive';
443+
if (!globals.fs.isDirectorySync(outputDir)) {
444+
globals.printError('Archive succeeded but the expected xcarchive at $outputDir not found');
445+
}
431446
}
432447
return XcodeBuildResult(
433448
success: true,
@@ -568,6 +583,24 @@ Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result, Usage flutterUsa
568583
}
569584
}
570585

586+
/// xcodebuild <buildaction> parameter (see man xcodebuild for details).
587+
///
588+
/// `clean`, `test`, `analyze`, and `install` are not supported.
589+
enum XcodeBuildAction { build, archive }
590+
591+
extension XcodeBuildActionExtension on XcodeBuildAction {
592+
String get name {
593+
switch (this) {
594+
case XcodeBuildAction.build:
595+
return 'build';
596+
case XcodeBuildAction.archive:
597+
return 'archive';
598+
default:
599+
throw UnsupportedError('Unknown Xcode build action');
600+
}
601+
}
602+
}
603+
571604
class XcodeBuildResult {
572605
XcodeBuildResult({
573606
@required this.success,

0 commit comments

Comments
 (0)