Victoria Ashworth eb07c51230
Add lldb init file (#164344)
Adds an .lldbinit file to iOS app xcscheme.

Adding to scheme files can be error prone since a developer may be using
custom schemes (flavors). If we can't add it to the scheme, we print an
error without failing.

Since it is part of the scheme, it will be added to the project and will
be used on every run regardless of the device type/version. The Dart
side handles limiting to specific devices. If needed, we can alter the
.lldbinit file during `flutter assemble` to rewrite it since it doesn't
read the file until launch time (therefore it can be changed during
build time).

During `flutter assemble`, if the project doesn't have an LLDB Init File
set for any schemes, it'll throw an error if running in debug mode with
an iOS 18.4+ device.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-03-06 21:21:52 +00:00

869 lines
29 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../../artifacts.dart';
import '../../base/build.dart';
import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../base/process.dart';
import '../../base/version.dart';
import '../../build_info.dart';
import '../../devfs.dart';
import '../../globals.dart' as globals;
import '../../ios/mac.dart';
import '../../macos/xcode.dart';
import '../../project.dart';
import '../build_system.dart';
import '../depfile.dart';
import '../exceptions.dart';
import '../tools/shader_compiler.dart';
import 'assets.dart';
import 'common.dart';
import 'icon_tree_shaker.dart';
import 'native_assets.dart';
/// Supports compiling a dart kernel file to an assembly file.
///
/// If more than one iOS arch is provided, then this rule will
/// produce a universal binary.
abstract class AotAssemblyBase extends Target {
const AotAssemblyBase();
@override
String get analyticsName => 'ios_aot';
@override
Future<void> build(Environment environment) async {
final AOTSnapshotter snapshotter = AOTSnapshotter(
fileSystem: environment.fileSystem,
logger: environment.logger,
xcode: globals.xcode!,
artifacts: environment.artifacts,
processManager: environment.processManager,
);
final String buildOutputPath = environment.buildDir.path;
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, 'aot_assembly');
}
final String? environmentTargetPlatform = environment.defines[kTargetPlatform];
if (environmentTargetPlatform == null) {
throw MissingDefineException(kTargetPlatform, 'aot_assembly');
}
final String? sdkRoot = environment.defines[kSdkRoot];
if (sdkRoot == null) {
throw MissingDefineException(kSdkRoot, 'aot_assembly');
}
final List<String> extraGenSnapshotOptions = decodeCommaSeparated(
environment.defines,
kExtraGenSnapshotOptions,
);
final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode);
final TargetPlatform targetPlatform = getTargetPlatformForName(environmentTargetPlatform);
final String? splitDebugInfo = environment.defines[kSplitDebugInfo];
final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true';
final List<DarwinArch> darwinArchs =
environment.defines[kIosArchs]?.split(' ').map(getIOSArchForName).toList() ??
<DarwinArch>[DarwinArch.arm64];
if (targetPlatform != TargetPlatform.ios) {
throw Exception('aot_assembly is only supported for iOS applications.');
}
final EnvironmentType? environmentType = environmentTypeFromSdkroot(
sdkRoot,
environment.fileSystem,
);
if (environmentType == EnvironmentType.simulator) {
throw Exception(
'release/profile builds are only supported for physical devices. '
'attempted to build for simulator.',
);
}
final String? codeSizeDirectory = environment.defines[kCodeSizeDirectory];
// If we're building multiple iOS archs the binaries need to be lipo'd
// together.
final List<Future<int>> pending = <Future<int>>[];
for (final DarwinArch darwinArch in darwinArchs) {
final List<String> archExtraGenSnapshotOptions = List<String>.of(extraGenSnapshotOptions);
if (codeSizeDirectory != null) {
final File codeSizeFile = environment.fileSystem
.directory(codeSizeDirectory)
.childFile('snapshot.${darwinArch.name}.json');
final File precompilerTraceFile = environment.fileSystem
.directory(codeSizeDirectory)
.childFile('trace.${darwinArch.name}.json');
archExtraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}');
archExtraGenSnapshotOptions.add('--trace-precompiler-to=${precompilerTraceFile.path}');
}
pending.add(
snapshotter.build(
platform: targetPlatform,
buildMode: buildMode,
mainPath: environment.buildDir.childFile('app.dill').path,
outputPath: environment.fileSystem.path.join(buildOutputPath, darwinArch.name),
darwinArch: darwinArch,
sdkRoot: sdkRoot,
quiet: true,
splitDebugInfo: splitDebugInfo,
dartObfuscation: dartObfuscation,
extraGenSnapshotOptions: archExtraGenSnapshotOptions,
),
);
}
final List<int> results = await Future.wait(pending);
if (results.any((int result) => result != 0)) {
throw Exception('AOT snapshotter exited with code ${results.join()}');
}
// Combine the app lib into a fat framework.
await Lipo.create(
environment,
darwinArchs,
relativePath: 'App.framework/App',
inputDir: buildOutputPath,
);
// And combine the dSYM for each architecture too, if it was created.
await Lipo.create(
environment,
darwinArchs,
relativePath: 'App.framework.dSYM/Contents/Resources/DWARF/App',
inputDir: buildOutputPath,
// Don't fail if the dSYM wasn't created (i.e. during a debug build).
skipMissingInputs: true,
);
}
}
/// Generate an assembly target from a dart kernel file in release mode.
class AotAssemblyRelease extends AotAssemblyBase {
const AotAssemblyRelease();
@override
String get name => 'aot_assembly_release';
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
Source.pattern('{BUILD_DIR}/app.dill'),
Source.artifact(Artifact.engineDartBinary),
Source.artifact(Artifact.skyEnginePath),
// TODO(zanderso): cannot reference gen_snapshot with artifacts since
// it resolves to a file (ios/gen_snapshot) that never exists. This was
// split into gen_snapshot_arm64 and gen_snapshot_armv7.
// Source.artifact(Artifact.genSnapshot,
// platform: TargetPlatform.ios,
// mode: BuildMode.release,
// ),
];
@override
List<Source> get outputs => const <Source>[Source.pattern('{OUTPUT_DIR}/App.framework/App')];
@override
List<Target> get dependencies => const <Target>[ReleaseUnpackIOS(), KernelSnapshot()];
}
/// Generate an assembly target from a dart kernel file in profile mode.
class AotAssemblyProfile extends AotAssemblyBase {
const AotAssemblyProfile();
@override
String get name => 'aot_assembly_profile';
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
Source.pattern('{BUILD_DIR}/app.dill'),
Source.artifact(Artifact.engineDartBinary),
Source.artifact(Artifact.skyEnginePath),
// TODO(zanderso): cannot reference gen_snapshot with artifacts since
// it resolves to a file (ios/gen_snapshot) that never exists. This was
// split into gen_snapshot_arm64 and gen_snapshot_armv7.
// Source.artifact(Artifact.genSnapshot,
// platform: TargetPlatform.ios,
// mode: BuildMode.profile,
// ),
];
@override
List<Source> get outputs => const <Source>[Source.pattern('{OUTPUT_DIR}/App.framework/App')];
@override
List<Target> get dependencies => const <Target>[ProfileUnpackIOS(), KernelSnapshot()];
}
/// Create a trivial App.framework file for debug iOS builds.
class DebugUniversalFramework extends Target {
const DebugUniversalFramework();
@override
String get name => 'debug_universal_framework';
@override
List<Target> get dependencies => const <Target>[DebugUnpackIOS(), KernelSnapshot()];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
];
@override
List<Source> get outputs => const <Source>[Source.pattern('{BUILD_DIR}/App.framework/App')];
@override
Future<void> build(Environment environment) async {
final String? sdkRoot = environment.defines[kSdkRoot];
if (sdkRoot == null) {
throw MissingDefineException(kSdkRoot, name);
}
// Generate a trivial App.framework.
final Set<String>? iosArchNames = environment.defines[kIosArchs]?.split(' ').toSet();
final File output = environment.buildDir.childDirectory('App.framework').childFile('App');
environment.buildDir.createSync(recursive: true);
await _createStubAppFramework(output, environment, iosArchNames, sdkRoot);
}
}
/// Copy the iOS framework to the correct copy dir by invoking 'rsync'.
///
/// This class is abstract to share logic between the three concrete
/// implementations. The shelling out is done to avoid complications with
/// preserving special files (e.g., symbolic links) in the framework structure.
abstract class UnpackIOS extends Target {
const UnpackIOS();
@override
List<Source> get inputs => <Source>[
const Source.pattern(
'{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart',
),
Source.artifact(Artifact.flutterXcframework, platform: TargetPlatform.ios, mode: buildMode),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/Flutter.framework/Flutter'),
];
@override
List<Target> get dependencies => <Target>[];
@visibleForOverriding
BuildMode get buildMode;
@override
Future<void> build(Environment environment) async {
final String? sdkRoot = environment.defines[kSdkRoot];
if (sdkRoot == null) {
throw MissingDefineException(kSdkRoot, name);
}
final String? archs = environment.defines[kIosArchs];
if (archs == null) {
throw MissingDefineException(kIosArchs, name);
}
await _copyFramework(environment, sdkRoot);
final File frameworkBinary = environment.outputDir
.childDirectory('Flutter.framework')
.childFile('Flutter');
final String frameworkBinaryPath = frameworkBinary.path;
if (!await frameworkBinary.exists()) {
throw Exception('Binary $frameworkBinaryPath does not exist, cannot thin');
}
await _thinFramework(environment, frameworkBinaryPath, archs);
await _signFramework(environment, frameworkBinary, buildMode);
}
Future<void> _copyFramework(Environment environment, String sdkRoot) async {
// Copy Flutter framework.
final EnvironmentType? environmentType = environmentTypeFromSdkroot(
sdkRoot,
environment.fileSystem,
);
final String basePath = environment.artifacts.getArtifactPath(
Artifact.flutterFramework,
platform: TargetPlatform.ios,
mode: buildMode,
environmentType: environmentType,
);
final ProcessResult result = await environment.processManager.run(<String>[
'rsync',
'-av',
'--delete',
'--filter',
'- .DS_Store/',
'--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r',
basePath,
environment.outputDir.path,
]);
if (result.exitCode != 0) {
throw Exception(
'Failed to copy framework (exit ${result.exitCode}:\n'
'${result.stdout}\n---\n${result.stderr}',
);
}
// Copy Flutter framework dSYM (debug symbol) bundle, if present.
final Directory frameworkDsym = environment.fileSystem.directory(
environment.artifacts.getArtifactPath(
Artifact.flutterFrameworkDsym,
platform: TargetPlatform.ios,
mode: buildMode,
environmentType: environmentType,
),
);
if (frameworkDsym.existsSync()) {
final ProcessResult result = await environment.processManager.run(<String>[
'rsync',
'-av',
'--delete',
'--filter',
'- .DS_Store/',
'--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r',
frameworkDsym.path,
environment.outputDir.path,
]);
if (result.exitCode != 0) {
throw Exception(
'Failed to copy framework dSYM (exit ${result.exitCode}:\n'
'${result.stdout}\n---\n${result.stderr}',
);
}
}
}
/// Destructively thin Flutter.framework to include only the specified architectures.
Future<void> _thinFramework(
Environment environment,
String frameworkBinaryPath,
String archs,
) async {
final List<String> archList = archs.split(' ').toList();
final ProcessResult infoResult = await environment.processManager.run(<String>[
'lipo',
'-info',
frameworkBinaryPath,
]);
final String lipoInfo = infoResult.stdout as String;
final ProcessResult verifyResult = await environment.processManager.run(<String>[
'lipo',
frameworkBinaryPath,
'-verify_arch',
...archList,
]);
if (verifyResult.exitCode != 0) {
throw Exception(
'Binary $frameworkBinaryPath does not contain architectures "$archs".\n'
'\n'
'lipo -info:\n'
'$lipoInfo',
);
}
// Skip thinning for non-fat executables.
if (lipoInfo.startsWith('Non-fat file:')) {
environment.logger.printTrace('Skipping lipo for non-fat file $frameworkBinaryPath');
return;
}
// Thin in-place.
final ProcessResult extractResult = await environment.processManager.run(<String>[
'lipo',
'-output',
frameworkBinaryPath,
for (final String arch in archList) ...<String>['-extract', arch],
...<String>[frameworkBinaryPath],
]);
if (extractResult.exitCode != 0) {
throw Exception(
'Failed to extract architectures "$archs" for $frameworkBinaryPath.\n'
'\n'
'stderr:\n'
'${extractResult.stderr}\n\n'
'lipo -info:\n'
'$lipoInfo',
);
}
}
}
/// Unpack the release prebuilt engine framework.
class ReleaseUnpackIOS extends UnpackIOS {
const ReleaseUnpackIOS();
@override
String get name => 'release_unpack_ios';
@override
BuildMode get buildMode => BuildMode.release;
}
/// Unpack the profile prebuilt engine framework.
class ProfileUnpackIOS extends UnpackIOS {
const ProfileUnpackIOS();
@override
String get name => 'profile_unpack_ios';
@override
BuildMode get buildMode => BuildMode.profile;
}
/// Unpack the debug prebuilt engine framework.
class DebugUnpackIOS extends UnpackIOS {
const DebugUnpackIOS();
@override
String get name => 'debug_unpack_ios';
@override
BuildMode get buildMode => BuildMode.debug;
}
abstract class IosLLDBInit extends Target {
const IosLLDBInit();
@override
List<Source> get inputs => <Source>[
const Source.pattern(
'{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart',
),
];
@override
List<Source> get outputs {
final FlutterProject flutterProject = FlutterProject.current();
final String lldbInitFilePath = flutterProject.ios.lldbInitFile.path.replaceFirst(
flutterProject.directory.path,
'{PROJECT_DIR}/',
);
return <Source>[Source.pattern(lldbInitFilePath)];
}
@override
List<Target> get dependencies => <Target>[];
@visibleForOverriding
BuildMode get buildMode;
@override
Future<void> build(Environment environment) async {
final String? sdkRoot = environment.defines[kSdkRoot];
if (sdkRoot == null) {
throw MissingDefineException(kSdkRoot, name);
}
final EnvironmentType? environmentType = environmentTypeFromSdkroot(
sdkRoot,
environment.fileSystem,
);
// LLDB Init File is only required for physical devices in debug mode.
if (!buildMode.isJit || environmentType != EnvironmentType.physical) {
return;
}
final String? targetDeviceVersionString = environment.defines[kTargetDeviceOSVersion];
if (targetDeviceVersionString == null) {
// Skip if TARGET_DEVICE_OS_VERSION is not found. TARGET_DEVICE_OS_VERSION
// is not set if "build ios-framework" is called, which builds the
// DebugIosApplicationBundle directly rather than through flutter assemble.
// If may also be null if the build is targeting multiple device types.
return;
}
final Version? targetDeviceVersion = Version.parse(targetDeviceVersionString);
if (targetDeviceVersion == null) {
environment.logger.printError(
'Failed to parse Target Device Version $targetDeviceVersionString',
);
return;
}
// LLDB Init File is only needed for iOS 18.4+.
if (targetDeviceVersion < Version(18, 4, null)) {
return;
}
// The scheme name is not available in Xcode Build Phases Run Scripts.
// Instead, find all xcscheme files in the Xcode project (this may be the
// Flutter Xcode project or an Add to App native Xcode project) and check
// if any of them contain "customLLDBInitFile". If none have it set, throw
// an error.
final String? srcRoot = environment.defines[kSrcRoot];
if (srcRoot == null) {
throw MissingDefineException(kSdkRoot, name);
}
final Directory xcodeProjectDir = environment.fileSystem.directory(srcRoot);
if (!xcodeProjectDir.existsSync()) {
throw Exception('Failed to find ${xcodeProjectDir.path}');
}
bool anyLLDBInitFound = false;
await for (final FileSystemEntity entity in xcodeProjectDir.list(recursive: true)) {
if (environment.fileSystem.path.extension(entity.path) == '.xcscheme' && entity is File) {
if (entity.readAsStringSync().contains('customLLDBInitFile')) {
anyLLDBInitFound = true;
break;
}
}
}
if (!anyLLDBInitFound) {
final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
if (flutterProject.isModule) {
throwToolExit(
'Debugging Flutter on new iOS versions requires an LLDB Init File. To '
'ensure debug mode works, please run "flutter build ios --config-only" '
'in your Flutter project and follow the instructions to add the file.',
);
} else {
throwToolExit(
'Debugging Flutter on new iOS versions requires an LLDB Init File. To '
'ensure debug mode works, please run "flutter build ios --config-only" '
'in your Flutter project and automatically add the files.',
);
}
}
return;
}
}
class DebugIosLLDBInit extends IosLLDBInit {
const DebugIosLLDBInit();
@override
String get name => 'debug_ios_lldb_init';
@override
BuildMode get buildMode => BuildMode.debug;
}
/// The base class for all iOS bundle targets.
///
/// This is responsible for setting up the basic App.framework structure, including:
/// * Copying the app.dill/kernel_blob.bin from the build directory to assets (debug)
/// * Copying the precompiled isolate/vm data from the engine (debug)
/// * Copying the flutter assets to App.framework/flutter_assets
/// * Copying either the stub or real App assembly file to App.framework/App
abstract class IosAssetBundle extends Target {
const IosAssetBundle();
@override
List<Target> get dependencies => const <Target>[KernelSnapshot(), InstallCodeAssets()];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{BUILD_DIR}/App.framework/App'),
Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
...IconTreeShaker.inputs,
...ShaderCompiler.inputs,
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/App.framework/App'),
Source.pattern('{OUTPUT_DIR}/App.framework/Info.plist'),
];
@override
List<String> get depfiles => <String>['flutter_assets.d'];
@override
Future<void> build(Environment environment) async {
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode);
final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework');
final File frameworkBinary = frameworkDirectory.childFile('App');
final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets');
frameworkDirectory.createSync(recursive: true);
assetDirectory.createSync();
// Only copy the prebuilt runtimes and kernel blob in debug mode.
if (buildMode == BuildMode.debug) {
// Copy the App.framework to the output directory.
environment.buildDir
.childDirectory('App.framework')
.childFile('App')
.copySync(frameworkBinary.path);
final String vmSnapshotData = environment.artifacts.getArtifactPath(
Artifact.vmSnapshotData,
mode: BuildMode.debug,
);
final String isolateSnapshotData = environment.artifacts.getArtifactPath(
Artifact.isolateSnapshotData,
mode: BuildMode.debug,
);
environment.buildDir
.childFile('app.dill')
.copySync(assetDirectory.childFile('kernel_blob.bin').path);
environment.fileSystem
.file(vmSnapshotData)
.copySync(assetDirectory.childFile('vm_snapshot_data').path);
environment.fileSystem
.file(isolateSnapshotData)
.copySync(assetDirectory.childFile('isolate_snapshot_data').path);
} else {
environment.buildDir
.childDirectory('App.framework')
.childFile('App')
.copySync(frameworkBinary.path);
}
// Copy the dSYM
if (environment.buildDir.childDirectory('App.framework.dSYM').existsSync()) {
final File dsymOutputBinary = environment.outputDir
.childDirectory('App.framework.dSYM')
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF')
.childFile('App');
dsymOutputBinary.parent.createSync(recursive: true);
environment.buildDir
.childDirectory('App.framework.dSYM')
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF')
.childFile('App')
.copySync(dsymOutputBinary.path);
}
final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
final String? flavor = await flutterProject.ios.parseFlavorFromConfiguration(environment);
// Copy the assets.
final Depfile assetDepfile = await copyAssets(
environment,
assetDirectory,
targetPlatform: TargetPlatform.ios,
buildMode: buildMode,
additionalInputs: <File>[
flutterProject.ios.infoPlist,
flutterProject.ios.appFrameworkInfoPlist,
],
additionalContent: <String, DevFSContent>{
'NativeAssetsManifest.json': DevFSFileContent(
environment.buildDir.childFile('native_assets.json'),
),
},
flavor: flavor,
);
environment.depFileService.writeToFile(
assetDepfile,
environment.buildDir.childFile('flutter_assets.d'),
);
// Copy the plist from either the project or module.
flutterProject.ios.appFrameworkInfoPlist.copySync(
environment.outputDir.childDirectory('App.framework').childFile('Info.plist').path,
);
await _signFramework(environment, frameworkBinary, buildMode);
}
}
/// Build a debug iOS application bundle.
class DebugIosApplicationBundle extends IosAssetBundle {
const DebugIosApplicationBundle();
@override
String get name => 'debug_ios_bundle_flutter_assets';
@override
List<Source> get inputs => <Source>[
const Source.artifact(Artifact.vmSnapshotData, mode: BuildMode.debug),
const Source.artifact(Artifact.isolateSnapshotData, mode: BuildMode.debug),
const Source.pattern('{BUILD_DIR}/app.dill'),
...super.inputs,
];
@override
List<Source> get outputs => <Source>[
const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/vm_snapshot_data'),
const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/isolate_snapshot_data'),
const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/kernel_blob.bin'),
...super.outputs,
];
@override
List<Target> get dependencies => <Target>[
const DebugUniversalFramework(),
const DebugIosLLDBInit(),
...super.dependencies,
];
}
/// IosAssetBundle with debug symbols, used for Profile and Release builds.
abstract class _IosAssetBundleWithDSYM extends IosAssetBundle {
const _IosAssetBundleWithDSYM();
@override
List<Source> get inputs => <Source>[
...super.inputs,
const Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
@override
List<Source> get outputs => <Source>[
...super.outputs,
const Source.pattern('{OUTPUT_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
}
/// Build a profile iOS application bundle.
class ProfileIosApplicationBundle extends _IosAssetBundleWithDSYM {
const ProfileIosApplicationBundle();
@override
String get name => 'profile_ios_bundle_flutter_assets';
@override
List<Target> get dependencies => const <Target>[AotAssemblyProfile(), InstallCodeAssets()];
}
/// Build a release iOS application bundle.
class ReleaseIosApplicationBundle extends _IosAssetBundleWithDSYM {
const ReleaseIosApplicationBundle();
@override
String get name => 'release_ios_bundle_flutter_assets';
@override
List<Target> get dependencies => const <Target>[AotAssemblyRelease(), InstallCodeAssets()];
@override
Future<void> build(Environment environment) async {
bool buildSuccess = true;
try {
await super.build(environment);
} catch (_) {
buildSuccess = false;
rethrow;
} finally {
// Send a usage event when the app is being archived.
// Since assemble is run during a `flutter build`/`run` as well as an out-of-band
// archive command from Xcode, this is a more accurate count than `flutter build ipa` alone.
if (environment.defines[kXcodeAction]?.toLowerCase() == 'install') {
environment.logger.printTrace('Sending archive event if usage enabled.');
environment.analytics.send(
Event.appleUsageEvent(
workflow: 'assemble',
parameter: 'ios-archive',
result: buildSuccess ? 'success' : 'fail',
),
);
}
}
}
}
/// Create an App.framework for debug iOS targets.
///
/// This framework needs to exist for the Xcode project to link/bundle,
/// but it isn't actually executed. To generate something valid, we compile a trivial
/// constant.
Future<void> _createStubAppFramework(
File outputFile,
Environment environment,
Set<String>? iosArchNames,
String sdkRoot,
) async {
try {
outputFile.createSync(recursive: true);
} on Exception catch (e) {
throwToolExit('Failed to create App.framework stub at ${outputFile.path}: $e');
}
final FileSystem fileSystem = environment.fileSystem;
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync(
'flutter_tools_stub_source.',
);
try {
final File stubSource = tempDir.childFile('debug_app.cc')..writeAsStringSync(r'''
static const int Moo = 88;
''');
final EnvironmentType? environmentType = environmentTypeFromSdkroot(sdkRoot, fileSystem);
await globals.xcode!.clang(<String>[
'-x',
'c',
for (final String arch in iosArchNames ?? <String>{}) ...<String>['-arch', arch],
stubSource.path,
'-dynamiclib',
// Keep version in sync with AOTSnapshotter flag
if (environmentType == EnvironmentType.physical)
'-miphoneos-version-min=12.0'
else
'-miphonesimulator-version-min=12.0',
'-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
'-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
'-fapplication-extension',
'-install_name', '@rpath/App.framework/App',
'-isysroot', sdkRoot,
'-o', outputFile.path,
]);
} finally {
try {
tempDir.deleteSync(recursive: true);
} on FileSystemException {
// Best effort. Sometimes we can't delete things from system temp.
} on Exception catch (e) {
throwToolExit('Failed to create App.framework stub at ${outputFile.path}: $e');
}
}
await _signFramework(environment, outputFile, BuildMode.debug);
}
Future<void> _signFramework(Environment environment, File binary, BuildMode buildMode) async {
await removeFinderExtendedAttributes(
binary,
ProcessUtils(processManager: environment.processManager, logger: environment.logger),
environment.logger,
);
String? codesignIdentity = environment.defines[kCodesignIdentity];
if (codesignIdentity == null || codesignIdentity.isEmpty) {
codesignIdentity = '-';
}
final ProcessResult result = environment.processManager.runSync(<String>[
'codesign',
'--force',
'--sign',
codesignIdentity,
if (buildMode != BuildMode.release) ...<String>[
// Mimic Xcode's timestamp codesigning behavior on non-release binaries.
'--timestamp=none',
],
binary.path,
]);
if (result.exitCode != 0) {
final String stdout = (result.stdout as String).trim();
final String stderr = (result.stderr as String).trim();
final StringBuffer output = StringBuffer();
output.writeln('Failed to codesign ${binary.path} with identity $codesignIdentity.');
if (stdout.isNotEmpty) {
output.writeln(stdout);
}
if (stderr.isNotEmpty) {
output.writeln(stderr);
}
throw Exception(output.toString());
}
}