Skip to content

Commit e18c5e2

Browse files
authored
Improve build time when using SwiftPM (#150052)
When using SwiftPM, we use `flutter assemble` in an Xcode Pre-action to run the `debug_unpack_macos` (or profile/release) target. This target is also later used in a Run Script build phase. Depending on `ARCHS` build setting, the Flutter/FlutterMacOS binary is thinned. In the Run Script build phase, `ARCHS` is filtered to the active arch. However, in the Pre-action it doesn't always filter to the active arch. As a workaround, assume arm64 if the [`NATIVE_ARCH`](https://developer.apple.com/documentation/xcode/build-settings-reference/#NATIVEARCH) is arm, otherwise assume x86_64. Also, this PR adds a define flag `PreBuildAction`, which gives the Pre-action a [unique configuration of defines](https://github.com/flutter/flutter/blob/fdb74fd3e7e08d99d8e19a0d7c548cf6cab56b31/packages/flutter_tools/lib/src/build_system/build_system.dart#L355-L372) and therefore a separate filecache from the Run Script build phase filecache. This improves caching so the Run Script build phase action doesn't find missing targets in the filecache. It also uses this flag to skip cleaning up the previous build files. Lastly, skip the Pre-action if the build command is `clean`. Note: For iOS, if [`CodesignIdentity`](https://github.com/flutter/flutter/blob/14df7be3f9471a97f34e4601fb7710850373ac3b/packages/flutter_tools/bin/xcode_backend.dart#L470-L473) is set, the Pre-action and Run Script build phase will not match because the Pre-action does not send the `EXPANDED_CODE_SIGN_IDENTITY` and therefore will codesign it with [`-`](https://github.com/flutter/flutter/blob/14df7be3f9471a97f34e4601fb7710850373ac3b/packages/flutter_tools/lib/src/build_system/targets/ios.dart#L695) instead. This will cause `debug_unpack_macos` to invalidate and rerun every time. A potential solution would be to move [codesigning out of the Run Script build phase](https://github.com/flutter/flutter/blob/14df7be3f9471a97f34e4601fb7710850373ac3b/packages/flutter_tools/lib/src/build_system/targets/ios.dart#L299) to the [Thin Binary build phase](https://github.com/flutter/flutter/blob/14df7be3f9471a97f34e4601fb7710850373ac3b/packages/flutter_tools/bin/xcode_backend.dart#L204-L257) after it's copied into the TARGET_BUILD_DIR, like we do with [macOS](https://github.com/flutter/flutter/blob/14df7be3f9471a97f34e4601fb7710850373ac3b/packages/flutter_tools/bin/macos_assemble.sh#L179-L183).
1 parent d68e05b commit e18c5e2

File tree

7 files changed

+276
-16
lines changed

7 files changed

+276
-16
lines changed

packages/flutter_tools/bin/macos_assemble.sh

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,28 @@ BuildApp() {
111111
if [[ -n "$LOCAL_ENGINE_HOST" ]]; then
112112
flutter_args+=("--local-engine-host=${LOCAL_ENGINE_HOST}")
113113
fi
114+
115+
local architectures="${ARCHS}"
116+
if [[ -n "$1" && "$1" == "prepare" ]]; then
117+
# The "prepare" command runs in a pre-action script, which doesn't always
118+
# filter the "ARCHS" build setting to only the active arch. To workaround,
119+
# if "ONLY_ACTIVE_ARCH" is true and the "NATIVE_ARCH" is arm, assume the
120+
# active arch is also arm to improve caching. If this assumption is
121+
# incorrect, it will later be corrected by the "build" command.
122+
if [[ -n "$ONLY_ACTIVE_ARCH" && "$ONLY_ACTIVE_ARCH" == "YES" && -n "$NATIVE_ARCH" ]]; then
123+
if [[ "$NATIVE_ARCH" == *"arm"* ]]; then
124+
architectures="arm64"
125+
else
126+
architectures="x86_64"
127+
fi
128+
fi
129+
fi
130+
114131
flutter_args+=(
115132
"assemble"
116133
"--no-version-check"
117134
"-dTargetPlatform=darwin"
118-
"-dDarwinArchs=${ARCHS}"
135+
"-dDarwinArchs=${architectures}"
119136
"-dTargetFile=${target_path}"
120137
"-dBuildMode=${build_mode}"
121138
"-dTreeShakeIcons=${TREE_SHAKE_ICONS}"
@@ -132,6 +149,19 @@ BuildApp() {
132149
"--output=${BUILT_PRODUCTS_DIR}"
133150
)
134151

152+
local target="${build_mode}_macos_bundle_flutter_assets";
153+
if [[ -n "$1" && "$1" == "prepare" ]]; then
154+
# The "prepare" command only targets the UnpackMacOS target, which copies the
155+
# FlutterMacOS framework to the BUILT_PRODUCTS_DIR.
156+
target="${build_mode}_unpack_macos"
157+
158+
# Use the PreBuildAction define flag to force the tool to use a different
159+
# filecache file for the "prepare" command. This will make the environment
160+
# buildPrefix for the "prepare" command unique from the "build" command.
161+
# This will improve caching since the "build" command has more target dependencies.
162+
flutter_args+=("-dPreBuildAction=PrepareFramework")
163+
fi
164+
135165
if [[ -n "$FLAVOR" ]]; then
136166
flutter_args+=("-dFlavor=${FLAVOR}")
137167
fi
@@ -145,20 +175,18 @@ BuildApp() {
145175
flutter_args+=("-dCodeSizeDirectory=${CODE_SIZE_DIRECTORY}")
146176
fi
147177

148-
# Run flutter assemble with the build mode specific target that was passed in.
149-
# If no target was passed it, default to build mode specific
150-
# macos_bundle_flutter_assets target.
151-
if [[ -n "$1" ]]; then
152-
flutter_args+=("${build_mode}$1")
153-
else
154-
flutter_args+=("${build_mode}_macos_bundle_flutter_assets")
155-
fi
178+
flutter_args+=("${target}")
156179

157180
RunCommand "${flutter_args[@]}"
158181
}
159182

160183
PrepareFramework() {
161-
BuildApp "_unpack_macos"
184+
# The "prepare" command runs in a pre-action script, which also runs when
185+
# using the Xcode/xcodebuild clean command. Skip if cleaning.
186+
if [[ $ACTION == "clean" ]]; then
187+
exit 0
188+
fi
189+
BuildApp "prepare"
162190
}
163191

164192
# Adds the App.framework as an embedded binary, the flutter_assets as

packages/flutter_tools/bin/xcode_backend.dart

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,14 +354,25 @@ class Context {
354354
}
355355

356356
void prepare() {
357+
// The "prepare" command runs in a pre-action script, which also runs when
358+
// using the Xcode/xcodebuild clean command. Skip if cleaning.
359+
if (environment['ACTION'] == 'clean') {
360+
return;
361+
}
357362
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
358363
final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
359364
final String projectPath = environment['FLUTTER_APPLICATION_PATH'] ?? '$sourceRoot/..';
360365

361366
final String buildMode = parseFlutterBuildMode();
362367

363-
final List<String> flutterArgs = _generateFlutterArgsForAssemble(buildMode, verbose);
368+
final List<String> flutterArgs = _generateFlutterArgsForAssemble(
369+
'prepare',
370+
buildMode,
371+
verbose,
372+
);
364373

374+
// The "prepare" command only targets the UnpackIOS target, which copies the
375+
// Flutter framework to the BUILT_PRODUCTS_DIR.
365376
flutterArgs.add('${buildMode}_unpack_ios');
366377

367378
final ProcessResult result = runSync(
@@ -385,7 +396,11 @@ class Context {
385396

386397
final String buildMode = parseFlutterBuildMode();
387398

388-
final List<String> flutterArgs = _generateFlutterArgsForAssemble(buildMode, verbose);
399+
final List<String> flutterArgs = _generateFlutterArgsForAssemble(
400+
'build',
401+
buildMode,
402+
verbose,
403+
);
389404

390405
flutterArgs.add('${buildMode}_ios_bundle_flutter_assets');
391406

@@ -408,7 +423,11 @@ class Context {
408423
echo('Project $projectPath built and packaged successfully.');
409424
}
410425

411-
List<String> _generateFlutterArgsForAssemble(String buildMode, bool verbose) {
426+
List<String> _generateFlutterArgsForAssemble(
427+
String command,
428+
String buildMode,
429+
bool verbose,
430+
) {
412431
String targetPath = 'lib/main.dart';
413432
if (environment['FLUTTER_TARGET'] != null) {
414433
targetPath = environment['FLUTTER_TARGET']!;
@@ -442,6 +461,22 @@ class Context {
442461
flutterArgs.add('--local-engine-host=${environment['LOCAL_ENGINE_HOST']}');
443462
}
444463

464+
String architectures = environment['ARCHS'] ?? '';
465+
if (command == 'prepare') {
466+
// The "prepare" command runs in a pre-action script, which doesn't always
467+
// filter the "ARCHS" build setting to only the active arch. To workaround,
468+
// if "ONLY_ACTIVE_ARCH" is true and the "NATIVE_ARCH" is arm, assume the
469+
// active arch is also arm to improve caching. If this assumption is
470+
// incorrect, it will later be corrected by the "build" command.
471+
if (environment['ONLY_ACTIVE_ARCH'] == 'YES' && environment['NATIVE_ARCH'] != null) {
472+
if (environment['NATIVE_ARCH']!.contains('arm')) {
473+
architectures = 'arm64';
474+
} else {
475+
architectures = 'x86_64';
476+
}
477+
}
478+
}
479+
445480
flutterArgs.addAll(<String>[
446481
'assemble',
447482
'--no-version-check',
@@ -450,7 +485,7 @@ class Context {
450485
'-dTargetFile=$targetPath',
451486
'-dBuildMode=$buildMode',
452487
if (environment['FLAVOR'] != null) '-dFlavor=${environment['FLAVOR']}',
453-
'-dIosArchs=${environment['ARCHS'] ?? ''}',
488+
'-dIosArchs=$architectures',
454489
'-dSdkRoot=${environment['SDKROOT'] ?? ''}',
455490
'-dSplitDebugInfo=${environment['SPLIT_DEBUG_INFO'] ?? ''}',
456491
'-dTreeShakeIcons=${environment['TREE_SHAKE_ICONS'] ?? ''}',
@@ -463,6 +498,14 @@ class Context {
463498
'--ExtraFrontEndOptions=${environment['EXTRA_FRONT_END_OPTIONS'] ?? ''}',
464499
]);
465500

501+
if (command == 'prepare') {
502+
// Use the PreBuildAction define flag to force the tool to use a different
503+
// filecache file for the "prepare" command. This will make the environment
504+
// buildPrefix for the "prepare" command unique from the "build" command.
505+
// This will improve caching since the "build" command has more target dependencies.
506+
flutterArgs.add('-dPreBuildAction=PrepareFramework');
507+
}
508+
466509
if (environment['PERFORMANCE_MEASUREMENT_FILE'] != null && environment['PERFORMANCE_MEASUREMENT_FILE']!.isNotEmpty) {
467510
flutterArgs.add('--performance-measurement-file=${environment['PERFORMANCE_MEASUREMENT_FILE']}');
468511
}

packages/flutter_tools/lib/src/build_info.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,12 @@ const String kBuildNumber = 'BuildNumber';
958958
/// Will be "build" when building and "install" when archiving.
959959
const String kXcodeAction = 'Action';
960960

961+
/// The define of the Xcode build Pre-action.
962+
///
963+
/// Will be "PrepareFramework" when copying the Flutter/FlutterMacOS framework
964+
/// to the BUILT_PRODUCTS_DIR prior to the build.
965+
const String kXcodePreAction = 'PreBuildAction';
966+
961967
final Converter<String, String> _defineEncoder = utf8.encoder.fuse(base64.encoder);
962968
final Converter<String, String> _defineDecoder = base64.decoder.fuse(utf8.decoder);
963969

packages/flutter_tools/lib/src/build_system/build_system.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import '../base/file_system.dart';
1616
import '../base/logger.dart';
1717
import '../base/platform.dart';
1818
import '../base/utils.dart';
19+
import '../build_info.dart';
1920
import '../cache.dart';
2021
import '../convert.dart';
2122
import '../reporting/reporting.dart';
@@ -735,6 +736,13 @@ class FlutterBuildSystem extends BuildSystem {
735736
FileSystem fileSystem,
736737
Map<String, File> currentOutputs,
737738
) {
739+
if (environment.defines[kXcodePreAction] == 'PrepareFramework') {
740+
// If the current build is the PrepareFramework Xcode pre-action, skip
741+
// updating the last build identifier and cleaning up the previous build
742+
// since this build is not a complete build.
743+
return;
744+
}
745+
738746
final String currentBuildId = fileSystem.path.basename(environment.buildDir.path);
739747
final File lastBuildIdFile = environment.outputDir.childFile('.last_build_id');
740748
if (!lastBuildIdFile.existsSync()) {

packages/flutter_tools/test/general.shard/xcode_backend_test.dart

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ void main() {
262262
'--ExtraGenSnapshotOptions=',
263263
'--DartDefines=',
264264
'--ExtraFrontEndOptions=',
265+
'-dPreBuildAction=PrepareFramework',
265266
'debug_unpack_ios',
266267
],
267268
),
@@ -315,6 +316,7 @@ void main() {
315316
'--ExtraGenSnapshotOptions=',
316317
'--DartDefines=',
317318
'--ExtraFrontEndOptions=',
319+
'-dPreBuildAction=PrepareFramework',
318320
'debug_unpack_ios',
319321
],
320322
),
@@ -386,6 +388,7 @@ void main() {
386388
'--ExtraGenSnapshotOptions=$extraGenSnapshotOptions',
387389
'--DartDefines=$dartDefines',
388390
'--ExtraFrontEndOptions=$extraFrontEndOptions',
391+
'-dPreBuildAction=PrepareFramework',
389392
'-dCodesignIdentity=$expandedCodeSignIdentity',
390393
'release_unpack_ios',
391394
],
@@ -395,6 +398,107 @@ void main() {
395398
)..run();
396399
expect(context.stderr, isEmpty);
397400
});
401+
402+
test('assumes ARCHS based on NATIVE_ARCH if ONLY_ACTIVE_ARCH is YES', () {
403+
final Directory buildDir = fileSystem.directory('/path/to/builds')
404+
..createSync(recursive: true);
405+
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
406+
..createSync(recursive: true);
407+
final File pipe = fileSystem.file('/tmp/pipe')
408+
..createSync(recursive: true);
409+
const String buildMode = 'Debug';
410+
final TestContext context = TestContext(
411+
<String>['prepare'],
412+
<String, String>{
413+
'BUILT_PRODUCTS_DIR': buildDir.path,
414+
'CONFIGURATION': buildMode,
415+
'FLUTTER_ROOT': flutterRoot.path,
416+
'INFOPLIST_PATH': 'Info.plist',
417+
'ARCHS': 'arm64 x86_64',
418+
'ONLY_ACTIVE_ARCH': 'YES',
419+
'NATIVE_ARCH': 'arm64e'
420+
},
421+
commands: <FakeCommand>[
422+
FakeCommand(
423+
command: <String>[
424+
'${flutterRoot.path}/bin/flutter',
425+
'assemble',
426+
'--no-version-check',
427+
'--output=${buildDir.path}/',
428+
'-dTargetPlatform=ios',
429+
'-dTargetFile=lib/main.dart',
430+
'-dBuildMode=${buildMode.toLowerCase()}',
431+
'-dIosArchs=arm64',
432+
'-dSdkRoot=',
433+
'-dSplitDebugInfo=',
434+
'-dTreeShakeIcons=',
435+
'-dTrackWidgetCreation=',
436+
'-dDartObfuscation=',
437+
'-dAction=',
438+
'-dFrontendServerStarterPath=',
439+
'--ExtraGenSnapshotOptions=',
440+
'--DartDefines=',
441+
'--ExtraFrontEndOptions=',
442+
'-dPreBuildAction=PrepareFramework',
443+
'debug_unpack_ios',
444+
],
445+
),
446+
],
447+
fileSystem: fileSystem,
448+
scriptOutputStreamFile: pipe,
449+
)..run();
450+
expect(context.stderr, isEmpty);
451+
});
452+
453+
test('does not assumes ARCHS if ONLY_ACTIVE_ARCH is not YES', () {
454+
final Directory buildDir = fileSystem.directory('/path/to/builds')
455+
..createSync(recursive: true);
456+
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
457+
..createSync(recursive: true);
458+
final File pipe = fileSystem.file('/tmp/pipe')
459+
..createSync(recursive: true);
460+
const String buildMode = 'Debug';
461+
final TestContext context = TestContext(
462+
<String>['prepare'],
463+
<String, String>{
464+
'BUILT_PRODUCTS_DIR': buildDir.path,
465+
'CONFIGURATION': buildMode,
466+
'FLUTTER_ROOT': flutterRoot.path,
467+
'INFOPLIST_PATH': 'Info.plist',
468+
'ARCHS': 'arm64 x86_64',
469+
'NATIVE_ARCH': 'arm64e'
470+
},
471+
commands: <FakeCommand>[
472+
FakeCommand(
473+
command: <String>[
474+
'${flutterRoot.path}/bin/flutter',
475+
'assemble',
476+
'--no-version-check',
477+
'--output=${buildDir.path}/',
478+
'-dTargetPlatform=ios',
479+
'-dTargetFile=lib/main.dart',
480+
'-dBuildMode=${buildMode.toLowerCase()}',
481+
'-dIosArchs=arm64 x86_64',
482+
'-dSdkRoot=',
483+
'-dSplitDebugInfo=',
484+
'-dTreeShakeIcons=',
485+
'-dTrackWidgetCreation=',
486+
'-dDartObfuscation=',
487+
'-dAction=',
488+
'-dFrontendServerStarterPath=',
489+
'--ExtraGenSnapshotOptions=',
490+
'--DartDefines=',
491+
'--ExtraFrontEndOptions=',
492+
'-dPreBuildAction=PrepareFramework',
493+
'debug_unpack_ios',
494+
],
495+
),
496+
],
497+
fileSystem: fileSystem,
498+
scriptOutputStreamFile: pipe,
499+
)..run();
500+
expect(context.stderr, isEmpty);
501+
});
398502
});
399503
}
400504

0 commit comments

Comments
 (0)