1// Copyright 2016 The Chromium 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 5import 'dart:async'; 6 7import 'package:meta/meta.dart'; 8 9import '../artifacts.dart'; 10import '../base/context.dart'; 11import '../base/file_system.dart'; 12import '../base/io.dart'; 13import '../base/os.dart'; 14import '../base/platform.dart'; 15import '../base/process.dart'; 16import '../base/process_manager.dart'; 17import '../base/utils.dart'; 18import '../build_info.dart'; 19import '../cache.dart'; 20import '../globals.dart'; 21import '../project.dart'; 22 23final RegExp _settingExpr = RegExp(r'(\w+)\s*=\s*(.*)$'); 24final RegExp _varExpr = RegExp(r'\$\(([^)]*)\)'); 25 26String flutterFrameworkDir(BuildMode mode) { 27 return fs.path.normalize(fs.path.dirname(artifacts.getArtifactPath( 28 Artifact.flutterFramework, platform: TargetPlatform.ios, mode: mode))); 29} 30 31String flutterMacOSFrameworkDir(BuildMode mode) { 32 return fs.path.normalize(fs.path.dirname(artifacts.getArtifactPath( 33 Artifact.flutterMacOSFramework, platform: TargetPlatform.darwin_x64, mode: mode))); 34} 35 36/// Writes or rewrites Xcode property files with the specified information. 37/// 38/// useMacOSConfig: Optional parameter that controls whether we use the macOS 39/// project file instead. Defaults to false. 40/// 41/// setSymroot: Optional parameter to control whether to set SYMROOT. 42/// 43/// targetOverride: Optional parameter, if null or unspecified the default value 44/// from xcode_backend.sh is used 'lib/main.dart'. 45Future<void> updateGeneratedXcodeProperties({ 46 @required FlutterProject project, 47 @required BuildInfo buildInfo, 48 String targetOverride, 49 bool useMacOSConfig = false, 50 bool setSymroot = true, 51 String buildDirOverride, 52}) async { 53 final List<String> xcodeBuildSettings = _xcodeBuildSettingsLines( 54 project: project, 55 buildInfo: buildInfo, 56 targetOverride: targetOverride, 57 useMacOSConfig: useMacOSConfig, 58 setSymroot: setSymroot, 59 buildDirOverride: buildDirOverride 60 ); 61 62 _updateGeneratedXcodePropertiesFile( 63 project: project, 64 xcodeBuildSettings: xcodeBuildSettings, 65 useMacOSConfig: useMacOSConfig, 66 ); 67 68 _updateGeneratedEnvironmentVariablesScript( 69 project: project, 70 xcodeBuildSettings: xcodeBuildSettings, 71 useMacOSConfig: useMacOSConfig, 72 ); 73} 74 75/// Generate a xcconfig file to inherit FLUTTER_ build settings 76/// for Xcode targets that need them. 77/// See [XcodeBasedProject.generatedXcodePropertiesFile]. 78void _updateGeneratedXcodePropertiesFile({ 79 @required FlutterProject project, 80 @required List<String> xcodeBuildSettings, 81 bool useMacOSConfig = false, 82}) { 83 final StringBuffer localsBuffer = StringBuffer(); 84 85 localsBuffer.writeln('// This is a generated file; do not edit or check into version control.'); 86 xcodeBuildSettings.forEach(localsBuffer.writeln); 87 final File generatedXcodePropertiesFile = useMacOSConfig 88 ? project.macos.generatedXcodePropertiesFile 89 : project.ios.generatedXcodePropertiesFile; 90 91 generatedXcodePropertiesFile.createSync(recursive: true); 92 generatedXcodePropertiesFile.writeAsStringSync(localsBuffer.toString()); 93} 94 95/// Generate a script to export all the FLUTTER_ environment variables needed 96/// as flags for Flutter tools. 97/// See [XcodeBasedProject.generatedEnvironmentVariableExportScript]. 98void _updateGeneratedEnvironmentVariablesScript({ 99 @required FlutterProject project, 100 @required List<String> xcodeBuildSettings, 101 bool useMacOSConfig = false, 102}) { 103 final StringBuffer localsBuffer = StringBuffer(); 104 105 localsBuffer.writeln('#!/bin/sh'); 106 localsBuffer.writeln('# This is a generated file; do not edit or check into version control.'); 107 for (String line in xcodeBuildSettings) { 108 localsBuffer.writeln('export "$line"'); 109 } 110 111 final File generatedModuleBuildPhaseScript = useMacOSConfig 112 ? project.macos.generatedEnvironmentVariableExportScript 113 : project.ios.generatedEnvironmentVariableExportScript; 114 generatedModuleBuildPhaseScript.createSync(recursive: true); 115 generatedModuleBuildPhaseScript.writeAsStringSync(localsBuffer.toString()); 116 os.chmod(generatedModuleBuildPhaseScript, '755'); 117} 118 119/// List of lines of build settings. Example: 'FLUTTER_BUILD_DIR=build' 120List<String> _xcodeBuildSettingsLines({ 121 @required FlutterProject project, 122 @required BuildInfo buildInfo, 123 String targetOverride, 124 bool useMacOSConfig = false, 125 bool setSymroot = true, 126 String buildDirOverride, 127}) { 128 final List<String> xcodeBuildSettings = <String>[]; 129 130 final String flutterRoot = fs.path.normalize(Cache.flutterRoot); 131 xcodeBuildSettings.add('FLUTTER_ROOT=$flutterRoot'); 132 133 // This holds because requiresProjectRoot is true for this command 134 xcodeBuildSettings.add('FLUTTER_APPLICATION_PATH=${fs.path.normalize(project.directory.path)}'); 135 136 // Relative to FLUTTER_APPLICATION_PATH, which is [Directory.current]. 137 if (targetOverride != null) 138 xcodeBuildSettings.add('FLUTTER_TARGET=$targetOverride'); 139 140 // The build outputs directory, relative to FLUTTER_APPLICATION_PATH. 141 xcodeBuildSettings.add('FLUTTER_BUILD_DIR=${buildDirOverride ?? getBuildDirectory()}'); 142 143 if (setSymroot) { 144 xcodeBuildSettings.add('SYMROOT=\${SOURCE_ROOT}/../${getIosBuildDirectory()}'); 145 } 146 147 if (!project.isModule) { 148 // For module projects we do not want to write the FLUTTER_FRAMEWORK_DIR 149 // explicitly. Rather we rely on the xcode backend script and the Podfile 150 // logic to derive it from FLUTTER_ROOT and FLUTTER_BUILD_MODE. 151 // However, this is necessary for regular projects using Cocoapods. 152 final String frameworkDir = useMacOSConfig 153 ? flutterMacOSFrameworkDir(buildInfo.mode) 154 : flutterFrameworkDir(buildInfo.mode); 155 xcodeBuildSettings.add('FLUTTER_FRAMEWORK_DIR=$frameworkDir'); 156 } 157 158 final String buildNameToParse = buildInfo?.buildName ?? project.manifest.buildName; 159 final String buildName = validatedBuildNameForPlatform(TargetPlatform.ios, buildNameToParse); 160 if (buildName != null) { 161 xcodeBuildSettings.add('FLUTTER_BUILD_NAME=$buildName'); 162 } 163 164 String buildNumber = validatedBuildNumberForPlatform(TargetPlatform.ios, buildInfo?.buildNumber ?? project.manifest.buildNumber); 165 // Drop back to parsing build name if build number is not present. Build number is optional in the manifest, but 166 // FLUTTER_BUILD_NUMBER is required as the backing value for the required CFBundleVersion. 167 buildNumber ??= validatedBuildNumberForPlatform(TargetPlatform.ios, buildNameToParse); 168 169 if (buildNumber != null) { 170 xcodeBuildSettings.add('FLUTTER_BUILD_NUMBER=$buildNumber'); 171 } 172 173 if (artifacts is LocalEngineArtifacts) { 174 final LocalEngineArtifacts localEngineArtifacts = artifacts; 175 final String engineOutPath = localEngineArtifacts.engineOutPath; 176 xcodeBuildSettings.add('FLUTTER_ENGINE=${fs.path.dirname(fs.path.dirname(engineOutPath))}'); 177 xcodeBuildSettings.add('LOCAL_ENGINE=${fs.path.basename(engineOutPath)}'); 178 179 // Tell Xcode not to build universal binaries for local engines, which are 180 // single-architecture. 181 // 182 // NOTE: this assumes that local engine binary paths are consistent with 183 // the conventions uses in the engine: 32-bit iOS engines are built to 184 // paths ending in _arm, 64-bit builds are not. 185 // 186 // Skip this step for macOS builds. 187 if (!useMacOSConfig) { 188 final String arch = engineOutPath.endsWith('_arm') ? 'armv7' : 'arm64'; 189 xcodeBuildSettings.add('ARCHS=$arch'); 190 } 191 } 192 193 if (buildInfo.trackWidgetCreation) { 194 xcodeBuildSettings.add('TRACK_WIDGET_CREATION=true'); 195 } 196 197 return xcodeBuildSettings; 198} 199 200XcodeProjectInterpreter get xcodeProjectInterpreter => context.get<XcodeProjectInterpreter>(); 201 202/// Interpreter of Xcode projects. 203class XcodeProjectInterpreter { 204 static const String _executable = '/usr/bin/xcodebuild'; 205 static final RegExp _versionRegex = RegExp(r'Xcode ([0-9.]+)'); 206 207 void _updateVersion() { 208 if (!platform.isMacOS || !fs.file(_executable).existsSync()) { 209 return; 210 } 211 try { 212 final ProcessResult result = processManager.runSync(<String>[_executable, '-version']); 213 if (result.exitCode != 0) { 214 return; 215 } 216 _versionText = result.stdout.trim().replaceAll('\n', ', '); 217 final Match match = _versionRegex.firstMatch(versionText); 218 if (match == null) 219 return; 220 final String version = match.group(1); 221 final List<String> components = version.split('.'); 222 _majorVersion = int.parse(components[0]); 223 _minorVersion = components.length == 1 ? 0 : int.parse(components[1]); 224 } on ProcessException { 225 // Ignored, leave values null. 226 } 227 } 228 229 bool get isInstalled => majorVersion != null; 230 231 String _versionText; 232 String get versionText { 233 if (_versionText == null) 234 _updateVersion(); 235 return _versionText; 236 } 237 238 int _majorVersion; 239 int get majorVersion { 240 if (_majorVersion == null) 241 _updateVersion(); 242 return _majorVersion; 243 } 244 245 int _minorVersion; 246 int get minorVersion { 247 if (_minorVersion == null) 248 _updateVersion(); 249 return _minorVersion; 250 } 251 252 Map<String, String> getBuildSettings(String projectPath, String target) { 253 try { 254 final String out = runCheckedSync(<String>[ 255 _executable, 256 '-project', 257 fs.path.absolute(projectPath), 258 '-target', 259 target, 260 '-showBuildSettings', 261 ], workingDirectory: projectPath); 262 return parseXcodeBuildSettings(out); 263 } catch(error) { 264 printTrace('Unexpected failure to get the build settings: $error.'); 265 return const <String, String>{}; 266 } 267 } 268 269 Future<XcodeProjectInfo> getInfo(String projectPath) async { 270 final RunResult result = await runCheckedAsync(<String>[ 271 _executable, '-list', 272 ], workingDirectory: projectPath); 273 return XcodeProjectInfo.fromXcodeBuildOutput(result.toString()); 274 } 275} 276 277Map<String, String> parseXcodeBuildSettings(String showBuildSettingsOutput) { 278 final Map<String, String> settings = <String, String>{}; 279 for (Match match in showBuildSettingsOutput.split('\n').map<Match>(_settingExpr.firstMatch)) { 280 if (match != null) { 281 settings[match[1]] = match[2]; 282 } 283 } 284 return settings; 285} 286 287/// Substitutes variables in [str] with their values from the specified Xcode 288/// project and target. 289String substituteXcodeVariables(String str, Map<String, String> xcodeBuildSettings) { 290 final Iterable<Match> matches = _varExpr.allMatches(str); 291 if (matches.isEmpty) 292 return str; 293 294 return str.replaceAllMapped(_varExpr, (Match m) => xcodeBuildSettings[m[1]] ?? m[0]); 295} 296 297/// Information about an Xcode project. 298/// 299/// Represents the output of `xcodebuild -list`. 300class XcodeProjectInfo { 301 XcodeProjectInfo(this.targets, this.buildConfigurations, this.schemes); 302 303 factory XcodeProjectInfo.fromXcodeBuildOutput(String output) { 304 final List<String> targets = <String>[]; 305 final List<String> buildConfigurations = <String>[]; 306 final List<String> schemes = <String>[]; 307 List<String> collector; 308 for (String line in output.split('\n')) { 309 if (line.isEmpty) { 310 collector = null; 311 continue; 312 } else if (line.endsWith('Targets:')) { 313 collector = targets; 314 continue; 315 } else if (line.endsWith('Build Configurations:')) { 316 collector = buildConfigurations; 317 continue; 318 } else if (line.endsWith('Schemes:')) { 319 collector = schemes; 320 continue; 321 } 322 collector?.add(line.trim()); 323 } 324 if (schemes.isEmpty) 325 schemes.add('Runner'); 326 return XcodeProjectInfo(targets, buildConfigurations, schemes); 327 } 328 329 final List<String> targets; 330 final List<String> buildConfigurations; 331 final List<String> schemes; 332 333 bool get definesCustomTargets => !(targets.contains('Runner') && targets.length == 1); 334 bool get definesCustomSchemes => !(schemes.contains('Runner') && schemes.length == 1); 335 bool get definesCustomBuildConfigurations { 336 return !(buildConfigurations.contains('Debug') && 337 buildConfigurations.contains('Release') && 338 buildConfigurations.length == 2); 339 } 340 341 /// The expected scheme for [buildInfo]. 342 static String expectedSchemeFor(BuildInfo buildInfo) { 343 return toTitleCase(buildInfo.flavor ?? 'runner'); 344 } 345 346 /// The expected build configuration for [buildInfo] and [scheme]. 347 static String expectedBuildConfigurationFor(BuildInfo buildInfo, String scheme) { 348 final String baseConfiguration = _baseConfigurationFor(buildInfo); 349 if (buildInfo.flavor == null) 350 return baseConfiguration; 351 else 352 return baseConfiguration + '-$scheme'; 353 } 354 355 /// Checks whether the [buildConfigurations] contains the specified string, without 356 /// regard to case. 357 bool hasBuildConfiguratinForBuildMode(String buildMode) { 358 buildMode = buildMode.toLowerCase(); 359 for (String name in buildConfigurations) { 360 if (name.toLowerCase() == buildMode) { 361 return true; 362 } 363 } 364 return false; 365 } 366 /// Returns unique scheme matching [buildInfo], or null, if there is no unique 367 /// best match. 368 String schemeFor(BuildInfo buildInfo) { 369 final String expectedScheme = expectedSchemeFor(buildInfo); 370 if (schemes.contains(expectedScheme)) 371 return expectedScheme; 372 return _uniqueMatch(schemes, (String candidate) { 373 return candidate.toLowerCase() == expectedScheme.toLowerCase(); 374 }); 375 } 376 377 /// Returns unique build configuration matching [buildInfo] and [scheme], or 378 /// null, if there is no unique best match. 379 String buildConfigurationFor(BuildInfo buildInfo, String scheme) { 380 final String expectedConfiguration = expectedBuildConfigurationFor(buildInfo, scheme); 381 if (hasBuildConfiguratinForBuildMode(expectedConfiguration)) 382 return expectedConfiguration; 383 final String baseConfiguration = _baseConfigurationFor(buildInfo); 384 return _uniqueMatch(buildConfigurations, (String candidate) { 385 candidate = candidate.toLowerCase(); 386 if (buildInfo.flavor == null) 387 return candidate == expectedConfiguration.toLowerCase(); 388 else 389 return candidate.contains(baseConfiguration.toLowerCase()) && candidate.contains(scheme.toLowerCase()); 390 }); 391 } 392 393 static String _baseConfigurationFor(BuildInfo buildInfo) { 394 if (buildInfo.isDebug) 395 return 'Debug'; 396 if (buildInfo.isProfile) 397 return 'Profile'; 398 return 'Release'; 399 } 400 401 static String _uniqueMatch(Iterable<String> strings, bool matches(String s)) { 402 final List<String> options = strings.where(matches).toList(); 403 if (options.length == 1) 404 return options.first; 405 else 406 return null; 407 } 408 409 @override 410 String toString() { 411 return 'XcodeProjectInfo($targets, $buildConfigurations, $schemes)'; 412 } 413} 414