• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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