blob: 5e48bf93d046178c9573e8580c0dd67ca4535530 [file] [log] [blame]
Ian Hickson449f4a62019-11-27 15:04:02 -08001// Copyright 2014 The Flutter Authors. All rights reserved.
Chinmay Garde5cebf702016-02-16 15:01:48 -08002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Jenn Magder675fd552021-01-27 11:04:03 -08005import 'package:file/memory.dart';
xster837f1012017-05-04 12:26:58 -07006import 'package:meta/meta.dart';
Jonah Williams021311e2021-02-08 09:21:46 -08007import 'package:process/process.dart';
xster837f1012017-05-04 12:26:58 -07008
stuartmorgan72888c72019-09-12 16:03:02 -07009import '../base/common.dart';
Todd Volkert8bb27032017-01-06 16:51:44 -080010import '../base/file_system.dart';
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +010011import '../base/io.dart';
Zachary Andersonb7c714e2019-08-28 10:03:53 -070012import '../base/logger.dart';
Jenn Magder2f3cccc2020-11-04 19:54:02 -080013import '../base/os.dart';
Zachary Anderson6f0ed5e2020-05-06 08:15:39 -070014import '../base/platform.dart';
Chris Brackena45e4e92016-09-26 13:25:57 -070015import '../base/process.dart';
Jonah Williamsef15eac2020-01-23 15:03:04 -080016import '../base/terminal.dart';
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +020017import '../base/utils.dart';
Jenn Magder72976f52021-04-16 17:22:46 -070018import '../base/version.dart';
Jason Simmonsa590ee22016-05-12 12:22:15 -070019import '../build_info.dart';
Zachary Anderson0f2af972019-09-05 09:50:46 -070020import '../reporting/reporting.dart';
Chinmay Garde5cebf702016-02-16 15:01:48 -080021
Alexandre Ardhuind927c932018-09-12 08:29:29 +020022final RegExp _settingExpr = RegExp(r'(\w+)\s*=\s*(.*)$');
23final RegExp _varExpr = RegExp(r'\$\(([^)]*)\)');
Chris Brackena45e4e92016-09-26 13:25:57 -070024
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +010025/// Interpreter of Xcode projects.
Mikkel Nygaard Ravn20004352018-02-16 10:17:28 +010026class XcodeProjectInterpreter {
Jenn Magder675fd552021-01-27 11:04:03 -080027 factory XcodeProjectInterpreter({
Jenn Magder72976f52021-04-16 17:22:46 -070028 required Platform platform,
29 required ProcessManager processManager,
30 required Logger logger,
31 required FileSystem fileSystem,
32 required Usage usage,
Jenn Magder675fd552021-01-27 11:04:03 -080033 }) {
34 return XcodeProjectInterpreter._(
35 platform: platform,
36 processManager: processManager,
37 logger: logger,
38 fileSystem: fileSystem,
Jenn Magder675fd552021-01-27 11:04:03 -080039 usage: usage,
40 );
41 }
42
43 XcodeProjectInterpreter._({
Jenn Magder72976f52021-04-16 17:22:46 -070044 required Platform platform,
45 required ProcessManager processManager,
46 required Logger logger,
47 required FileSystem fileSystem,
48 required Usage usage,
49 Version? version,
Jonah Williamsef15eac2020-01-23 15:03:04 -080050 }) : _platform = platform,
Jenn Magder675fd552021-01-27 11:04:03 -080051 _fileSystem = fileSystem,
Jenn Magder675fd552021-01-27 11:04:03 -080052 _logger = logger,
53 _processUtils = ProcessUtils(logger: logger, processManager: processManager),
54 _operatingSystemUtils = OperatingSystemUtils(
55 fileSystem: fileSystem,
56 logger: logger,
57 platform: platform,
58 processManager: processManager,
59 ),
Jenn Magder72976f52021-04-16 17:22:46 -070060 _version = version,
Jenn Magder675fd552021-01-27 11:04:03 -080061 _usage = usage;
62
63 /// Create an [XcodeProjectInterpreter] for testing.
64 ///
65 /// Defaults to installed with sufficient version,
66 /// a memory file system, fake platform, buffer logger,
67 /// test [Usage], and test [Terminal].
Jenn Magder72976f52021-04-16 17:22:46 -070068 /// Set [version] to null to simulate Xcode not being installed.
Jenn Magder675fd552021-01-27 11:04:03 -080069 factory XcodeProjectInterpreter.test({
Jenn Magder72976f52021-04-16 17:22:46 -070070 required ProcessManager processManager,
71 Version? version = const Version.withText(1000, 0, 0, '1000.0.0'),
Jenn Magder675fd552021-01-27 11:04:03 -080072 }) {
73 final Platform platform = FakePlatform(
74 operatingSystem: 'macos',
75 environment: <String, String>{},
76 );
77 return XcodeProjectInterpreter._(
78 fileSystem: MemoryFileSystem.test(),
79 platform: platform,
80 processManager: processManager,
Jonah Williamsb926c7b2021-01-28 12:41:14 -080081 usage: TestUsage(),
Jenn Magder675fd552021-01-27 11:04:03 -080082 logger: BufferLogger.test(),
Jenn Magder72976f52021-04-16 17:22:46 -070083 version: version,
Jenn Magder675fd552021-01-27 11:04:03 -080084 );
85 }
Jonah Williamsef15eac2020-01-23 15:03:04 -080086
87 final Platform _platform;
88 final FileSystem _fileSystem;
89 final ProcessUtils _processUtils;
Jenn Magder2f3cccc2020-11-04 19:54:02 -080090 final OperatingSystemUtils _operatingSystemUtils;
Jonah Williamsef15eac2020-01-23 15:03:04 -080091 final Logger _logger;
Jenn Magder77ea8482020-04-02 10:57:37 -070092 final Usage _usage;
Jonah Williamsef15eac2020-01-23 15:03:04 -080093
Alexandre Ardhuind927c932018-09-12 08:29:29 +020094 static final RegExp _versionRegex = RegExp(r'Xcode ([0-9.]+)');
Mikkel Nygaard Ravn20004352018-02-16 10:17:28 +010095
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +010096 void _updateVersion() {
Jenn Magder2f3cccc2020-11-04 19:54:02 -080097 if (!_platform.isMacOS || !_fileSystem.file('/usr/bin/xcodebuild').existsSync()) {
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +010098 return;
99 }
100 try {
Jenn Magder4fba7742020-07-14 17:16:02 -0700101 if (_versionText == null) {
102 final RunResult result = _processUtils.runSync(
Jenn Magder2f3cccc2020-11-04 19:54:02 -0800103 <String>[...xcrunCommand(), 'xcodebuild', '-version'],
Jenn Magder4fba7742020-07-14 17:16:02 -0700104 );
105 if (result.exitCode != 0) {
106 return;
107 }
108 _versionText = result.stdout.trim().replaceAll('\n', ', ');
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100109 }
Jenn Magder72976f52021-04-16 17:22:46 -0700110 final Match? match = _versionRegex.firstMatch(versionText!);
Zachary Andersone2340c62019-09-13 14:51:35 -0700111 if (match == null) {
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100112 return;
Zachary Andersone2340c62019-09-13 14:51:35 -0700113 }
Jenn Magder72976f52021-04-16 17:22:46 -0700114 final String version = match.group(1)!;
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100115 final List<String> components = version.split('.');
Jenn Magder72976f52021-04-16 17:22:46 -0700116 final int majorVersion = int.parse(components[0]);
117 final int minorVersion = components.length < 2 ? 0 : int.parse(components[1]);
118 final int patchVersion = components.length < 3 ? 0 : int.parse(components[2]);
119 _version = Version(majorVersion, minorVersion, patchVersion);
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100120 } on ProcessException {
Michael Goderbauer6d20ff22019-01-30 08:56:12 -0800121 // Ignored, leave values null.
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100122 }
123 }
Mikkel Nygaard Ravn20004352018-02-16 10:17:28 +0100124
Jenn Magder72976f52021-04-16 17:22:46 -0700125 bool get isInstalled => version != null;
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100126
Jenn Magder72976f52021-04-16 17:22:46 -0700127 String? _versionText;
128 String? get versionText {
Zachary Andersone2340c62019-09-13 14:51:35 -0700129 if (_versionText == null) {
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100130 _updateVersion();
Zachary Andersone2340c62019-09-13 14:51:35 -0700131 }
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100132 return _versionText;
133 }
134
Jenn Magder72976f52021-04-16 17:22:46 -0700135 Version? _version;
136 Version? get version {
137 if (_version == null) {
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100138 _updateVersion();
Zachary Andersone2340c62019-09-13 14:51:35 -0700139 }
Jenn Magder72976f52021-04-16 17:22:46 -0700140 return _version;
Jenn Magder4fba7742020-07-14 17:16:02 -0700141 }
142
Jenn Magder2f3cccc2020-11-04 19:54:02 -0800143 /// The `xcrun` Xcode command to run or locate development
144 /// tools and properties.
145 ///
146 /// Returns `xcrun` on x86 macOS.
147 /// Returns `/usr/bin/arch -arm64e xcrun` on ARM macOS to force Xcode commands
148 /// to run outside the x86 Rosetta translation, which may cause crashes.
149 List<String> xcrunCommand() {
150 final List<String> xcrunCommand = <String>[];
151 if (_operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm) {
152 // Force Xcode commands to run outside Rosetta.
153 xcrunCommand.addAll(<String>[
154 '/usr/bin/arch',
155 '-arm64e',
156 ]);
157 }
158 xcrunCommand.add('xcrun');
159 return xcrunCommand;
160 }
161
Zachary Andersonb7c714e2019-08-28 10:03:53 -0700162 /// Asynchronously retrieve xcode build settings. This one is preferred for
163 /// new call-sites.
Jenn Magder9c87b322020-05-11 11:56:44 -0700164 ///
165 /// If [scheme] is null, xcodebuild will return build settings for the first discovered
166 /// target (by default this is Runner).
Zachary Anderson8a33d242019-09-16 07:51:50 -0700167 Future<Map<String, String>> getBuildSettings(
Jenn Magder9c87b322020-05-11 11:56:44 -0700168 String projectPath, {
Jenn Magderdb3f49b2021-04-27 17:20:11 -0700169 required XcodeProjectBuildContext buildContext,
Zachary Andersonb7c714e2019-08-28 10:03:53 -0700170 Duration timeout = const Duration(minutes: 1),
171 }) async {
Jonah Williams4807f802021-03-16 16:46:21 -0700172 final Status status = _logger.startSpinner();
Jenn Magderdb3f49b2021-04-27 17:20:11 -0700173 final String? scheme = buildContext.scheme;
174 final String? configuration = buildContext.configuration;
Zachary Anderson0f2af972019-09-05 09:50:46 -0700175 final List<String> showBuildSettingsCommand = <String>[
Jenn Magder2f3cccc2020-11-04 19:54:02 -0800176 ...xcrunCommand(),
177 'xcodebuild',
Zachary Anderson0f2af972019-09-05 09:50:46 -0700178 '-project',
Jonah Williamsef15eac2020-01-23 15:03:04 -0800179 _fileSystem.path.absolute(projectPath),
Jenn Magder9c87b322020-05-11 11:56:44 -0700180 if (scheme != null)
181 ...<String>['-scheme', scheme],
Jenn Magderdb3f49b2021-04-27 17:20:11 -0700182 if (configuration != null)
183 ...<String>['-configuration', configuration],
184 if (buildContext.environmentType == EnvironmentType.simulator)
185 ...<String>['-sdk', 'iphonesimulator'],
Zachary Anderson0f2af972019-09-05 09:50:46 -0700186 '-showBuildSettings',
Jenn Magderdb3f49b2021-04-27 17:20:11 -0700187 'BUILD_DIR=${_fileSystem.path.absolute(getIosBuildDirectory())}',
Jonah Williamsef15eac2020-01-23 15:03:04 -0800188 ...environmentVariablesAsXcodeBuildSettings(_platform)
Zachary Anderson0f2af972019-09-05 09:50:46 -0700189 ];
Zachary Andersonb7c714e2019-08-28 10:03:53 -0700190 try {
Jenn Magder01dc19b2019-10-28 19:58:06 -0700191 // showBuildSettings is reported to occasionally timeout. Here, we give it
Zachary Andersonb7c714e2019-08-28 10:03:53 -0700192 // a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
193 // When there is a timeout, we retry once.
Jonah Williamsef15eac2020-01-23 15:03:04 -0800194 final RunResult result = await _processUtils.run(
Zachary Anderson0f2af972019-09-05 09:50:46 -0700195 showBuildSettingsCommand,
Zachary Anderson73c10e82019-09-11 18:20:42 -0700196 throwOnError: true,
Zachary Andersonb7c714e2019-08-28 10:03:53 -0700197 workingDirectory: projectPath,
198 timeout: timeout,
199 timeoutRetries: 1,
200 );
201 final String out = result.stdout.trim();
202 return parseXcodeBuildSettings(out);
Zachary Anderson6c408a02020-03-06 10:22:12 -0800203 } on Exception catch (error) {
Zachary Anderson0f2af972019-09-05 09:50:46 -0700204 if (error is ProcessException && error.toString().contains('timed out')) {
205 BuildEvent('xcode-show-build-settings-timeout',
Jenn Magder23404df2021-04-12 16:45:03 -0700206 type: 'ios',
Zachary Anderson0f2af972019-09-05 09:50:46 -0700207 command: showBuildSettingsCommand.join(' '),
Jenn Magder77ea8482020-04-02 10:57:37 -0700208 flutterUsage: _usage,
Zachary Anderson0f2af972019-09-05 09:50:46 -0700209 ).send();
210 }
Jenn Magderdb3f49b2021-04-27 17:20:11 -0700211 _logger.printTrace('Unexpected failure to get Xcode build settings: $error.');
Zachary Andersonb7c714e2019-08-28 10:03:53 -0700212 return const <String, String>{};
213 } finally {
214 status.stop();
215 }
216 }
217
Jenn Magdera61bff22020-03-19 09:51:01 -0700218 Future<void> cleanWorkspace(String workspacePath, String scheme, { bool verbose = false }) async {
Jenn Magder4fb9ce82020-02-27 12:18:06 -0800219 await _processUtils.run(<String>[
Jenn Magder2f3cccc2020-11-04 19:54:02 -0800220 ...xcrunCommand(),
221 'xcodebuild',
Jenn Magder892d62f2019-08-21 18:42:56 -0700222 '-workspace',
223 workspacePath,
224 '-scheme',
225 scheme,
Jenn Magdera61bff22020-03-19 09:51:01 -0700226 if (!verbose)
227 '-quiet',
Alexandre Ardhuin89427d62019-09-24 08:06:09 +0200228 'clean',
Jonah Williamsef15eac2020-01-23 15:03:04 -0800229 ...environmentVariablesAsXcodeBuildSettings(_platform)
230 ], workingDirectory: _fileSystem.currentDirectory.path);
Jenn Magder892d62f2019-08-21 18:42:56 -0700231 }
232
Jenn Magder72976f52021-04-16 17:22:46 -0700233 Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async {
stuartmorgan72888c72019-09-12 16:03:02 -0700234 // The exit code returned by 'xcodebuild -list' when either:
235 // * -project is passed and the given project isn't there, or
236 // * no -project is passed and there isn't a project.
237 const int missingProjectExitCode = 66;
Jenn Magder8334fb02021-05-13 21:34:04 -0700238 // The exit code returned by 'xcodebuild -list' when the project is corrupted.
239 const int corruptedProjectExitCode = 74;
240 bool _allowedFailures(int c) => c == missingProjectExitCode || c == corruptedProjectExitCode;
Jonah Williamsef15eac2020-01-23 15:03:04 -0800241 final RunResult result = await _processUtils.run(
Zachary Anderson73c10e82019-09-11 18:20:42 -0700242 <String>[
Jenn Magder2f3cccc2020-11-04 19:54:02 -0800243 ...xcrunCommand(),
244 'xcodebuild',
Zachary Anderson73c10e82019-09-11 18:20:42 -0700245 '-list',
246 if (projectFilename != null) ...<String>['-project', projectFilename],
247 ],
248 throwOnError: true,
Jenn Magder8334fb02021-05-13 21:34:04 -0700249 allowedFailures: _allowedFailures,
Zachary Anderson73c10e82019-09-11 18:20:42 -0700250 workingDirectory: projectPath,
251 );
Jenn Magder8334fb02021-05-13 21:34:04 -0700252 if (_allowedFailures(result.exitCode)) {
253 // User configuration error, tool exit instead of crashing.
stuartmorgan72888c72019-09-12 16:03:02 -0700254 throwToolExit('Unable to get Xcode project information:\n ${result.stderr}');
255 }
Jenn Magdere110ca72020-07-09 16:56:02 -0700256 return XcodeProjectInfo.fromXcodeBuildOutput(result.toString(), _logger);
Mikkel Nygaard Ravn20004352018-02-16 10:17:28 +0100257 }
xsterd401bd72018-02-13 01:56:13 -0800258}
259
Jenn Magder01dc19b2019-10-28 19:58:06 -0700260/// Environment variables prefixed by FLUTTER_XCODE_ will be passed as build configurations to xcodebuild.
261/// This allows developers to pass arbitrary build settings in without the tool needing to make a flag
262/// for or be aware of each one. This could be used to set code signing build settings in a CI
263/// environment without requiring settings changes in the Xcode project.
Jonah Williamsef15eac2020-01-23 15:03:04 -0800264List<String> environmentVariablesAsXcodeBuildSettings(Platform platform) {
Jenn Magder01dc19b2019-10-28 19:58:06 -0700265 const String xcodeBuildSettingPrefix = 'FLUTTER_XCODE_';
Jonah Williamsef15eac2020-01-23 15:03:04 -0800266 return platform.environment.entries.where((MapEntry<String, String> mapEntry) {
Jenn Magder01dc19b2019-10-28 19:58:06 -0700267 return mapEntry.key.startsWith(xcodeBuildSettingPrefix);
268 }).expand<String>((MapEntry<String, String> mapEntry) {
269 // Remove FLUTTER_XCODE_ prefix from the environment variable to get the build setting.
270 final String trimmedBuildSettingKey = mapEntry.key.substring(xcodeBuildSettingPrefix.length);
271 return <String>['$trimmedBuildSettingKey=${mapEntry.value}'];
272 }).toList();
273}
274
xsterd401bd72018-02-13 01:56:13 -0800275Map<String, String> parseXcodeBuildSettings(String showBuildSettingsOutput) {
Chris Bracken7a093162017-03-03 17:50:46 -0800276 final Map<String, String> settings = <String, String>{};
Jenn Magder72976f52021-04-16 17:22:46 -0700277 for (final Match? match in showBuildSettingsOutput.split('\n').map<Match?>(_settingExpr.firstMatch)) {
Mikkel Nygaard Ravn8507b722018-02-13 22:19:03 +0100278 if (match != null) {
Jenn Magder72976f52021-04-16 17:22:46 -0700279 settings[match[1]!] = match[2]!;
Mikkel Nygaard Ravn8507b722018-02-13 22:19:03 +0100280 }
Chris Brackena45e4e92016-09-26 13:25:57 -0700281 }
282 return settings;
283}
284
Chris Brackena45e4e92016-09-26 13:25:57 -0700285/// Substitutes variables in [str] with their values from the specified Xcode
286/// project and target.
xsterb232a842017-05-15 12:54:32 -0700287String substituteXcodeVariables(String str, Map<String, String> xcodeBuildSettings) {
Chris Bracken7a093162017-03-03 17:50:46 -0800288 final Iterable<Match> matches = _varExpr.allMatches(str);
Zachary Andersone2340c62019-09-13 14:51:35 -0700289 if (matches.isEmpty) {
Chris Brackena45e4e92016-09-26 13:25:57 -0700290 return str;
Zachary Andersone2340c62019-09-13 14:51:35 -0700291 }
Chris Brackena45e4e92016-09-26 13:25:57 -0700292
Jenn Magder72976f52021-04-16 17:22:46 -0700293 return str.replaceAllMapped(_varExpr, (Match m) => xcodeBuildSettings[m[1]!] ?? m[0]!);
Chris Brackena45e4e92016-09-26 13:25:57 -0700294}
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200295
Jenn Magderdb3f49b2021-04-27 17:20:11 -0700296@immutable
297class XcodeProjectBuildContext {
298 const XcodeProjectBuildContext({this.scheme, this.configuration, this.environmentType = EnvironmentType.physical});
299
300 final String? scheme;
301 final String? configuration;
302 final EnvironmentType environmentType;
303
304 @override
Dan Fielde36e62e2021-06-30 09:46:54 -0700305 int get hashCode => Object.hash(scheme, configuration, environmentType);
Jenn Magderdb3f49b2021-04-27 17:20:11 -0700306
307 @override
308 bool operator ==(Object other) {
309 if (identical(other, this)) {
310 return true;
311 }
312 return other is XcodeProjectBuildContext &&
313 other.scheme == scheme &&
314 other.configuration == configuration &&
315 other.environmentType == environmentType;
316 }
317}
318
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200319/// Information about an Xcode project.
320///
321/// Represents the output of `xcodebuild -list`.
322class XcodeProjectInfo {
Jenn Magdere110ca72020-07-09 16:56:02 -0700323 XcodeProjectInfo(
324 this.targets,
325 this.buildConfigurations,
326 this.schemes,
327 Logger logger
328 ) : _logger = logger;
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200329
Jenn Magdere110ca72020-07-09 16:56:02 -0700330 factory XcodeProjectInfo.fromXcodeBuildOutput(String output, Logger logger) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200331 final List<String> targets = <String>[];
332 final List<String> buildConfigurations = <String>[];
333 final List<String> schemes = <String>[];
Jenn Magder72976f52021-04-16 17:22:46 -0700334 List<String>? collector;
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100335 for (final String line in output.split('\n')) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200336 if (line.isEmpty) {
337 collector = null;
338 continue;
339 } else if (line.endsWith('Targets:')) {
340 collector = targets;
341 continue;
342 } else if (line.endsWith('Build Configurations:')) {
343 collector = buildConfigurations;
344 continue;
345 } else if (line.endsWith('Schemes:')) {
346 collector = schemes;
347 continue;
348 }
349 collector?.add(line.trim());
350 }
Zachary Andersone2340c62019-09-13 14:51:35 -0700351 if (schemes.isEmpty) {
Mikkel Nygaard Ravn32ab3db2017-08-23 11:58:21 +0200352 schemes.add('Runner');
Zachary Andersone2340c62019-09-13 14:51:35 -0700353 }
Jenn Magdere110ca72020-07-09 16:56:02 -0700354 return XcodeProjectInfo(targets, buildConfigurations, schemes, logger);
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200355 }
356
357 final List<String> targets;
358 final List<String> buildConfigurations;
359 final List<String> schemes;
Jenn Magdere110ca72020-07-09 16:56:02 -0700360 final Logger _logger;
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200361
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200362 bool get definesCustomSchemes => !(schemes.contains('Runner') && schemes.length == 1);
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200363
364 /// The expected scheme for [buildInfo].
Jenn Magdere110ca72020-07-09 16:56:02 -0700365 @visibleForTesting
Jenn Magder72976f52021-04-16 17:22:46 -0700366 static String expectedSchemeFor(BuildInfo? buildInfo) {
Jenn Magder9c87b322020-05-11 11:56:44 -0700367 return toTitleCase(buildInfo?.flavor ?? 'runner');
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200368 }
369
370 /// The expected build configuration for [buildInfo] and [scheme].
371 static String expectedBuildConfigurationFor(BuildInfo buildInfo, String scheme) {
372 final String baseConfiguration = _baseConfigurationFor(buildInfo);
Zachary Andersone2340c62019-09-13 14:51:35 -0700373 if (buildInfo.flavor == null) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200374 return baseConfiguration;
Zachary Andersone2340c62019-09-13 14:51:35 -0700375 }
Alexandre Ardhuin34059ee2021-06-01 20:14:06 +0200376 return '$baseConfiguration-$scheme';
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200377 }
378
Dan Fieldfd6b2e12018-10-31 16:37:53 -0700379 /// Checks whether the [buildConfigurations] contains the specified string, without
380 /// regard to case.
Jonah Williams08576cb2020-10-12 09:31:02 -0700381 bool hasBuildConfigurationForBuildMode(String buildMode) {
Dan Fieldfd6b2e12018-10-31 16:37:53 -0700382 buildMode = buildMode.toLowerCase();
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100383 for (final String name in buildConfigurations) {
Dan Fieldfd6b2e12018-10-31 16:37:53 -0700384 if (name.toLowerCase() == buildMode) {
385 return true;
386 }
387 }
388 return false;
389 }
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200390 /// Returns unique scheme matching [buildInfo], or null, if there is no unique
391 /// best match.
Jenn Magdercbc7ce02021-05-26 16:04:03 -0700392 String? schemeFor(BuildInfo? buildInfo) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200393 final String expectedScheme = expectedSchemeFor(buildInfo);
Zachary Andersone2340c62019-09-13 14:51:35 -0700394 if (schemes.contains(expectedScheme)) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200395 return expectedScheme;
Zachary Andersone2340c62019-09-13 14:51:35 -0700396 }
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200397 return _uniqueMatch(schemes, (String candidate) {
398 return candidate.toLowerCase() == expectedScheme.toLowerCase();
399 });
400 }
401
Jenn Magderdb3f49b2021-04-27 17:20:11 -0700402 Never reportFlavorNotFoundAndExit() {
Jenn Magdere110ca72020-07-09 16:56:02 -0700403 _logger.printError('');
404 if (definesCustomSchemes) {
405 _logger.printError('The Xcode project defines schemes: ${schemes.join(', ')}');
406 throwToolExit('You must specify a --flavor option to select one of the available schemes.');
407 } else {
408 throwToolExit('The Xcode project does not define custom schemes. You cannot use the --flavor option.');
409 }
410 }
411
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200412 /// Returns unique build configuration matching [buildInfo] and [scheme], or
413 /// null, if there is no unique best match.
Jenn Magderdb3f49b2021-04-27 17:20:11 -0700414 String? buildConfigurationFor(BuildInfo? buildInfo, String scheme) {
415 if (buildInfo == null) {
416 return null;
417 }
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200418 final String expectedConfiguration = expectedBuildConfigurationFor(buildInfo, scheme);
Jonah Williams08576cb2020-10-12 09:31:02 -0700419 if (hasBuildConfigurationForBuildMode(expectedConfiguration)) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200420 return expectedConfiguration;
Zachary Andersone2340c62019-09-13 14:51:35 -0700421 }
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200422 final String baseConfiguration = _baseConfigurationFor(buildInfo);
423 return _uniqueMatch(buildConfigurations, (String candidate) {
424 candidate = candidate.toLowerCase();
Zachary Andersone2340c62019-09-13 14:51:35 -0700425 if (buildInfo.flavor == null) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200426 return candidate == expectedConfiguration.toLowerCase();
Zachary Andersone2340c62019-09-13 14:51:35 -0700427 }
428 return candidate.contains(baseConfiguration.toLowerCase()) && candidate.contains(scheme.toLowerCase());
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200429 });
430 }
431
Dan Fieldfd6b2e12018-10-31 16:37:53 -0700432 static String _baseConfigurationFor(BuildInfo buildInfo) {
Zachary Andersone2340c62019-09-13 14:51:35 -0700433 if (buildInfo.isDebug) {
Dan Fieldfd6b2e12018-10-31 16:37:53 -0700434 return 'Debug';
Zachary Andersone2340c62019-09-13 14:51:35 -0700435 }
436 if (buildInfo.isProfile) {
Dan Fieldfd6b2e12018-10-31 16:37:53 -0700437 return 'Profile';
Zachary Andersone2340c62019-09-13 14:51:35 -0700438 }
Dan Fieldfd6b2e12018-10-31 16:37:53 -0700439 return 'Release';
440 }
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200441
Jenn Magder72976f52021-04-16 17:22:46 -0700442 static String? _uniqueMatch(Iterable<String> strings, bool Function(String s) matches) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200443 final List<String> options = strings.where(matches).toList();
Zachary Andersone2340c62019-09-13 14:51:35 -0700444 if (options.length == 1) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200445 return options.first;
Zachary Andersone2340c62019-09-13 14:51:35 -0700446 }
447 return null;
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200448 }
449
450 @override
451 String toString() {
452 return 'XcodeProjectInfo($targets, $buildConfigurations, $schemes)';
453 }
454}