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:unified_analytics/unified_analytics.dart';
7
8import '../../artifacts.dart';
9import '../../base/build.dart';
10import '../../base/common.dart';
11import '../../base/file_system.dart';
12import '../../base/io.dart';
13import '../../base/logger.dart' show Logger;
14import '../../base/process.dart';
15import '../../base/version.dart';
16import '../../build_info.dart';
17import '../../darwin/darwin.dart';
18import '../../devfs.dart';
19import '../../globals.dart' as globals;
20import '../../ios/mac.dart';
21import '../../macos/xcode.dart';
22import '../../project.dart';
23import '../build_system.dart';
24import '../depfile.dart';
25import '../exceptions.dart';
26import '../tools/shader_compiler.dart';
27import 'assets.dart';
28import 'common.dart';
29import 'darwin.dart';
30import 'icon_tree_shaker.dart';
31import 'native_assets.dart';
32
33/// Supports compiling a dart kernel file to an assembly file.
34///
35/// If more than one iOS arch is provided, then this rule will
36/// produce a universal binary.
37abstract class AotAssemblyBase extends Target {
38 const AotAssemblyBase();
39
40 @override
41 String get analyticsName => 'ios_aot';
42
43 @override
44 Future<void> build(Environment environment) async {
45 final snapshotter = AOTSnapshotter(
46 fileSystem: environment.fileSystem,
47 logger: environment.logger,
48 xcode: globals.xcode!,
49 artifacts: environment.artifacts,
50 processManager: environment.processManager,
51 );
52 final String buildOutputPath = environment.buildDir.path;
53 final String? environmentBuildMode = environment.defines[kBuildMode];
54 if (environmentBuildMode == null) {
55 throw MissingDefineException(kBuildMode, 'aot_assembly');
56 }
57 final String? environmentTargetPlatform = environment.defines[kTargetPlatform];
58 if (environmentTargetPlatform == null) {
59 throw MissingDefineException(kTargetPlatform, 'aot_assembly');
60 }
61 final String? sdkRoot = environment.defines[kSdkRoot];
62 if (sdkRoot == null) {
63 throw MissingDefineException(kSdkRoot, 'aot_assembly');
64 }
65
66 final List<String> extraGenSnapshotOptions = decodeCommaSeparated(
67 environment.defines,
68 kExtraGenSnapshotOptions,
69 );
70 final buildMode = BuildMode.fromCliName(environmentBuildMode);
71 final TargetPlatform targetPlatform = getTargetPlatformForName(environmentTargetPlatform);
72 final String? splitDebugInfo = environment.defines[kSplitDebugInfo];
73 final dartObfuscation = environment.defines[kDartObfuscation] == 'true';
74 final List<DarwinArch> darwinArchs =
75 environment.defines[kIosArchs]?.split(' ').map(getIOSArchForName).toList() ??
76 <DarwinArch>[DarwinArch.arm64];
77 if (targetPlatform != TargetPlatform.ios) {
78 throw Exception('aot_assembly is only supported for iOS applications.');
79 }
80
81 final EnvironmentType? environmentType = environmentTypeFromSdkroot(
82 sdkRoot,
83 environment.fileSystem,
84 );
85 if (environmentType == EnvironmentType.simulator) {
86 throw Exception(
87 'release/profile builds are only supported for physical devices. '
88 'attempted to build for simulator.',
89 );
90 }
91 final String? codeSizeDirectory = environment.defines[kCodeSizeDirectory];
92
93 // If we're building multiple iOS archs the binaries need to be lipo'd
94 // together.
95 final pending = <Future<int>>[];
96 for (final darwinArch in darwinArchs) {
97 final archExtraGenSnapshotOptions = List<String>.of(extraGenSnapshotOptions);
98 if (codeSizeDirectory != null) {
99 final File codeSizeFile = environment.fileSystem
100 .directory(codeSizeDirectory)
101 .childFile('snapshot.${darwinArch.name}.json');
102 final File precompilerTraceFile = environment.fileSystem
103 .directory(codeSizeDirectory)
104 .childFile('trace.${darwinArch.name}.json');
105 archExtraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}');
106 archExtraGenSnapshotOptions.add('--trace-precompiler-to=${precompilerTraceFile.path}');
107 }
108 pending.add(
109 snapshotter.build(
110 platform: targetPlatform,
111 buildMode: buildMode,
112 mainPath: environment.buildDir.childFile('app.dill').path,
113 outputPath: environment.fileSystem.path.join(buildOutputPath, darwinArch.name),
114 darwinArch: darwinArch,
115 sdkRoot: sdkRoot,
116 quiet: true,
117 splitDebugInfo: splitDebugInfo,
118 dartObfuscation: dartObfuscation,
119 extraGenSnapshotOptions: archExtraGenSnapshotOptions,
120 ),
121 );
122 }
123 final List<int> results = await Future.wait(pending);
124 if (results.any((int result) => result != 0)) {
125 throw Exception('AOT snapshotter exited with code ${results.join()}');
126 }
127
128 // Combine the app lib into a fat framework.
129 await Lipo.create(
130 environment,
131 darwinArchs,
132 relativePath: 'App.framework/App',
133 inputDir: buildOutputPath,
134 );
135
136 // And combine the dSYM for each architecture too, if it was created.
137 await Lipo.create(
138 environment,
139 darwinArchs,
140 relativePath: 'App.framework.dSYM/Contents/Resources/DWARF/App',
141 inputDir: buildOutputPath,
142 // Don't fail if the dSYM wasn't created (i.e. during a debug build).
143 skipMissingInputs: true,
144 );
145 }
146}
147
148/// Generate an assembly target from a dart kernel file in release mode.
149class AotAssemblyRelease extends AotAssemblyBase {
150 const AotAssemblyRelease();
151
152 @override
153 String get name => 'aot_assembly_release';
154
155 @override
156 List<Source> get inputs => const <Source>[
157 Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
158 Source.pattern('{BUILD_DIR}/app.dill'),
159 Source.artifact(Artifact.engineDartBinary),
160 Source.artifact(Artifact.skyEnginePath),
161 // TODO(zanderso): cannot reference gen_snapshot with artifacts since
162 // it resolves to a file (ios/gen_snapshot) that never exists. This was
163 // split into gen_snapshot_arm64 and gen_snapshot_armv7.
164 // Source.artifact(Artifact.genSnapshot,
165 // platform: TargetPlatform.ios,
166 // mode: BuildMode.release,
167 // ),
168 ];
169
170 @override
171 List<Source> get outputs => const <Source>[Source.pattern('{OUTPUT_DIR}/App.framework/App')];
172
173 @override
174 List<Target> get dependencies => const <Target>[ReleaseUnpackIOS(), KernelSnapshot()];
175}
176
177/// Generate an assembly target from a dart kernel file in profile mode.
178class AotAssemblyProfile extends AotAssemblyBase {
179 const AotAssemblyProfile();
180
181 @override
182 String get name => 'aot_assembly_profile';
183
184 @override
185 List<Source> get inputs => const <Source>[
186 Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
187 Source.pattern('{BUILD_DIR}/app.dill'),
188 Source.artifact(Artifact.engineDartBinary),
189 Source.artifact(Artifact.skyEnginePath),
190 // TODO(zanderso): cannot reference gen_snapshot with artifacts since
191 // it resolves to a file (ios/gen_snapshot) that never exists. This was
192 // split into gen_snapshot_arm64 and gen_snapshot_armv7.
193 // Source.artifact(Artifact.genSnapshot,
194 // platform: TargetPlatform.ios,
195 // mode: BuildMode.profile,
196 // ),
197 ];
198
199 @override
200 List<Source> get outputs => const <Source>[Source.pattern('{OUTPUT_DIR}/App.framework/App')];
201
202 @override
203 List<Target> get dependencies => const <Target>[ProfileUnpackIOS(), KernelSnapshot()];
204}
205
206/// Create a trivial App.framework file for debug iOS builds.
207class DebugUniversalFramework extends Target {
208 const DebugUniversalFramework();
209
210 @override
211 String get name => 'debug_universal_framework';
212
213 @override
214 List<Target> get dependencies => const <Target>[DebugUnpackIOS(), KernelSnapshot()];
215
216 @override
217 List<Source> get inputs => const <Source>[
218 Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
219 ];
220
221 @override
222 List<Source> get outputs => const <Source>[Source.pattern('{BUILD_DIR}/App.framework/App')];
223
224 @override
225 Future<void> build(Environment environment) async {
226 final String? sdkRoot = environment.defines[kSdkRoot];
227 if (sdkRoot == null) {
228 throw MissingDefineException(kSdkRoot, name);
229 }
230
231 // Generate a trivial App.framework.
232 final Set<String>? iosArchNames = environment.defines[kIosArchs]?.split(' ').toSet();
233 final File output = environment.buildDir.childDirectory('App.framework').childFile('App');
234 environment.buildDir.createSync(recursive: true);
235 await _createStubAppFramework(output, environment, iosArchNames, sdkRoot);
236 }
237}
238
239/// Copy the iOS framework to the correct copy dir by invoking 'rsync'.
240///
241/// This class is abstract to share logic between the three concrete
242/// implementations. The shelling out is done to avoid complications with
243/// preserving special files (e.g., symbolic links) in the framework structure.
244abstract class UnpackIOS extends UnpackDarwin {
245 const UnpackIOS();
246
247 @override
248 List<Source> get inputs => <Source>[
249 const Source.pattern(
250 '{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart',
251 ),
252 Source.artifact(Artifact.flutterXcframework, platform: TargetPlatform.ios, mode: buildMode),
253 ];
254
255 @override
256 List<Source> get outputs => const <Source>[
257 Source.pattern('{OUTPUT_DIR}/Flutter.framework/Flutter'),
258 ];
259
260 @override
261 List<Target> get dependencies => <Target>[];
262
263 @visibleForOverriding
264 BuildMode get buildMode;
265
266 @override
267 Future<void> build(Environment environment) async {
268 final String? sdkRoot = environment.defines[kSdkRoot];
269 if (sdkRoot == null) {
270 throw MissingDefineException(kSdkRoot, name);
271 }
272 final String? archs = environment.defines[kIosArchs];
273 if (archs == null) {
274 throw MissingDefineException(kIosArchs, name);
275 }
276
277 // Copy Flutter framework.
278 final EnvironmentType? environmentType = environmentTypeFromSdkroot(
279 sdkRoot,
280 environment.fileSystem,
281 );
282 await copyFramework(
283 environment,
284 environmentType: environmentType,
285 framework: Artifact.flutterFramework,
286 targetPlatform: TargetPlatform.ios,
287 buildMode: buildMode,
288 );
289 await _copyFrameworkDysm(environment, sdkRoot: sdkRoot, environmentType: environmentType);
290
291 final File frameworkBinary = environment.outputDir
292 .childDirectory(FlutterDarwinPlatform.ios.frameworkName)
293 .childFile(FlutterDarwinPlatform.ios.binaryName);
294 final String frameworkBinaryPath = frameworkBinary.path;
295 if (!await frameworkBinary.exists()) {
296 throw Exception('Binary $frameworkBinaryPath does not exist, cannot thin');
297 }
298 await thinFramework(environment, frameworkBinaryPath, archs);
299 await _signFramework(environment, frameworkBinary, buildMode);
300 }
301
302 Future<void> _copyFrameworkDysm(
303 Environment environment, {
304 required String sdkRoot,
305 EnvironmentType? environmentType,
306 }) async {
307 // Copy Flutter framework dSYM (debug symbol) bundle, if present.
308 final Directory frameworkDsym = environment.fileSystem.directory(
309 environment.artifacts.getArtifactPath(
310 Artifact.flutterFrameworkDsym,
311 platform: TargetPlatform.ios,
312 mode: buildMode,
313 environmentType: environmentType,
314 ),
315 );
316 if (frameworkDsym.existsSync()) {
317 final ProcessResult result = await environment.processManager.run(<String>[
318 'rsync',
319 '-av',
320 '--delete',
321 '--filter',
322 '- .DS_Store/',
323 '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r',
324 frameworkDsym.path,
325 environment.outputDir.path,
326 ]);
327 if (result.exitCode != 0) {
328 throw Exception(
329 'Failed to copy framework dSYM (exit ${result.exitCode}:\n'
330 '${result.stdout}\n---\n${result.stderr}',
331 );
332 }
333 }
334 }
335}
336
337/// Unpack the release prebuilt engine framework.
338class ReleaseUnpackIOS extends UnpackIOS {
339 const ReleaseUnpackIOS();
340
341 @override
342 String get name => 'release_unpack_ios';
343
344 @override
345 BuildMode get buildMode => BuildMode.release;
346}
347
348/// Unpack the profile prebuilt engine framework.
349class ProfileUnpackIOS extends UnpackIOS {
350 const ProfileUnpackIOS();
351
352 @override
353 String get name => 'profile_unpack_ios';
354
355 @override
356 BuildMode get buildMode => BuildMode.profile;
357}
358
359/// Unpack the debug prebuilt engine framework.
360class DebugUnpackIOS extends UnpackIOS {
361 const DebugUnpackIOS();
362
363 @override
364 String get name => 'debug_unpack_ios';
365
366 @override
367 BuildMode get buildMode => BuildMode.debug;
368}
369
370// TODO(gaaclarke): Remove this after a reasonable amount of time where the
371// UISceneDelegate migration being on stable. This incurs a minor build time
372// cost.
373Future<void> _checkForLaunchRootViewControllerAccessDeprecation(
374 Logger logger,
375 File file,
376 Pattern usage,
377 Pattern terminator,
378) async {
379 final List<String> lines = file.readAsLinesSync();
380
381 var inDidFinishLaunchingWithOptions = false;
382 var lineNumber = 0;
383 for (final line in lines) {
384 lineNumber += 1;
385 if (!inDidFinishLaunchingWithOptions) {
386 if (line.contains('didFinishLaunchingWithOptions')) {
387 inDidFinishLaunchingWithOptions = true;
388 }
389 } else {
390 if (line.startsWith(terminator)) {
391 inDidFinishLaunchingWithOptions = false;
392 } else if (line.contains(usage)) {
393 _printWarning(
394 logger,
395 file.path,
396 lineNumber,
397 // TODO(gaaclarke): Add a link to the migration guide when it's written.
398 'Flutter deprecation: Accessing rootViewController in `application:didFinishLaunchingWithOptions:` [flutter-launch-rootvc].\n'
399 '\tnote: \n' // The space after `note:` is meaningful, it is required associate the note with the warning in Xcode.
400 '\tAfter the UISceneDelegate migration the `UIApplicationDelegate.window` and '
401 '`UIWindow.rootViewController` properties will not be set in '
402 '`application:didFinishLaunchingWithOptions:`. If you are relying on that '
403 'in order to register platform channels at application launch use the '
404 '`FlutterPluginRegistry` API instead. Other setup can be moved to a '
405 'FlutterViewController subclass (ex: `awakeFromNib`).',
406 );
407 }
408 }
409 }
410}
411
412/// Checks [file] representing objc code for deprecated usage of the
413/// rootViewController and writes it to [logger].
414@visibleForTesting
415Future<void> checkForLaunchRootViewControllerAccessDeprecationObjc(Logger logger, File file) async {
416 try {
417 await _checkForLaunchRootViewControllerAccessDeprecation(
418 logger,
419 file,
420 RegExp('self.*?window.*?rootViewController'),
421 RegExp('^}'),
422 );
423 // ignore: avoid_catches_without_on_clauses
424 } catch (_) {}
425}
426
427/// Checks [file] representing swift code for deprecated usage of the
428/// rootViewController and writes it to [logger].
429@visibleForTesting
430Future<void> checkForLaunchRootViewControllerAccessDeprecationSwift(
431 Logger logger,
432 File file,
433) async {
434 try {
435 await _checkForLaunchRootViewControllerAccessDeprecation(
436 logger,
437 file,
438 'window?.rootViewController',
439 RegExp(r'^.*?func\s*?\S*?\('),
440 );
441 // ignore: avoid_catches_without_on_clauses
442 } catch (_) {}
443}
444
445void _printWarning(Logger logger, String path, int line, String warning) {
446 logger.printWarning('$path:$line: warning: $warning');
447}
448
449class _IssueLaunchRootViewControllerAccess extends Target {
450 const _IssueLaunchRootViewControllerAccess();
451
452 @override
453 Future<void> build(Environment environment) async {
454 final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
455 if (flutterProject.ios.appDelegateSwift.existsSync()) {
456 await checkForLaunchRootViewControllerAccessDeprecationSwift(
457 environment.logger,
458 flutterProject.ios.appDelegateSwift,
459 );
460 }
461 if (flutterProject.ios.appDelegateObjc.existsSync()) {
462 await checkForLaunchRootViewControllerAccessDeprecationObjc(
463 environment.logger,
464 flutterProject.ios.appDelegateObjc,
465 );
466 }
467 }
468
469 @override
470 List<Target> get dependencies => <Target>[];
471
472 @override
473 List<Source> get inputs {
474 return <Source>[
475 const Source.pattern(
476 '{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart',
477 ),
478 Source.fromProject((FlutterProject project) => project.ios.appDelegateObjc, optional: true),
479 Source.fromProject((FlutterProject project) => project.ios.appDelegateSwift, optional: true),
480 ];
481 }
482
483 @override
484 String get name => 'IssueLaunchRootViewControllerAccess';
485
486 @override
487 List<Source> get outputs => <Source>[];
488}
489
490/// This target verifies that the Xcode project has an LLDB Init File set within
491/// at least one scheme.
492///
493/// LLDB Init File is needed for debugging on physical iOS 26+ devices.
494class DebugIosLLDBInit extends Target {
495 const DebugIosLLDBInit();
496
497 @override
498 String get name => 'debug_ios_lldb_init';
499
500 @override
501 List<Source> get inputs => const <Source>[
502 Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
503 Source.pattern(
504 '{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/darwin.dart',
505 ),
506 ];
507
508 @override
509 List<Source> get outputs => <Source>[
510 Source.fromProject((FlutterProject project) => project.ios.lldbInitFile),
511 ];
512
513 @override
514 List<Target> get dependencies => <Target>[];
515
516 @override
517 Future<void> build(Environment environment) async {
518 final String? sdkRoot = environment.defines[kSdkRoot];
519 if (sdkRoot == null) {
520 throw MissingDefineException(kSdkRoot, name);
521 }
522 final EnvironmentType? environmentType = environmentTypeFromSdkroot(
523 sdkRoot,
524 environment.fileSystem,
525 );
526
527 // LLDB Init File is only required for physical devices in debug mode.
528 if (environmentType != EnvironmentType.physical) {
529 return;
530 }
531
532 final String? targetDeviceVersionString = environment.defines[kTargetDeviceOSVersion];
533 if (targetDeviceVersionString == null) {
534 // Skip if TARGET_DEVICE_OS_VERSION is not found. TARGET_DEVICE_OS_VERSION
535 // is not set if "build ios-framework" is called, which builds the
536 // DebugIosApplicationBundle directly rather than through flutter assemble.
537 // If may also be null if the build is targeting multiple architectures.
538 return;
539 }
540
541 final Version? targetDeviceVersion = Version.parse(targetDeviceVersionString);
542 if (targetDeviceVersion == null) {
543 environment.logger.printError(
544 'Failed to parse TARGET_DEVICE_OS_VERSION: $targetDeviceVersionString',
545 );
546 return;
547 }
548
549 // LLDB Init File is only needed for iOS 26+.
550 if (targetDeviceVersion < Version(26, 0, null)) {
551 return;
552 }
553
554 final String? srcRoot = environment.defines[kSrcRoot];
555 if (srcRoot == null) {
556 environment.logger.printError('Failed to find $srcRoot');
557 return;
558 }
559
560 final Directory xcodeProjectDir = environment.fileSystem.directory(srcRoot);
561 if (!xcodeProjectDir.existsSync()) {
562 environment.logger.printError('Failed to find ${xcodeProjectDir.path}');
563 return;
564 }
565
566 // The scheme name is not available in Xcode Build Phases Run Scripts.
567 // Instead, find all xcscheme files in the Xcode project (this may be the
568 // Flutter Xcode project or an Add to App native Xcode project) and check
569 // if any of them contain "customLLDBInitFile". If none have it set, print
570 // a warning.
571 // Also, this cannot check for a specific path/name for the LLDB Init File
572 // since Flutter's LLDB Init file may be imported from within a user's
573 // custom LLDB Init File.
574 var anyLLDBInitFound = false;
575 await for (final FileSystemEntity entity in xcodeProjectDir.list(recursive: true)) {
576 if (environment.fileSystem.path.extension(entity.path) == '.xcscheme' && entity is File) {
577 if (entity.readAsStringSync().contains('customLLDBInitFile')) {
578 anyLLDBInitFound = true;
579 break;
580 }
581 }
582 }
583 if (!anyLLDBInitFound) {
584 final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
585 final tab = flutterProject.isModule ? 'Use CocoaPods' : 'Use frameworks';
586 printXcodeWarning(
587 'Debugging Flutter on new iOS versions requires an LLDB Init File. To '
588 'ensure debug mode works, please complete instructions found in '
589 '"Embed a Flutter module in your iOS app > $tab > Set LLDB Init File" '
590 'section of https://docs.flutter.dev/to/ios-add-to-app-embed-setup.',
591 );
592 }
593 return;
594 }
595}
596
597/// The base class for all iOS bundle targets.
598///
599/// This is responsible for setting up the basic App.framework structure, including:
600/// * Copying the app.dill/kernel_blob.bin from the build directory to assets (debug)
601/// * Copying the precompiled isolate/vm data from the engine (debug)
602/// * Copying the flutter assets to App.framework/flutter_assets
603/// * Copying either the stub or real App assembly file to App.framework/App
604abstract class IosAssetBundle extends Target {
605 const IosAssetBundle();
606
607 @override
608 List<Target> get dependencies => const <Target>[
609 KernelSnapshot(),
610 InstallCodeAssets(),
611 _IssueLaunchRootViewControllerAccess(),
612 ];
613
614 @override
615 List<Source> get inputs => const <Source>[
616 Source.pattern('{BUILD_DIR}/App.framework/App'),
617 Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
618 ...IconTreeShaker.inputs,
619 ...ShaderCompiler.inputs,
620 ];
621
622 @override
623 List<Source> get outputs => const <Source>[
624 Source.pattern('{OUTPUT_DIR}/App.framework/App'),
625 Source.pattern('{OUTPUT_DIR}/App.framework/Info.plist'),
626 ];
627
628 @override
629 List<String> get depfiles => <String>['flutter_assets.d'];
630
631 @override
632 Future<void> build(Environment environment) async {
633 final String? environmentBuildMode = environment.defines[kBuildMode];
634 if (environmentBuildMode == null) {
635 throw MissingDefineException(kBuildMode, name);
636 }
637 final buildMode = BuildMode.fromCliName(environmentBuildMode);
638 final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework');
639 final File frameworkBinary = frameworkDirectory.childFile('App');
640 final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets');
641 frameworkDirectory.createSync(recursive: true);
642 assetDirectory.createSync();
643
644 // Only copy the prebuilt runtimes and kernel blob in debug mode.
645 if (buildMode == BuildMode.debug) {
646 // Copy the App.framework to the output directory.
647 environment.buildDir
648 .childDirectory('App.framework')
649 .childFile('App')
650 .copySync(frameworkBinary.path);
651
652 final String vmSnapshotData = environment.artifacts.getArtifactPath(
653 Artifact.vmSnapshotData,
654 mode: BuildMode.debug,
655 );
656 final String isolateSnapshotData = environment.artifacts.getArtifactPath(
657 Artifact.isolateSnapshotData,
658 mode: BuildMode.debug,
659 );
660 environment.buildDir
661 .childFile('app.dill')
662 .copySync(assetDirectory.childFile('kernel_blob.bin').path);
663 environment.fileSystem
664 .file(vmSnapshotData)
665 .copySync(assetDirectory.childFile('vm_snapshot_data').path);
666 environment.fileSystem
667 .file(isolateSnapshotData)
668 .copySync(assetDirectory.childFile('isolate_snapshot_data').path);
669 } else {
670 environment.buildDir
671 .childDirectory('App.framework')
672 .childFile('App')
673 .copySync(frameworkBinary.path);
674 }
675
676 // Copy the dSYM
677 if (environment.buildDir.childDirectory('App.framework.dSYM').existsSync()) {
678 final File dsymOutputBinary = environment.outputDir
679 .childDirectory('App.framework.dSYM')
680 .childDirectory('Contents')
681 .childDirectory('Resources')
682 .childDirectory('DWARF')
683 .childFile('App');
684 dsymOutputBinary.parent.createSync(recursive: true);
685 environment.buildDir
686 .childDirectory('App.framework.dSYM')
687 .childDirectory('Contents')
688 .childDirectory('Resources')
689 .childDirectory('DWARF')
690 .childFile('App')
691 .copySync(dsymOutputBinary.path);
692 }
693
694 final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
695 final String? flavor = await flutterProject.ios.parseFlavorFromConfiguration(environment);
696
697 // Copy the assets.
698 final Depfile assetDepfile = await copyAssets(
699 environment,
700 assetDirectory,
701 targetPlatform: TargetPlatform.ios,
702 buildMode: buildMode,
703 additionalInputs: <File>[
704 flutterProject.ios.infoPlist,
705 flutterProject.ios.appFrameworkInfoPlist,
706 ],
707 additionalContent: <String, DevFSContent>{
708 'NativeAssetsManifest.json': DevFSFileContent(
709 environment.buildDir.childFile('native_assets.json'),
710 ),
711 },
712 flavor: flavor,
713 );
714 environment.depFileService.writeToFile(
715 assetDepfile,
716 environment.buildDir.childFile('flutter_assets.d'),
717 );
718
719 // Copy the plist from either the project or module.
720 flutterProject.ios.appFrameworkInfoPlist.copySync(
721 environment.outputDir.childDirectory('App.framework').childFile('Info.plist').path,
722 );
723
724 await _signFramework(environment, frameworkBinary, buildMode);
725 }
726}
727
728/// Build a debug iOS application bundle.
729class DebugIosApplicationBundle extends IosAssetBundle {
730 const DebugIosApplicationBundle();
731
732 @override
733 String get name => 'debug_ios_bundle_flutter_assets';
734
735 @override
736 List<Source> get inputs => <Source>[
737 const Source.artifact(Artifact.vmSnapshotData, mode: BuildMode.debug),
738 const Source.artifact(Artifact.isolateSnapshotData, mode: BuildMode.debug),
739 const Source.pattern('{BUILD_DIR}/app.dill'),
740 ...super.inputs,
741 ];
742
743 @override
744 List<Source> get outputs => <Source>[
745 const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/vm_snapshot_data'),
746 const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/isolate_snapshot_data'),
747 const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/kernel_blob.bin'),
748 ...super.outputs,
749 ];
750
751 @override
752 List<Target> get dependencies => <Target>[
753 const DebugUniversalFramework(),
754 const DebugIosLLDBInit(),
755 ...super.dependencies,
756 ];
757}
758
759/// IosAssetBundle with debug symbols, used for Profile and Release builds.
760abstract class _IosAssetBundleWithDSYM extends IosAssetBundle {
761 const _IosAssetBundleWithDSYM();
762
763 @override
764 List<Source> get inputs => <Source>[
765 ...super.inputs,
766 const Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
767 ];
768
769 @override
770 List<Source> get outputs => <Source>[
771 ...super.outputs,
772 const Source.pattern('{OUTPUT_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
773 ];
774}
775
776/// Build a profile iOS application bundle.
777class ProfileIosApplicationBundle extends _IosAssetBundleWithDSYM {
778 const ProfileIosApplicationBundle();
779
780 @override
781 String get name => 'profile_ios_bundle_flutter_assets';
782
783 @override
784 List<Target> get dependencies => const <Target>[AotAssemblyProfile(), InstallCodeAssets()];
785}
786
787/// Build a release iOS application bundle.
788class ReleaseIosApplicationBundle extends _IosAssetBundleWithDSYM {
789 const ReleaseIosApplicationBundle();
790
791 @override
792 String get name => 'release_ios_bundle_flutter_assets';
793
794 @override
795 List<Target> get dependencies => const <Target>[AotAssemblyRelease(), InstallCodeAssets()];
796
797 @override
798 Future<void> build(Environment environment) async {
799 var buildSuccess = true;
800 try {
801 await super.build(environment);
802 } catch (_) {
803 buildSuccess = false;
804 rethrow;
805 } finally {
806 // Send a usage event when the app is being archived.
807 // Since assemble is run during a `flutter build`/`run` as well as an out-of-band
808 // archive command from Xcode, this is a more accurate count than `flutter build ipa` alone.
809 if (environment.defines[kXcodeAction]?.toLowerCase() == 'install') {
810 environment.logger.printTrace('Sending archive event if usage enabled.');
811 environment.analytics.send(
812 Event.appleUsageEvent(
813 workflow: 'assemble',
814 parameter: 'ios-archive',
815 result: buildSuccess ? 'success' : 'fail',
816 ),
817 );
818 }
819 }
820 }
821}
822
823/// Create an App.framework for debug iOS targets.
824///
825/// This framework needs to exist for the Xcode project to link/bundle,
826/// but it isn't actually executed. To generate something valid, we compile a trivial
827/// constant.
828Future<void> _createStubAppFramework(
829 File outputFile,
830 Environment environment,
831 Set<String>? iosArchNames,
832 String sdkRoot,
833) async {
834 try {
835 outputFile.createSync(recursive: true);
836 } on Exception catch (e) {
837 throwToolExit('Failed to create App.framework stub at ${outputFile.path}: $e');
838 }
839
840 final FileSystem fileSystem = environment.fileSystem;
841 final Directory tempDir = fileSystem.systemTempDirectory.createTempSync(
842 'flutter_tools_stub_source.',
843 );
844 try {
845 final File stubSource = tempDir.childFile('debug_app.cc')
846 ..writeAsStringSync(r'''
847 static const int Moo = 88;
848 ''');
849
850 final EnvironmentType? environmentType = environmentTypeFromSdkroot(sdkRoot, fileSystem);
851
852 await globals.xcode!.clang(<String>[
853 '-x',
854 'c',
855 for (final String arch in iosArchNames ?? <String>{}) ...<String>['-arch', arch],
856 stubSource.path,
857 '-dynamiclib',
858 // Keep version in sync with AOTSnapshotter flag
859 if (environmentType == EnvironmentType.physical)
860 '-miphoneos-version-min=${FlutterDarwinPlatform.ios.deploymentTarget()}'
861 else
862 '-miphonesimulator-version-min=${FlutterDarwinPlatform.ios.deploymentTarget()}',
863 '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
864 '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
865 '-fapplication-extension',
866 '-install_name', '@rpath/App.framework/App',
867 '-isysroot', sdkRoot,
868 '-o', outputFile.path,
869 ]);
870 } finally {
871 try {
872 tempDir.deleteSync(recursive: true);
873 } on FileSystemException {
874 // Best effort. Sometimes we can't delete things from system temp.
875 } on Exception catch (e) {
876 throwToolExit('Failed to create App.framework stub at ${outputFile.path}: $e');
877 }
878 }
879
880 await _signFramework(environment, outputFile, BuildMode.debug);
881}
882
883Future<void> _signFramework(Environment environment, File binary, BuildMode buildMode) async {
884 await removeFinderExtendedAttributes(
885 binary,
886 ProcessUtils(processManager: environment.processManager, logger: environment.logger),
887 environment.logger,
888 );
889
890 String? codesignIdentity = environment.defines[kCodesignIdentity];
891 if (codesignIdentity == null || codesignIdentity.isEmpty) {
892 codesignIdentity = '-';
893 }
894 final ProcessResult result = environment.processManager.runSync(<String>[
895 'codesign',
896 '--force',
897 '--sign',
898 codesignIdentity,
899 if (buildMode != BuildMode.release) ...<String>[
900 // Mimic Xcode's timestamp codesigning behavior on non-release binaries.
901 '--timestamp=none',
902 ],
903 binary.path,
904 ]);
905 if (result.exitCode != 0) {
906 final String stdout = (result.stdout as String).trim();
907 final String stderr = (result.stderr as String).trim();
908 final output = StringBuffer();
909 output.writeln('Failed to codesign ${binary.path} with identity $codesignIdentity.');
910 if (stdout.isNotEmpty) {
911 output.writeln(stdout);
912 }
913 if (stderr.isNotEmpty) {
914 output.writeln(stderr);
915 }
916 throw Exception(output.toString());
917 }
918}
919