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/// @docImport 'ios/mac.dart';
6library;
7
8import 'base/error_handling_io.dart';
9import 'base/file_system.dart';
10import 'base/logger.dart';
11import 'base/template.dart';
12import 'base/utils.dart';
13import 'base/version.dart';
14import 'build_info.dart';
15import 'build_system/build_system.dart';
16import 'bundle.dart' as bundle;
17import 'convert.dart';
18import 'features.dart';
19import 'flutter_plugins.dart';
20import 'globals.dart' as globals;
21import 'ios/code_signing.dart';
22import 'ios/plist_parser.dart';
23import 'ios/xcode_build_settings.dart' as xcode;
24import 'ios/xcodeproj.dart';
25import 'macos/swift_package_manager.dart';
26import 'macos/xcode.dart';
27import 'platform_plugins.dart';
28import 'project.dart';
29import 'template.dart';
30
31/// Represents an Xcode-based sub-project.
32///
33/// This defines interfaces common to iOS and macOS projects.
34abstract class XcodeBasedProject extends FlutterProjectPlatform {
35 static const _defaultHostAppName = 'Runner';
36
37 /// The Xcode workspace (.xcworkspace directory) of the host app.
38 Directory? get xcodeWorkspace {
39 if (!hostAppRoot.existsSync()) {
40 return null;
41 }
42 return _xcodeDirectoryWithExtension('.xcworkspace');
43 }
44
45 /// The project name (.xcodeproj basename) of the host app.
46 late final String hostAppProjectName = () {
47 if (!hostAppRoot.existsSync()) {
48 return _defaultHostAppName;
49 }
50 final Directory? xcodeProjectDirectory = _xcodeDirectoryWithExtension('.xcodeproj');
51 return xcodeProjectDirectory != null
52 ? xcodeProjectDirectory.fileSystem.path.basenameWithoutExtension(xcodeProjectDirectory.path)
53 : _defaultHostAppName;
54 }();
55
56 Directory? _xcodeDirectoryWithExtension(String extension) {
57 final List<FileSystemEntity> contents = hostAppRoot.listSync();
58 for (final entity in contents) {
59 if (globals.fs.path.extension(entity.path) == extension &&
60 !globals.fs.path.basename(entity.path).startsWith('.')) {
61 return hostAppRoot.childDirectory(entity.basename);
62 }
63 }
64 return null;
65 }
66
67 /// The parent of this project.
68 FlutterProject get parent;
69
70 Directory get hostAppRoot;
71
72 /// The default 'Info.plist' file of the host app. The developer can change this location in Xcode.
73 File get defaultHostInfoPlist =>
74 hostAppRoot.childDirectory(_defaultHostAppName).childFile('Info.plist');
75
76 /// The Xcode project (.xcodeproj directory) of the host app.
77 Directory get xcodeProject => hostAppRoot.childDirectory('$hostAppProjectName.xcodeproj');
78
79 /// The 'project.pbxproj' file of [xcodeProject].
80 File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
81
82 /// The 'Runner.xcscheme' file of [xcodeProject].
83 File xcodeProjectSchemeFile({String? scheme}) {
84 final String schemeName = scheme ?? 'Runner';
85 return xcodeProject
86 .childDirectory('xcshareddata')
87 .childDirectory('xcschemes')
88 .childFile('$schemeName.xcscheme');
89 }
90
91 File get xcodeProjectWorkspaceData =>
92 xcodeProject.childDirectory('project.xcworkspace').childFile('contents.xcworkspacedata');
93
94 /// Xcode workspace shared data directory for the host app.
95 Directory? get xcodeWorkspaceSharedData => xcodeWorkspace?.childDirectory('xcshareddata');
96
97 /// Xcode workspace shared workspace settings file for the host app.
98 File? get xcodeWorkspaceSharedSettings =>
99 xcodeWorkspaceSharedData?.childFile('WorkspaceSettings.xcsettings');
100
101 /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
102 /// the Xcode build.
103 File get generatedXcodePropertiesFile;
104
105 /// The Flutter-managed Xcode config file for [mode].
106 File xcodeConfigFor(String mode);
107
108 /// The script that exports environment variables needed for Flutter tools.
109 /// Can be run first in a Xcode Script build phase to make FLUTTER_ROOT,
110 /// LOCAL_ENGINE, and other Flutter variables available to any flutter
111 /// tooling (`flutter build`, etc) to convert into flags.
112 File get generatedEnvironmentVariableExportScript;
113
114 /// The CocoaPods 'Podfile'.
115 File get podfile => hostAppRoot.childFile('Podfile');
116
117 /// The CocoaPods 'Podfile.lock'.
118 File get podfileLock => hostAppRoot.childFile('Podfile.lock');
119
120 /// The CocoaPods 'Manifest.lock'.
121 File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock');
122
123 /// The CocoaPods generated 'Pods-Runner-frameworks.sh'.
124 File get podRunnerFrameworksScript =>
125 podRunnerTargetSupportFiles.childFile('Pods-Runner-frameworks.sh');
126
127 /// The CocoaPods generated directory 'Pods-Runner'.
128 Directory get podRunnerTargetSupportFiles => hostAppRoot
129 .childDirectory('Pods')
130 .childDirectory('Target Support Files')
131 .childDirectory('Pods-Runner');
132
133 /// The directory in the project that is managed by Flutter. As much as
134 /// possible, files that are edited by Flutter tooling after initial project
135 /// creation should live here.
136 Directory get managedDirectory => hostAppRoot.childDirectory('Flutter');
137
138 /// The subdirectory of [managedDirectory] that contains files that are
139 /// generated on the fly. All generated files that are not intended to be
140 /// checked in should live here.
141 Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');
142
143 /// The Flutter generated directory for generated Swift packages.
144 Directory get flutterSwiftPackagesDirectory => ephemeralDirectory.childDirectory('Packages');
145
146 /// Flutter plugins that support SwiftPM will be symlinked in this directory to keep all
147 /// Swift packages relative to each other.
148 Directory get relativeSwiftPackagesDirectory =>
149 flutterSwiftPackagesDirectory.childDirectory('.packages');
150
151 /// The Flutter generated directory for the Swift package handling plugin
152 /// dependencies.
153 Directory get flutterPluginSwiftPackageDirectory =>
154 flutterSwiftPackagesDirectory.childDirectory(kFlutterGeneratedPluginSwiftPackageName);
155
156 /// The Flutter generated Swift package manifest (Package.swift) for plugin
157 /// dependencies.
158 File get flutterPluginSwiftPackageManifest =>
159 flutterPluginSwiftPackageDirectory.childFile('Package.swift');
160
161 /// Checks if FlutterGeneratedPluginSwiftPackage has been added to the
162 /// project's build settings by checking the contents of the pbxproj.
163 bool get flutterPluginSwiftPackageInProjectSettings {
164 return xcodeProjectInfoFile.existsSync() &&
165 xcodeProjectInfoFile.readAsStringSync().contains(kFlutterGeneratedPluginSwiftPackageName);
166 }
167
168 /// True if this project doesn't have Swift Package Manager disabled in the
169 /// pubspec, has either an iOS or macOS platform implementation, is not a
170 /// module project, Xcode is 15 or greater, and the Swift Package Manager
171 /// feature is enabled.
172 bool get usesSwiftPackageManager {
173 if (!featureFlags.isSwiftPackageManagerEnabled) {
174 return false;
175 }
176
177 // TODO(loic-sharma): Support Swift Package Manager in add-to-app modules.
178 // https://github.com/flutter/flutter/issues/146957
179 if (parent.isModule) {
180 return false;
181 }
182
183 if (!existsSync()) {
184 return false;
185 }
186
187 // Swift Package Manager requires Xcode 15 or greater.
188 final Xcode? xcode = globals.xcode;
189 final Version? xcodeVersion = xcode?.currentVersion;
190 if (xcodeVersion == null || xcodeVersion.major < 15) {
191 return false;
192 }
193
194 return true;
195 }
196
197 Future<XcodeProjectInfo?> projectInfo() async {
198 final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
199 if (!xcodeProject.existsSync() ||
200 xcodeProjectInterpreter == null ||
201 !xcodeProjectInterpreter.isInstalled) {
202 return null;
203 }
204 return _projectInfo ??= await xcodeProjectInterpreter.getInfo(hostAppRoot.path);
205 }
206
207 XcodeProjectInfo? _projectInfo;
208
209 /// Get the scheme using the Xcode's project [XcodeProjectInfo.schemes] and
210 /// the [BuildInfo.flavor].
211 Future<String?> schemeForBuildInfo(BuildInfo buildInfo, {Logger? logger}) async {
212 final XcodeProjectInfo? info = await projectInfo();
213 if (info == null) {
214 logger?.printError('Xcode project info not found.');
215 return null;
216 }
217
218 final String? scheme = info.schemeFor(buildInfo);
219 if (scheme == null) {
220 info.reportFlavorNotFoundAndExit();
221 }
222 return scheme;
223 }
224
225 /// The build settings for the host app of this project, as a detached map.
226 ///
227 /// Returns null, if Xcode tooling is unavailable.
228 Future<Map<String, String>?> buildSettingsForBuildInfo(
229 BuildInfo? buildInfo, {
230 String? scheme,
231 String? configuration,
232 String? target,
233 EnvironmentType environmentType = EnvironmentType.physical,
234 String? deviceId,
235 bool isWatch = false,
236 }) async {
237 if (!existsSync()) {
238 return null;
239 }
240 final XcodeProjectInfo? info = await projectInfo();
241 if (info == null) {
242 return null;
243 }
244
245 scheme ??= info.schemeFor(buildInfo);
246 if (scheme == null) {
247 info.reportFlavorNotFoundAndExit();
248 }
249
250 configuration ??= (await projectInfo())?.buildConfigurationFor(buildInfo, scheme);
251
252 final XcodeSdk sdk = switch ((environmentType, this)) {
253 (EnvironmentType.physical, _) when isWatch => XcodeSdk.WatchOS,
254 (EnvironmentType.simulator, _) when isWatch => XcodeSdk.WatchSimulator,
255 (EnvironmentType.physical, IosProject _) => XcodeSdk.IPhoneOS,
256 (EnvironmentType.simulator, IosProject _) => XcodeSdk.IPhoneSimulator,
257 (EnvironmentType.physical, MacOSProject _) => XcodeSdk.MacOSX,
258 (_, _) => throw ArgumentError('Unsupported SDK'),
259 };
260
261 return _buildSettingsForXcodeProjectBuildContext(
262 XcodeProjectBuildContext(
263 scheme: scheme,
264 configuration: configuration,
265 sdk: sdk,
266 target: target,
267 deviceId: deviceId,
268 ),
269 );
270 }
271
272 Future<Map<String, String>?> _buildSettingsForXcodeProjectBuildContext(
273 XcodeProjectBuildContext buildContext,
274 ) async {
275 if (!existsSync()) {
276 return null;
277 }
278 final Map<String, String>? currentBuildSettings = _buildSettingsByBuildContext[buildContext];
279 if (currentBuildSettings == null) {
280 final Map<String, String>? calculatedBuildSettings = await _xcodeProjectBuildSettings(
281 buildContext,
282 );
283 if (calculatedBuildSettings != null) {
284 _buildSettingsByBuildContext[buildContext] = calculatedBuildSettings;
285 }
286 }
287 return _buildSettingsByBuildContext[buildContext];
288 }
289
290 final _buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
291
292 Future<Map<String, String>?> _xcodeProjectBuildSettings(
293 XcodeProjectBuildContext buildContext,
294 ) async {
295 final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
296 if (xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) {
297 return null;
298 }
299
300 final Map<String, String> buildSettings = await xcodeProjectInterpreter.getBuildSettings(
301 xcodeProject.path,
302 buildContext: buildContext,
303 );
304 if (buildSettings.isNotEmpty) {
305 // No timeouts, flakes, or errors.
306 return buildSettings;
307 }
308 return null;
309 }
310
311 /// When flutter assemble runs within an Xcode run script, it does not know
312 /// the scheme and therefore doesn't know what flavor is being used. This
313 /// makes a best effort to parse the scheme name from the [kXcodeConfiguration].
314 /// Most flavor's [kXcodeConfiguration] should follow the naming convention
315 /// of '$baseConfiguration-$scheme'. This is only semi-enforced by
316 /// [buildXcodeProject], so it may not work. Also check if separated by a
317 /// space instead of a `-`. Once parsed, match it with a scheme/flavor name.
318 /// If the flavor cannot be parsed or matched, use the [kFlavor] environment
319 /// variable, which may or may not be set/correct, as a fallback.
320 Future<String?> parseFlavorFromConfiguration(Environment environment) async {
321 final String? configuration = environment.defines[kXcodeConfiguration];
322 final String? flavor = environment.defines[kFlavor];
323 if (configuration == null) {
324 return flavor;
325 }
326 List<String> splitConfiguration = configuration.split('-');
327 if (splitConfiguration.length == 1) {
328 splitConfiguration = configuration.split(' ');
329 }
330 if (splitConfiguration.length == 1) {
331 return flavor;
332 }
333 final String parsedScheme = splitConfiguration[1];
334
335 final XcodeProjectInfo? info = await projectInfo();
336 if (info == null) {
337 return flavor;
338 }
339 for (final String schemeName in info.schemes) {
340 if (schemeName.toLowerCase() == parsedScheme.toLowerCase()) {
341 return schemeName;
342 }
343 }
344 return flavor;
345 }
346}
347
348/// Represents the iOS sub-project of a Flutter project.
349///
350/// Instances will reflect the contents of the `ios/` sub-folder of
351/// Flutter applications and the `.ios/` sub-folder of Flutter module projects.
352class IosProject extends XcodeBasedProject {
353 IosProject.fromFlutter(this.parent);
354
355 @override
356 final FlutterProject parent;
357
358 @override
359 String get pluginConfigKey => IOSPlugin.kConfigKey;
360
361 // build setting keys
362 static const kProductBundleIdKey = 'PRODUCT_BUNDLE_IDENTIFIER';
363 static const kTeamIdKey = 'DEVELOPMENT_TEAM';
364 static const kEntitlementFilePathKey = 'CODE_SIGN_ENTITLEMENTS';
365 static const kProductNameKey = 'PRODUCT_NAME';
366
367 static final _productBundleIdPattern = RegExp(
368 '^\\s*$kProductBundleIdKey\\s*=\\s*(["\']?)(.*?)\\1;\\s*\$',
369 );
370 static const _kProductBundleIdVariable = '\$($kProductBundleIdKey)';
371
372 // The string starts with `applinks:` and ignores the query param which starts with `?`.
373 static final _associatedDomainPattern = RegExp(r'^applinks:([^?]+)');
374
375 static const _lldbPythonHelperTemplateName = 'flutter_lldb_helper.py';
376
377 static const _lldbInitTemplate =
378 '''
379#
380# Generated file, do not edit.
381#
382
383command script import --relative-to-command-file $_lldbPythonHelperTemplateName
384''';
385
386 static const _lldbPythonHelperTemplate = r'''
387#
388# Generated file, do not edit.
389#
390
391import lldb
392
393def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
394 """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
395 base = frame.register["x0"].GetValueAsAddress()
396 page_len = frame.register["x1"].GetValueAsUnsigned()
397
398 # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
399 # first page to see if handled it correctly. This makes diagnosing
400 # misconfiguration (e.g. missing breakpoint) easier.
401 data = bytearray(page_len)
402 data[0:8] = b'IHELPED!'
403
404 error = lldb.SBError()
405 frame.GetThread().GetProcess().WriteMemory(base, data, error)
406 if not error.Success():
407 print(f'Failed to write into {base}[+{page_len}]', error)
408 return
409
410def __lldb_init_module(debugger: lldb.SBDebugger, _):
411 target = debugger.GetDummyTarget()
412 # Caveat: must use BreakpointCreateByRegEx here and not
413 # BreakpointCreateByName. For some reasons callback function does not
414 # get carried over from dummy target for the later.
415 bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
416 bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
417 bp.SetAutoContinue(True)
418 print("-- LLDB integration loaded --")
419''';
420
421 Directory get ephemeralModuleDirectory => parent.directory.childDirectory('.ios');
422 Directory get _editableDirectory => parent.directory.childDirectory('ios');
423
424 /// This parent folder of `Runner.xcodeproj`.
425 @override
426 Directory get hostAppRoot {
427 if (!isModule || _editableDirectory.existsSync()) {
428 return _editableDirectory;
429 }
430 return ephemeralModuleDirectory;
431 }
432
433 /// The root directory of the iOS wrapping of Flutter and plugins. This is the
434 /// parent of the `Flutter/` folder into which Flutter artifacts are written
435 /// during build.
436 ///
437 /// This is the same as [hostAppRoot] except when the project is
438 /// a Flutter module with an editable host app.
439 Directory get _flutterLibRoot => isModule ? ephemeralModuleDirectory : _editableDirectory;
440
441 /// True, if the parent Flutter project is a module project.
442 bool get isModule => parent.isModule;
443
444 /// Whether the Flutter application has an iOS project.
445 bool get exists => hostAppRoot.existsSync();
446
447 @override
448 Directory get managedDirectory => _flutterLibRoot.childDirectory('Flutter');
449
450 @override
451 File xcodeConfigFor(String mode) => managedDirectory.childFile('$mode.xcconfig');
452
453 @override
454 File get generatedEnvironmentVariableExportScript =>
455 managedDirectory.childFile('flutter_export_environment.sh');
456
457 File get appFrameworkInfoPlist => managedDirectory.childFile('AppFrameworkInfo.plist');
458
459 /// The 'AppDelegate.swift' file of the host app. This file might not exist if the app project uses Objective-C.
460 File get appDelegateSwift =>
461 _editableDirectory.childDirectory('Runner').childFile('AppDelegate.swift');
462
463 /// The 'AppDelegate.m' file of the host app. This file might not exist if the app project uses Swift.
464 File get appDelegateObjc =>
465 _editableDirectory.childDirectory('Runner').childFile('AppDelegate.m');
466
467 File get infoPlist => _editableDirectory.childDirectory('Runner').childFile('Info.plist');
468
469 Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks');
470
471 /// True if the app project uses Swift.
472 bool get isSwift => appDelegateSwift.existsSync();
473
474 /// Do all plugins support arm64 simulators to run natively on an ARM Mac?
475 Future<bool> pluginsSupportArmSimulator() async {
476 final Directory podXcodeProject = hostAppRoot
477 .childDirectory('Pods')
478 .childDirectory('Pods.xcodeproj');
479 if (!podXcodeProject.existsSync()) {
480 // No plugins.
481 return true;
482 }
483
484 final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
485 if (xcodeProjectInterpreter == null) {
486 // Xcode isn't installed, don't try to check.
487 return false;
488 }
489 final String? buildSettings = await xcodeProjectInterpreter.pluginsBuildSettingsOutput(
490 podXcodeProject,
491 );
492
493 // See if any plugins or their dependencies exclude arm64 simulators
494 // as a valid architecture, usually because a binary is missing that slice.
495 // Example: EXCLUDED_ARCHS = arm64 i386
496 // NOT: EXCLUDED_ARCHS = i386
497 return buildSettings != null && !buildSettings.contains(RegExp('EXCLUDED_ARCHS.*arm64'));
498 }
499
500 @override
501 bool existsSync() {
502 return parent.isModule || _editableDirectory.existsSync();
503 }
504
505 /// Outputs universal link related project settings of the iOS sub-project into
506 /// a json file.
507 ///
508 /// The return future will resolve to string path to the output file.
509 Future<String> outputsUniversalLinkSettings({
510 required String configuration,
511 required String target,
512 }) async {
513 final context = XcodeProjectBuildContext(configuration: configuration, target: target);
514 final File file = await parent.buildDirectory
515 .childDirectory('deeplink_data')
516 .childFile('universal-link-settings-$configuration-$target.json')
517 .create(recursive: true);
518
519 await file.writeAsString(
520 jsonEncode(<String, Object?>{
521 'bundleIdentifier': await _productBundleIdentifierWithBuildContext(context),
522 'teamIdentifier': await _getTeamIdentifier(context),
523 'associatedDomains': await _getAssociatedDomains(context),
524 }),
525 );
526 return file.absolute.path;
527 }
528
529 /// The product bundle identifier of the host app, or null if not set or if
530 /// iOS tooling needed to read it is not installed.
531 Future<String?> productBundleIdentifier(BuildInfo? buildInfo) async {
532 if (!existsSync()) {
533 return null;
534 }
535
536 XcodeProjectBuildContext? buildContext;
537 final XcodeProjectInfo? info = await projectInfo();
538 if (info != null) {
539 final String? scheme = info.schemeFor(buildInfo);
540 if (scheme == null) {
541 info.reportFlavorNotFoundAndExit();
542 }
543 final String? configuration = info.buildConfigurationFor(buildInfo, scheme);
544 buildContext = XcodeProjectBuildContext(configuration: configuration, scheme: scheme);
545 }
546 return _productBundleIdentifierWithBuildContext(buildContext);
547 }
548
549 Future<String?> _productBundleIdentifierWithBuildContext(
550 XcodeProjectBuildContext? buildContext,
551 ) async {
552 if (!existsSync()) {
553 return null;
554 }
555 if (_productBundleIdentifiers.containsKey(buildContext)) {
556 return _productBundleIdentifiers[buildContext];
557 }
558 return _productBundleIdentifiers[buildContext] = await _parseProductBundleIdentifier(
559 buildContext,
560 );
561 }
562
563 final _productBundleIdentifiers = <XcodeProjectBuildContext?, String?>{};
564
565 Future<String?> _parseProductBundleIdentifier(XcodeProjectBuildContext? buildContext) async {
566 String? fromPlist;
567 final File defaultInfoPlist = defaultHostInfoPlist;
568 // Users can change the location of the Info.plist.
569 // Try parsing the default, first.
570 if (defaultInfoPlist.existsSync()) {
571 try {
572 fromPlist = globals.plistParser.getValueFromFile<String>(
573 defaultHostInfoPlist.path,
574 PlistParser.kCFBundleIdentifierKey,
575 );
576 } on FileNotFoundException {
577 // iOS tooling not found; likely not running OSX; let [fromPlist] be null
578 }
579 if (fromPlist != null && !fromPlist.contains(r'$')) {
580 // Info.plist has no build variables in product bundle ID.
581 return fromPlist;
582 }
583 }
584 if (buildContext == null) {
585 // Getting build settings to evaluate info.Plist requires a context.
586 return null;
587 }
588
589 final Map<String, String>? allBuildSettings = await _buildSettingsForXcodeProjectBuildContext(
590 buildContext,
591 );
592 if (allBuildSettings != null) {
593 if (fromPlist != null) {
594 // Perform variable substitution using build settings.
595 return substituteXcodeVariables(fromPlist, allBuildSettings);
596 }
597 return allBuildSettings[kProductBundleIdKey];
598 }
599
600 // On non-macOS platforms, parse the first PRODUCT_BUNDLE_IDENTIFIER from
601 // the project file. This can return the wrong bundle identifier if additional
602 // bundles have been added to the project and are found first, like frameworks
603 // or companion watchOS projects. However, on non-macOS platforms this is
604 // only used for display purposes and to regenerate organization names, so
605 // best-effort is probably fine.
606 final String? fromPbxproj = firstMatchInFile(
607 xcodeProjectInfoFile,
608 _productBundleIdPattern,
609 )?.group(2);
610 if (fromPbxproj != null && (fromPlist == null || fromPlist == _kProductBundleIdVariable)) {
611 return fromPbxproj;
612 }
613 return null;
614 }
615
616 Future<String?> _getTeamIdentifier(XcodeProjectBuildContext buildContext) async {
617 final Map<String, String>? buildSettings = await _buildSettingsForXcodeProjectBuildContext(
618 buildContext,
619 );
620 return buildSettings?[kTeamIdKey];
621 }
622
623 Future<List<String>> _getAssociatedDomains(XcodeProjectBuildContext buildContext) async {
624 final Map<String, String>? buildSettings = await _buildSettingsForXcodeProjectBuildContext(
625 buildContext,
626 );
627 if (buildSettings != null) {
628 final String? entitlementPath = buildSettings[kEntitlementFilePathKey];
629 if (entitlementPath != null) {
630 final File entitlement = hostAppRoot.childFile(entitlementPath);
631 if (entitlement.existsSync()) {
632 final List<String>? domains = globals.plistParser
633 .getValueFromFile<List<Object>>(entitlement.path, PlistParser.kAssociatedDomainsKey)
634 ?.cast<String>();
635
636 if (domains != null) {
637 return <String>[
638 for (final String domain in domains)
639 if (_associatedDomainPattern.firstMatch(domain) case final RegExpMatch match)
640 match.group(1)!,
641 ];
642 }
643 }
644 }
645 }
646 return const <String>[];
647 }
648
649 /// The product name of the app, `My App`.
650 Future<String?> productName(BuildInfo? buildInfo) async {
651 if (!existsSync()) {
652 return null;
653 }
654 return _productName ??= await _parseProductName(buildInfo);
655 }
656
657 String? _productName;
658
659 Future<String> _parseProductName(BuildInfo? buildInfo) async {
660 // The product name and bundle name are derived from the display name, which the user
661 // is instructed to change in Xcode as part of deploying to the App Store.
662 // https://flutter.dev/to/xcode-name-config
663 // The only source of truth for the name is Xcode's interpretation of the build settings.
664 String? productName;
665 if (globals.xcodeProjectInterpreter?.isInstalled ?? false) {
666 final Map<String, String>? xcodeBuildSettings = await buildSettingsForBuildInfo(buildInfo);
667 if (xcodeBuildSettings != null) {
668 productName = xcodeBuildSettings[kProductNameKey];
669 }
670 }
671 if (productName == null) {
672 globals.printTrace('$kProductNameKey not present, defaulting to $hostAppProjectName');
673 }
674 return productName ?? XcodeBasedProject._defaultHostAppName;
675 }
676
677 Future<void> ensureReadyForPlatformSpecificTooling() async {
678 await _regenerateModuleFromTemplateIfNeeded();
679 await _updateLLDBIfNeeded();
680 if (!_flutterLibRoot.existsSync()) {
681 return;
682 }
683 await _updateGeneratedXcodeConfigIfNeeded();
684 }
685
686 /// Check if one the [XcodeProjectInfo.targets] of the project is
687 /// a watchOS companion app target.
688 Future<bool> containsWatchCompanion({
689 required XcodeProjectInfo projectInfo,
690 required BuildInfo buildInfo,
691 String? deviceId,
692 }) async {
693 final String? bundleIdentifier = await productBundleIdentifier(buildInfo);
694 // A bundle identifier is required for a companion app.
695 if (bundleIdentifier == null) {
696 return false;
697 }
698 for (final String target in projectInfo.targets) {
699 // Create Info.plist file of the target.
700 final File infoFile = hostAppRoot.childDirectory(target).childFile('Info.plist');
701 // In older versions of Xcode, if the target was a watchOS companion app,
702 // the Info.plist file of the target contained the key WKCompanionAppBundleIdentifier.
703 if (infoFile.existsSync()) {
704 final String? fromPlist = globals.plistParser.getValueFromFile<String>(
705 infoFile.path,
706 'WKCompanionAppBundleIdentifier',
707 );
708 if (bundleIdentifier == fromPlist) {
709 return true;
710 }
711
712 // The key WKCompanionAppBundleIdentifier might contain an xcode variable
713 // that needs to be substituted before comparing it with bundle id
714 if (fromPlist != null && fromPlist.contains(r'$')) {
715 final Map<String, String>? allBuildSettings = await buildSettingsForBuildInfo(
716 buildInfo,
717 deviceId: deviceId,
718 );
719 if (allBuildSettings != null) {
720 final String substitutedVariable = substituteXcodeVariables(
721 fromPlist,
722 allBuildSettings,
723 );
724 if (substitutedVariable == bundleIdentifier) {
725 return true;
726 }
727 }
728 }
729 }
730 }
731
732 // If key not found in Info.plist above, do more expensive check of build settings.
733 // In newer versions of Xcode, the build settings of the watchOS companion
734 // app's scheme should contain the key INFOPLIST_KEY_WKCompanionAppBundleIdentifier.
735 final bool watchIdentifierFound = xcodeProjectInfoFile.readAsStringSync().contains(
736 'WKCompanionAppBundleIdentifier',
737 );
738 if (!watchIdentifierFound) {
739 return false;
740 }
741
742 final String? defaultScheme = projectInfo.schemeFor(buildInfo);
743 if (defaultScheme == null) {
744 projectInfo.reportFlavorNotFoundAndExit();
745 }
746 for (final String scheme in projectInfo.schemes) {
747 // the default scheme should not be a watch scheme, so skip it
748 if (scheme == defaultScheme) {
749 continue;
750 }
751 final Map<String, String>? allBuildSettings = await buildSettingsForBuildInfo(
752 buildInfo,
753 deviceId: deviceId,
754 scheme: scheme,
755 isWatch: true,
756 );
757 if (allBuildSettings != null) {
758 final String? fromBuild = allBuildSettings['INFOPLIST_KEY_WKCompanionAppBundleIdentifier'];
759 if (bundleIdentifier == fromBuild) {
760 return true;
761 }
762 if (fromBuild != null && fromBuild.contains(r'$')) {
763 final String substitutedVariable = substituteXcodeVariables(fromBuild, allBuildSettings);
764 if (substitutedVariable == bundleIdentifier) {
765 return true;
766 }
767 }
768 }
769 }
770 return false;
771 }
772
773 Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
774 if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
775 await xcode.updateGeneratedXcodeProperties(
776 project: parent,
777 buildInfo: BuildInfo.dummy,
778 targetOverride: bundle.defaultMainPath,
779 );
780 }
781 }
782
783 Future<void> _updateLLDBIfNeeded() async {
784 if (globals.cache.isOlderThanToolsStamp(lldbInitFile) ||
785 globals.cache.isOlderThanToolsStamp(lldbHelperPythonFile)) {
786 await _renderTemplateToFile(_lldbInitTemplate, null, lldbInitFile, globals.templateRenderer);
787 await _renderTemplateToFile(
788 _lldbPythonHelperTemplate,
789 null,
790 lldbHelperPythonFile,
791 globals.templateRenderer,
792 );
793 }
794 }
795
796 Future<void> _renderTemplateToFile(
797 String template,
798 Object? context,
799 File file,
800 TemplateRenderer templateRenderer,
801 ) async {
802 final String renderedTemplate = templateRenderer.renderString(template, context);
803 await file.create(recursive: true);
804 await file.writeAsString(renderedTemplate);
805 }
806
807 Future<void> _regenerateModuleFromTemplateIfNeeded() async {
808 if (!isModule) {
809 return;
810 }
811 final bool pubspecChanged = globals.fsUtils.isOlderThanReference(
812 entity: ephemeralModuleDirectory,
813 referenceFile: parent.pubspecFile,
814 );
815 final bool toolingChanged = globals.cache.isOlderThanToolsStamp(ephemeralModuleDirectory);
816 if (!pubspecChanged && !toolingChanged) {
817 return;
818 }
819
820 ErrorHandlingFileSystem.deleteIfExists(ephemeralModuleDirectory, recursive: true);
821 await _overwriteFromTemplate(
822 globals.fs.path.join('module', 'ios', 'library'),
823 ephemeralModuleDirectory,
824 );
825 // Add ephemeral host app, if a editable host app does not already exist.
826 if (!_editableDirectory.existsSync()) {
827 await _overwriteFromTemplate(
828 globals.fs.path.join('module', 'ios', 'host_app_ephemeral'),
829 ephemeralModuleDirectory,
830 );
831 if (hasPlugins(parent)) {
832 await _overwriteFromTemplate(
833 globals.fs.path.join('module', 'ios', 'host_app_ephemeral_cocoapods'),
834 ephemeralModuleDirectory,
835 );
836 }
837 }
838 }
839
840 @override
841 File get generatedXcodePropertiesFile =>
842 _flutterLibRoot.childDirectory('Flutter').childFile('Generated.xcconfig');
843
844 /// No longer compiled to this location.
845 ///
846 /// Used only for "flutter clean" to remove old references.
847 Directory get deprecatedCompiledDartFramework =>
848 _flutterLibRoot.childDirectory('Flutter').childDirectory('App.framework');
849
850 /// No longer copied to this location.
851 ///
852 /// Used only for "flutter clean" to remove old references.
853 Directory get deprecatedProjectFlutterFramework =>
854 _flutterLibRoot.childDirectory('Flutter').childDirectory('Flutter.framework');
855
856 /// Used only for "flutter clean" to remove old references.
857 File get flutterPodspec => _flutterLibRoot.childDirectory('Flutter').childFile('Flutter.podspec');
858
859 Directory get pluginRegistrantHost {
860 return isModule
861 ? _flutterLibRoot.childDirectory('Flutter').childDirectory('FlutterPluginRegistrant')
862 : hostAppRoot.childDirectory(XcodeBasedProject._defaultHostAppName);
863 }
864
865 File get pluginRegistrantHeader {
866 final Directory registryDirectory = isModule
867 ? pluginRegistrantHost.childDirectory('Classes')
868 : pluginRegistrantHost;
869 return registryDirectory.childFile('GeneratedPluginRegistrant.h');
870 }
871
872 File get pluginRegistrantImplementation {
873 final Directory registryDirectory = isModule
874 ? pluginRegistrantHost.childDirectory('Classes')
875 : pluginRegistrantHost;
876 return registryDirectory.childFile('GeneratedPluginRegistrant.m');
877 }
878
879 File get lldbInitFile {
880 return ephemeralDirectory.childFile('flutter_lldbinit');
881 }
882
883 File get lldbHelperPythonFile {
884 return ephemeralDirectory.childFile(_lldbPythonHelperTemplateName);
885 }
886
887 Future<void> _overwriteFromTemplate(String path, Directory target) async {
888 final Template template = await Template.fromName(
889 path,
890 fileSystem: globals.fs,
891 templateManifest: null,
892 logger: globals.logger,
893 templateRenderer: globals.templateRenderer,
894 );
895 final String iosBundleIdentifier =
896 parent.manifest.iosBundleIdentifier ?? 'com.example.${parent.manifest.appName}';
897
898 final String? iosDevelopmentTeam = await getCodeSigningIdentityDevelopmentTeam(
899 processManager: globals.processManager,
900 platform: globals.platform,
901 logger: globals.logger,
902 config: globals.config,
903 terminal: globals.terminal,
904 fileSystem: globals.fs,
905 fileSystemUtils: globals.fsUtils,
906 plistParser: globals.plistParser,
907 );
908
909 final String projectName = parent.manifest.appName;
910
911 // The dart project_name is in snake_case, this variable is the Title Case of the Project Name.
912 final String titleCaseProjectName = snakeCaseToTitleCase(projectName);
913
914 template.render(target, <String, Object>{
915 'ios': true,
916 'projectName': projectName,
917 'titleCaseProjectName': titleCaseProjectName,
918 'iosIdentifier': iosBundleIdentifier,
919 'hasIosDevelopmentTeam': iosDevelopmentTeam != null && iosDevelopmentTeam.isNotEmpty,
920 'iosDevelopmentTeam': iosDevelopmentTeam ?? '',
921 }, printStatusWhenWriting: false);
922 }
923}
924
925/// The macOS sub project.
926class MacOSProject extends XcodeBasedProject {
927 MacOSProject.fromFlutter(this.parent);
928
929 @override
930 final FlutterProject parent;
931
932 @override
933 String get pluginConfigKey => MacOSPlugin.kConfigKey;
934
935 @override
936 bool existsSync() => hostAppRoot.existsSync();
937
938 @override
939 Directory get hostAppRoot => parent.directory.childDirectory('macos');
940
941 /// The xcfilelist used to track the inputs for the Flutter script phase in
942 /// the Xcode build.
943 File get inputFileList => ephemeralDirectory.childFile('FlutterInputs.xcfilelist');
944
945 /// The xcfilelist used to track the outputs for the Flutter script phase in
946 /// the Xcode build.
947 File get outputFileList => ephemeralDirectory.childFile('FlutterOutputs.xcfilelist');
948
949 @override
950 File get generatedXcodePropertiesFile =>
951 ephemeralDirectory.childFile('Flutter-Generated.xcconfig');
952
953 File get pluginRegistrantImplementation =>
954 managedDirectory.childFile('GeneratedPluginRegistrant.swift');
955
956 /// The 'AppDelegate.swift' file of the host app. This file might not exist if the app project uses Objective-C.
957 File get appDelegateSwift => hostAppRoot.childDirectory('Runner').childFile('AppDelegate.swift');
958
959 @override
960 File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig');
961
962 @override
963 File get generatedEnvironmentVariableExportScript =>
964 ephemeralDirectory.childFile('flutter_export_environment.sh');
965
966 /// The file where the Xcode build will write the name of the built app.
967 ///
968 /// Ideally this will be replaced in the future with inspection of the Runner
969 /// scheme's target.
970 File get nameFile => ephemeralDirectory.childFile('.app_filename');
971
972 Future<void> ensureReadyForPlatformSpecificTooling() async {
973 // TODO(stuartmorgan): Add create-from-template logic here.
974 await _updateGeneratedXcodeConfigIfNeeded();
975 }
976
977 Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
978 if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
979 await xcode.updateGeneratedXcodeProperties(
980 project: parent,
981 buildInfo: BuildInfo.dummy,
982 useMacOSConfig: true,
983 );
984 }
985 }
986}
987