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