1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:meta/meta.dart';
6import 'package:process/process.dart';
7
8import '../artifacts.dart';
9import '../base/common.dart';
10import '../base/file_system.dart';
11import '../base/io.dart';
12import '../base/logger.dart';
13import '../base/platform.dart';
14import '../base/process.dart';
15import '../build_info.dart';
16import '../build_system/build_system.dart';
17import '../build_system/targets/ios.dart';
18import '../cache.dart';
19import '../darwin/darwin.dart';
20import '../flutter_plugins.dart';
21import '../globals.dart' as globals;
22import '../ios/xcodeproj.dart';
23import '../macos/cocoapod_utils.dart';
24import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
25import '../version.dart';
26import 'build.dart';
27
28abstract class BuildFrameworkCommand extends BuildSubCommand {
29 BuildFrameworkCommand({
30 // Instantiating FlutterVersion kicks off networking, so delay until it's needed, but allow test injection.
31 @visibleForTesting FlutterVersion? flutterVersion,
32 required BuildSystem buildSystem,
33 required bool verboseHelp,
34 Cache? cache,
35 Platform? platform,
36 required super.logger,
37 }) : _injectedFlutterVersion = flutterVersion,
38 _buildSystem = buildSystem,
39 _injectedCache = cache,
40 _injectedPlatform = platform,
41 super(verboseHelp: verboseHelp) {
42 addTreeShakeIconsFlag();
43 usesTargetOption();
44 usesPubOption();
45 usesDartDefineOption();
46 addSplitDebugInfoOption();
47 addDartObfuscationOption();
48 usesExtraDartFlagOptions(verboseHelp: verboseHelp);
49 addEnableExperimentation(hide: !verboseHelp);
50
51 argParser
52 ..addFlag(
53 'debug',
54 defaultsTo: true,
55 help:
56 'Whether to produce a framework for the debug build configuration. '
57 'By default, all build configurations are built.',
58 )
59 ..addFlag(
60 'profile',
61 defaultsTo: true,
62 help:
63 'Whether to produce a framework for the profile build configuration. '
64 'By default, all build configurations are built.',
65 )
66 ..addFlag(
67 'release',
68 defaultsTo: true,
69 help:
70 'Whether to produce a framework for the release build configuration. '
71 'By default, all build configurations are built.',
72 )
73 ..addFlag(
74 'cocoapods',
75 help:
76 'Produce a Flutter.podspec instead of an engine Flutter.xcframework (recommended if host app uses CocoaPods).',
77 )
78 ..addFlag(
79 'plugins',
80 defaultsTo: true,
81 help:
82 'Whether to produce frameworks for the plugins. '
83 'This is intended for cases where plugins are already being built separately.',
84 )
85 ..addFlag(
86 'static',
87 help:
88 'Build plugins as static frameworks. Link on, but do not embed these frameworks in the existing Xcode project.',
89 )
90 ..addOption(
91 'output',
92 abbr: 'o',
93 valueHelp: 'path/to/directory/',
94 help: 'Location to write the frameworks.',
95 )
96 ..addFlag(
97 'force',
98 abbr: 'f',
99 help:
100 'Force Flutter.podspec creation on the master channel. This is only intended for testing the tool itself.',
101 hide: !verboseHelp,
102 );
103 }
104
105 final BuildSystem? _buildSystem;
106 @protected
107 BuildSystem get buildSystem => _buildSystem ?? globals.buildSystem;
108
109 @protected
110 Cache get cache => _injectedCache ?? globals.cache;
111 final Cache? _injectedCache;
112
113 @protected
114 Platform get platform => _injectedPlatform ?? globals.platform;
115 final Platform? _injectedPlatform;
116
117 // FlutterVersion.instance kicks off git processing which can sometimes fail, so don't try it until needed.
118 @protected
119 FlutterVersion get flutterVersion => _injectedFlutterVersion ?? globals.flutterVersion;
120 final FlutterVersion? _injectedFlutterVersion;
121
122 Future<List<BuildInfo>> getBuildInfos() async {
123 return <BuildInfo>[
124 if (boolArg('debug')) await getBuildInfo(forcedBuildMode: BuildMode.debug),
125 if (boolArg('profile')) await getBuildInfo(forcedBuildMode: BuildMode.profile),
126 if (boolArg('release')) await getBuildInfo(forcedBuildMode: BuildMode.release),
127 ];
128 }
129
130 @override
131 bool get supported => platform.isMacOS;
132
133 @override
134 Future<void> validateCommand() async {
135 await super.validateCommand();
136 if (!supported) {
137 throwToolExit('Building frameworks for iOS is only supported on the Mac.');
138 }
139
140 if ((await getBuildInfos()).isEmpty) {
141 throwToolExit('At least one of "--debug" or "--profile", or "--release" is required.');
142 }
143
144 if (!boolArg('plugins') && boolArg('static')) {
145 throwToolExit('--static cannot be used with the --no-plugins flag');
146 }
147 }
148
149 static Future<void> produceXCFramework(
150 Iterable<Directory> frameworks,
151 String frameworkBinaryName,
152 Directory outputDirectory,
153 ProcessManager processManager,
154 ) async {
155 final xcframeworkCommand = <String>[
156 'xcrun',
157 'xcodebuild',
158 '-create-xcframework',
159 for (final Directory framework in frameworks) ...<String>[
160 '-framework',
161 framework.path,
162 ...framework.parent
163 .listSync()
164 .where(
165 (FileSystemEntity entity) =>
166 entity.basename.endsWith('dSYM') && !entity.basename.startsWith('Flutter'),
167 )
168 .map((FileSystemEntity entity) => <String>['-debug-symbols', entity.path])
169 .expand<String>((List<String> parameter) => parameter),
170 ],
171 '-output',
172 outputDirectory.childDirectory('$frameworkBinaryName.xcframework').path,
173 ];
174
175 final ProcessResult xcframeworkResult = await processManager.run(xcframeworkCommand);
176
177 if (xcframeworkResult.exitCode != 0) {
178 throwToolExit(
179 'Unable to create $frameworkBinaryName.xcframework: ${xcframeworkResult.stderr}',
180 );
181 }
182 }
183}
184
185/// Produces a .framework for integration into a host iOS app. The .framework
186/// contains the Flutter engine and framework code as well as plugins. It can
187/// be integrated into plain Xcode projects without using or other package
188/// managers.
189class BuildIOSFrameworkCommand extends BuildFrameworkCommand {
190 BuildIOSFrameworkCommand({
191 required super.logger,
192 super.flutterVersion,
193 required super.buildSystem,
194 required bool verboseHelp,
195 super.cache,
196 super.platform,
197 }) : super(verboseHelp: verboseHelp) {
198 usesFlavorOption();
199
200 argParser
201 ..addFlag(
202 'universal',
203 help: '(deprecated) Produce universal frameworks that include all valid architectures.',
204 hide: !verboseHelp,
205 )
206 ..addFlag(
207 'xcframework',
208 help: 'Produce xcframeworks that include all valid architectures.',
209 negatable: false,
210 defaultsTo: true,
211 hide: !verboseHelp,
212 );
213 }
214
215 @override
216 final name = 'ios-framework';
217
218 @override
219 final description =
220 'Produces .xcframeworks for a Flutter project '
221 'and its plugins for integration into existing, plain iOS Xcode projects.\n'
222 'This can only be run on macOS hosts.';
223
224 @override
225 Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
226 DevelopmentArtifact.iOS,
227 };
228
229 @override
230 Future<void> validateCommand() async {
231 await super.validateCommand();
232
233 if (boolArg('universal')) {
234 throwToolExit('--universal has been deprecated, only XCFrameworks are supported.');
235 }
236 }
237
238 @override
239 bool get regeneratePlatformSpecificToolingDuringVerify => false;
240
241 @override
242 Future<FlutterCommandResult> runCommand() async {
243 final String outputArgument =
244 stringArg('output') ??
245 globals.fs.path.join(globals.fs.currentDirectory.path, 'build', 'ios', 'framework');
246
247 if (outputArgument.isEmpty) {
248 throwToolExit('--output is required.');
249 }
250
251 if (!project.ios.existsSync()) {
252 throwToolExit('Project does not support iOS');
253 }
254
255 final Directory outputDirectory = globals.fs.directory(
256 globals.fs.path.absolute(globals.fs.path.normalize(outputArgument)),
257 );
258 final List<BuildInfo> buildInfos = await getBuildInfos();
259 for (final buildInfo in buildInfos) {
260 // Create the build-mode specific metadata.
261 //
262 // This normally would be done in the verifyAndRun step of FlutterCommand, but special "meta"
263 // build commands (like flutter build ios-framework) make multiple builds, and do not have a
264 // single "buildInfo", so the step has to be done manually for each build.
265 //
266 // See regeneratePlatformSpecificToolingDurifyVerify.
267 await regeneratePlatformSpecificToolingIfApplicable(
268 project,
269 releaseMode: buildInfo.mode.isRelease,
270 );
271
272 final String? productBundleIdentifier = await project.ios.productBundleIdentifier(buildInfo);
273 globals.printStatus(
274 'Building frameworks for $productBundleIdentifier in ${buildInfo.mode.cliName} mode...',
275 );
276
277 final String xcodeBuildConfiguration = buildInfo.mode.uppercaseName;
278 final Directory modeDirectory = outputDirectory.childDirectory(xcodeBuildConfiguration);
279
280 if (modeDirectory.existsSync()) {
281 modeDirectory.deleteSync(recursive: true);
282 }
283
284 if (boolArg('cocoapods')) {
285 produceFlutterPodspec(buildInfo.mode, modeDirectory, force: boolArg('force'));
286 } else {
287 // Copy Flutter.xcframework.
288 await _produceFlutterFramework(buildInfo, modeDirectory);
289 }
290
291 // Build aot, create module.framework and copy.
292 final Directory iPhoneBuildOutput = modeDirectory.childDirectory(
293 XcodeSdk.IPhoneOS.platformName,
294 );
295 final Directory simulatorBuildOutput = modeDirectory.childDirectory(
296 XcodeSdk.IPhoneSimulator.platformName,
297 );
298 await _produceAppFramework(buildInfo, modeDirectory, iPhoneBuildOutput, simulatorBuildOutput);
299
300 // Build and copy plugins.
301 await processPodsIfNeeded(
302 project.ios,
303 getIosBuildDirectory(),
304 buildInfo.mode,
305 forceCocoaPodsOnly: true,
306 );
307 if (boolArg('plugins') && hasPlugins(project)) {
308 await _producePlugins(
309 buildInfo.mode,
310 xcodeBuildConfiguration,
311 iPhoneBuildOutput,
312 simulatorBuildOutput,
313 modeDirectory,
314 );
315 }
316
317 final Status status = globals.logger.startProgress(
318 ' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}',
319 );
320
321 // Copy the native assets. The native assets have already been signed in
322 // buildNativeAssetsMacOS.
323 final Directory nativeAssetsDirectory = globals.fs
324 .directory(getBuildDirectory())
325 .childDirectory('native_assets/ios/');
326 if (await nativeAssetsDirectory.exists()) {
327 final ProcessResult rsyncResult = await globals.processManager.run(<Object>[
328 'rsync',
329 '-av',
330 '--filter',
331 '- .DS_Store',
332 '--filter',
333 '- native_assets.yaml',
334 '--filter',
335 '- native_assets.json',
336 nativeAssetsDirectory.path,
337 modeDirectory.path,
338 ]);
339 if (rsyncResult.exitCode != 0) {
340 throwToolExit('Failed to copy native assets:\n${rsyncResult.stderr}');
341 }
342 }
343
344 try {
345 // Delete the intermediaries since they would have been copied into our
346 // output frameworks.
347 if (iPhoneBuildOutput.existsSync()) {
348 iPhoneBuildOutput.deleteSync(recursive: true);
349 }
350 if (simulatorBuildOutput.existsSync()) {
351 simulatorBuildOutput.deleteSync(recursive: true);
352 }
353 } finally {
354 status.stop();
355 }
356 }
357
358 globals.printStatus('Frameworks written to ${outputDirectory.path}.');
359
360 if (!project.isModule && hasPlugins(project)) {
361 // Apps do not generate a FlutterPluginRegistrant.framework. Users will need
362 // to copy the GeneratedPluginRegistrant class to their project manually.
363 final File pluginRegistrantHeader = project.ios.pluginRegistrantHeader;
364 final File pluginRegistrantImplementation = project.ios.pluginRegistrantImplementation;
365 pluginRegistrantHeader.copySync(
366 outputDirectory.childFile(pluginRegistrantHeader.basename).path,
367 );
368 pluginRegistrantImplementation.copySync(
369 outputDirectory.childFile(pluginRegistrantImplementation.basename).path,
370 );
371 globals.printStatus(
372 '\nCopy the ${globals.fs.path.basenameWithoutExtension(pluginRegistrantHeader.path)} class into your project.\n'
373 'See https://flutter.dev/to/ios-create-flutter-engine for more information.',
374 );
375 }
376
377 if (buildInfos.any((BuildInfo info) => info.isDebug)) {
378 // Add-to-App must manually add the LLDB Init File to their native Xcode
379 // project, so provide the files and instructions.
380 final File lldbInitSourceFile = project.ios.lldbInitFile;
381 final File lldbInitTargetFile = outputDirectory.childFile(lldbInitSourceFile.basename);
382 final File lldbHelperPythonFile = project.ios.lldbHelperPythonFile;
383
384 if (!lldbInitTargetFile.existsSync()) {
385 // If LLDB is being added to the output, print a warning with instructions on how to add.
386 globals.printWarning(
387 'Debugging Flutter on new iOS versions requires an LLDB Init File. To '
388 'ensure debug mode works, please complete instructions found in '
389 '"Embed a Flutter module in your iOS app > Use frameworks > Set LLDB Init File" '
390 'section of https://docs.flutter.dev/to/ios-add-to-app-embed-setup.',
391 );
392 }
393 lldbInitSourceFile.copySync(lldbInitTargetFile.path);
394 lldbHelperPythonFile.copySync(outputDirectory.childFile(lldbHelperPythonFile.basename).path);
395 }
396
397 return FlutterCommandResult.success();
398 }
399
400 /// Create podspec that will download and unzip remote engine assets so host apps can leverage CocoaPods
401 /// vendored framework caching.
402 @visibleForTesting
403 void produceFlutterPodspec(BuildMode mode, Directory modeDirectory, {bool force = false}) {
404 final Status status = globals.logger.startProgress(' ├─Creating Flutter.podspec...');
405 try {
406 final GitTagVersion gitTagVersion = flutterVersion.gitTagVersion;
407 if (!force &&
408 (gitTagVersion.x == null ||
409 gitTagVersion.y == null ||
410 gitTagVersion.z == null ||
411 gitTagVersion.commits != 0)) {
412 throwToolExit(
413 '--cocoapods is only supported on the beta or stable channel. Detected version is ${flutterVersion.frameworkVersion}',
414 );
415 }
416
417 // Podspecs use semantic versioning, which don't support hotfixes.
418 // Fake out a semantic version with major.minor.(patch * 100) + hotfix.
419 // A real increasing version is required to prompt CocoaPods to fetch
420 // new artifacts when the source URL changes.
421 final int minorHotfixVersion = (gitTagVersion.z ?? 0) * 100 + (gitTagVersion.hotfix ?? 0);
422
423 final File license = cache.getLicenseFile();
424 if (!license.existsSync()) {
425 throwToolExit('Could not find license at ${license.path}');
426 }
427 final String licenseSource = license.readAsStringSync();
428 final String artifactsMode = FlutterDarwinPlatform.ios.artifactName(mode);
429
430 final podspecContents =
431 '''
432Pod::Spec.new do |s|
433 s.name = '${FlutterDarwinPlatform.ios.binaryName}'
434 s.version = '${gitTagVersion.x}.${gitTagVersion.y}.$minorHotfixVersion' # ${flutterVersion.frameworkVersion}
435 s.summary = 'A UI toolkit for beautiful and fast apps.'
436 s.description = <<-DESC
437Flutter is Google's UI toolkit for building beautiful, fast apps for mobile, web, desktop, and embedded devices from a single codebase.
438This pod vends the iOS Flutter engine framework. It is compatible with application frameworks created with this version of the engine and tools.
439The pod version matches Flutter version major.minor.(patch * 100) + hotfix.
440DESC
441 s.homepage = 'https://flutter.dev'
442 s.license = { :type => 'BSD', :text => <<-LICENSE
443$licenseSource
444LICENSE
445 }
446 s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
447 s.source = { :http => '${cache.storageBaseUrl}/flutter_infra_release/flutter/${cache.engineRevision}/$artifactsMode/${FlutterDarwinPlatform.ios.artifactZip}' }
448 s.documentation_url = 'https://docs.flutter.dev'
449 s.platform = :ios, '${FlutterDarwinPlatform.ios.deploymentTarget()}'
450 s.vendored_frameworks = '${FlutterDarwinPlatform.ios.xcframeworkName}'
451end
452''';
453
454 final File podspec = modeDirectory.childFile('Flutter.podspec')..createSync(recursive: true);
455 podspec.writeAsStringSync(podspecContents);
456 } finally {
457 status.stop();
458 }
459 }
460
461 Future<void> _produceFlutterFramework(BuildInfo buildInfo, Directory modeDirectory) async {
462 final Status status = globals.logger.startProgress(' ├─Copying Flutter.xcframework...');
463 final String engineCacheFlutterFrameworkDirectory = globals.artifacts!.getArtifactPath(
464 Artifact.flutterXcframework,
465 platform: TargetPlatform.ios,
466 mode: buildInfo.mode,
467 );
468 final String flutterFrameworkFileName = globals.fs.path.basename(
469 engineCacheFlutterFrameworkDirectory,
470 );
471 final Directory flutterFrameworkCopy = modeDirectory.childDirectory(flutterFrameworkFileName);
472
473 try {
474 // Copy xcframework engine cache framework to mode directory.
475 copyDirectory(
476 globals.fs.directory(engineCacheFlutterFrameworkDirectory),
477 flutterFrameworkCopy,
478 );
479 } finally {
480 status.stop();
481 }
482 }
483
484 Future<void> _produceAppFramework(
485 BuildInfo buildInfo,
486 Directory outputDirectory,
487 Directory iPhoneBuildOutput,
488 Directory simulatorBuildOutput,
489 ) async {
490 const appFrameworkName = 'App.framework';
491 final Status status = globals.logger.startProgress(' ├─Building App.xcframework...');
492 final frameworks = <Directory>[];
493
494 try {
495 for (final EnvironmentType sdkType in EnvironmentType.values) {
496 final Directory outputBuildDirectory = switch (sdkType) {
497 EnvironmentType.physical => iPhoneBuildOutput,
498 EnvironmentType.simulator => simulatorBuildOutput,
499 };
500 frameworks.add(outputBuildDirectory.childDirectory(appFrameworkName));
501 final environment = Environment(
502 projectDir: globals.fs.currentDirectory,
503 packageConfigPath: packageConfigPath(),
504 outputDir: outputBuildDirectory,
505 buildDir: project.dartTool.childDirectory('flutter_build'),
506 cacheDir: globals.cache.getRoot(),
507 flutterRootDir: globals.fs.directory(Cache.flutterRoot),
508 defines: <String, String>{
509 kTargetFile: targetFile,
510 kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios),
511 kIosArchs: defaultIOSArchsForEnvironment(
512 sdkType,
513 globals.artifacts!,
514 ).map((DarwinArch e) => e.name).join(' '),
515 kSdkRoot: await globals.xcode!.sdkLocation(sdkType),
516 ...buildInfo.toBuildSystemEnvironment(),
517 },
518 artifacts: globals.artifacts!,
519 fileSystem: globals.fs,
520 logger: globals.logger,
521 processManager: globals.processManager,
522 platform: globals.platform,
523 analytics: globals.analytics,
524 engineVersion: globals.artifacts!.usesLocalArtifacts
525 ? null
526 : globals.flutterVersion.engineRevision,
527 generateDartPluginRegistry: true,
528 );
529 Target target;
530 // Always build debug for simulator.
531 if (buildInfo.isDebug || sdkType == EnvironmentType.simulator) {
532 target = const DebugIosApplicationBundle();
533 } else if (buildInfo.isProfile) {
534 target = const ProfileIosApplicationBundle();
535 } else {
536 target = const ReleaseIosApplicationBundle();
537 }
538 final BuildResult result = await buildSystem.build(target, environment);
539 if (!result.success) {
540 for (final ExceptionMeasurement measurement in result.exceptions.values) {
541 globals.printError(measurement.exception.toString());
542 }
543 throwToolExit('The App.xcframework build failed.');
544 }
545 }
546 } finally {
547 status.stop();
548 }
549
550 await BuildFrameworkCommand.produceXCFramework(
551 frameworks,
552 'App',
553 outputDirectory,
554 globals.processManager,
555 );
556 }
557
558 Future<void> _producePlugins(
559 BuildMode mode,
560 String xcodeBuildConfiguration,
561 Directory iPhoneBuildOutput,
562 Directory simulatorBuildOutput,
563 Directory modeDirectory,
564 ) async {
565 final Status status = globals.logger.startProgress(' ├─Building plugins...');
566 try {
567 var pluginsBuildCommand = <String>[
568 ...globals.xcode!.xcrunCommand(),
569 'xcodebuild',
570 '-alltargets',
571 '-sdk',
572 XcodeSdk.IPhoneOS.platformName,
573 '-configuration',
574 xcodeBuildConfiguration,
575 'SYMROOT=${iPhoneBuildOutput.path}',
576 'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
577 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
578 if (boolArg('static')) 'MACH_O_TYPE=staticlib',
579 ];
580
581 RunResult buildPluginsResult = await globals.processUtils.run(
582 pluginsBuildCommand,
583 workingDirectory: project.ios.hostAppRoot.childDirectory('Pods').path,
584 );
585
586 if (buildPluginsResult.exitCode != 0) {
587 throwToolExit('Unable to build plugin frameworks: ${buildPluginsResult.stderr}');
588 }
589
590 // Always build debug for simulator.
591 final String simulatorConfiguration = BuildMode.debug.uppercaseName;
592 pluginsBuildCommand = <String>[
593 ...globals.xcode!.xcrunCommand(),
594 'xcodebuild',
595 '-alltargets',
596 '-sdk',
597 XcodeSdk.IPhoneSimulator.platformName,
598 '-configuration',
599 simulatorConfiguration,
600 'SYMROOT=${simulatorBuildOutput.path}',
601 'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
602 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
603 if (boolArg('static')) 'MACH_O_TYPE=staticlib',
604 ];
605
606 buildPluginsResult = await globals.processUtils.run(
607 pluginsBuildCommand,
608 workingDirectory: project.ios.hostAppRoot.childDirectory('Pods').path,
609 );
610
611 if (buildPluginsResult.exitCode != 0) {
612 throwToolExit(
613 'Unable to build plugin frameworks for simulator: ${buildPluginsResult.stderr}',
614 );
615 }
616
617 final Directory iPhoneBuildConfiguration = iPhoneBuildOutput.childDirectory(
618 '$xcodeBuildConfiguration-${XcodeSdk.IPhoneOS.platformName}',
619 );
620 final Directory simulatorBuildConfiguration = simulatorBuildOutput.childDirectory(
621 '$simulatorConfiguration-${XcodeSdk.IPhoneSimulator.platformName}',
622 );
623
624 final Iterable<Directory> products = iPhoneBuildConfiguration
625 .listSync(followLinks: false)
626 .whereType<Directory>();
627 for (final builtProduct in products) {
628 for (final FileSystemEntity podProduct in builtProduct.listSync(followLinks: false)) {
629 final String podFrameworkName = podProduct.basename;
630 if (globals.fs.path.extension(podFrameworkName) != '.framework') {
631 continue;
632 }
633 final String binaryName = globals.fs.path.basenameWithoutExtension(podFrameworkName);
634
635 final frameworks = <Directory>[
636 podProduct as Directory,
637 simulatorBuildConfiguration
638 .childDirectory(builtProduct.basename)
639 .childDirectory(podFrameworkName),
640 ];
641
642 await BuildFrameworkCommand.produceXCFramework(
643 frameworks,
644 binaryName,
645 modeDirectory,
646 globals.processManager,
647 );
648 }
649 }
650 } finally {
651 status.stop();
652 }
653 }
654}
655