| 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/logger.dart' show Logger; |
| 14 | import '../../base/process.dart'; |
| 15 | import '../../base/version.dart'; |
| 16 | import '../../build_info.dart'; |
| 17 | import '../../darwin/darwin.dart'; |
| 18 | import '../../devfs.dart'; |
| 19 | import '../../globals.dart' as globals; |
| 20 | import '../../ios/mac.dart'; |
| 21 | import '../../macos/xcode.dart'; |
| 22 | import '../../project.dart'; |
| 23 | import '../build_system.dart'; |
| 24 | import '../depfile.dart'; |
| 25 | import '../exceptions.dart'; |
| 26 | import '../tools/shader_compiler.dart'; |
| 27 | import 'assets.dart'; |
| 28 | import 'common.dart'; |
| 29 | import 'darwin.dart'; |
| 30 | import 'icon_tree_shaker.dart'; |
| 31 | import '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. |
| 37 | abstract 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. |
| 149 | class 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. |
| 178 | class 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. |
| 207 | class 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. |
| 244 | abstract 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. |
| 338 | class 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. |
| 349 | class 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. |
| 360 | class 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. |
| 373 | Future<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 |
| 415 | Future<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 |
| 430 | Future<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 | |
| 445 | void _printWarning(Logger logger, String path, int line, String warning) { |
| 446 | logger.printWarning(' $path: $line: warning: $warning' ); |
| 447 | } |
| 448 | |
| 449 | class _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. |
| 494 | class 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 |
| 604 | abstract 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. |
| 729 | class 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. |
| 760 | abstract 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. |
| 777 | class 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. |
| 788 | class 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. |
| 828 | Future<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 | |
| 883 | Future<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 | |