// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'package:meta/meta.dart'; import '../artifacts.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/platform.dart'; import '../base/process.dart'; import '../base/process_manager.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; import '../globals.dart'; import '../project.dart'; final RegExp _settingExpr = new RegExp(r'(\w+)\s*=\s*(.*)$'); final RegExp _varExpr = new RegExp(r'\$\((.*)\)'); String flutterFrameworkDir(BuildMode mode) { return fs.path.normalize(fs.path.dirname(artifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, mode))); } /// Writes or rewrites Xcode property files with the specified information. /// /// targetOverride: Optional parameter, if null or unspecified the default value /// from xcode_backend.sh is used 'lib/main.dart'. Future<void> updateGeneratedXcodeProperties({ @required FlutterProject project, @required BuildInfo buildInfo, String targetOverride, @required bool previewDart2, }) async { final StringBuffer localsBuffer = new StringBuffer(); localsBuffer.writeln('// This is a generated file; do not edit or check into version control.'); final String flutterRoot = fs.path.normalize(Cache.flutterRoot); localsBuffer.writeln('FLUTTER_ROOT=$flutterRoot'); // This holds because requiresProjectRoot is true for this command localsBuffer.writeln('FLUTTER_APPLICATION_PATH=${fs.path.normalize(project.directory.path)}'); // Relative to FLUTTER_APPLICATION_PATH, which is [Directory.current]. if (targetOverride != null) localsBuffer.writeln('FLUTTER_TARGET=$targetOverride'); // The runtime mode for the current build. localsBuffer.writeln('FLUTTER_BUILD_MODE=${buildInfo.modeName}'); // The build outputs directory, relative to FLUTTER_APPLICATION_PATH. localsBuffer.writeln('FLUTTER_BUILD_DIR=${getBuildDirectory()}'); localsBuffer.writeln('SYMROOT=\${SOURCE_ROOT}/../${getIosBuildDirectory()}'); if (!project.isModule) { // For module projects we do not want to write the FLUTTER_FRAMEWORK_DIR // explicitly. Rather we rely on the xcode backend script and the Podfile // logic to derive it from FLUTTER_ROOT and FLUTTER_BUILD_MODE. localsBuffer.writeln('FLUTTER_FRAMEWORK_DIR=${flutterFrameworkDir(buildInfo.mode)}'); } final String buildName = buildInfo?.buildName ?? project.manifest.buildName; if (buildName != null) { localsBuffer.writeln('FLUTTER_BUILD_NAME=$buildName'); } final int buildNumber = buildInfo?.buildNumber ?? project.manifest.buildNumber; if (buildNumber != null) { localsBuffer.writeln('FLUTTER_BUILD_NUMBER=$buildNumber'); } if (artifacts is LocalEngineArtifacts) { final LocalEngineArtifacts localEngineArtifacts = artifacts; localsBuffer.writeln('LOCAL_ENGINE=${localEngineArtifacts.engineOutPath}'); // Tell Xcode not to build universal binaries for local engines, which are // single-architecture. // // NOTE: this assumes that local engine binary paths are consistent with // the conventions uses in the engine: 32-bit iOS engines are built to // paths ending in _arm, 64-bit builds are not. final String arch = localEngineArtifacts.engineOutPath.endsWith('_arm') ? 'armv7' : 'arm64'; localsBuffer.writeln('ARCHS=$arch'); } if (previewDart2) { localsBuffer.writeln('PREVIEW_DART_2=true'); } if (buildInfo.trackWidgetCreation) { localsBuffer.writeln('TRACK_WIDGET_CREATION=true'); } final File generatedXcodePropertiesFile = project.ios.generatedXcodePropertiesFile; generatedXcodePropertiesFile.createSync(recursive: true); generatedXcodePropertiesFile.writeAsStringSync(localsBuffer.toString()); } XcodeProjectInterpreter get xcodeProjectInterpreter => context[XcodeProjectInterpreter]; /// Interpreter of Xcode projects. class XcodeProjectInterpreter { static const String _executable = '/usr/bin/xcodebuild'; static final RegExp _versionRegex = new RegExp(r'Xcode ([0-9.]+)'); void _updateVersion() { if (!platform.isMacOS || !fs.file(_executable).existsSync()) { return; } try { final ProcessResult result = processManager.runSync(<String>[_executable, '-version']); if (result.exitCode != 0) { return; } _versionText = result.stdout.trim().replaceAll('\n', ', '); final Match match = _versionRegex.firstMatch(versionText); if (match == null) return; final String version = match.group(1); final List<String> components = version.split('.'); _majorVersion = int.parse(components[0]); _minorVersion = components.length == 1 ? 0 : int.parse(components[1]); } on ProcessException { // Ignore: leave values null. } } bool get isInstalled => majorVersion != null; String _versionText; String get versionText { if (_versionText == null) _updateVersion(); return _versionText; } int _majorVersion; int get majorVersion { if (_majorVersion == null) _updateVersion(); return _majorVersion; } int _minorVersion; int get minorVersion { if (_minorVersion == null) _updateVersion(); return _minorVersion; } Map<String, String> getBuildSettings(String projectPath, String target) { final String out = runCheckedSync(<String>[ _executable, '-project', fs.path.absolute(projectPath), '-target', target, '-showBuildSettings' ], workingDirectory: projectPath); return parseXcodeBuildSettings(out); } XcodeProjectInfo getInfo(String projectPath) { final String out = runCheckedSync(<String>[ _executable, '-list', ], workingDirectory: projectPath); return new XcodeProjectInfo.fromXcodeBuildOutput(out); } } Map<String, String> parseXcodeBuildSettings(String showBuildSettingsOutput) { final Map<String, String> settings = <String, String>{}; for (Match match in showBuildSettingsOutput.split('\n').map(_settingExpr.firstMatch)) { if (match != null) { settings[match[1]] = match[2]; } } return settings; } /// Substitutes variables in [str] with their values from the specified Xcode /// project and target. String substituteXcodeVariables(String str, Map<String, String> xcodeBuildSettings) { final Iterable<Match> matches = _varExpr.allMatches(str); if (matches.isEmpty) return str; return str.replaceAllMapped(_varExpr, (Match m) => xcodeBuildSettings[m[1]] ?? m[0]); } /// Information about an Xcode project. /// /// Represents the output of `xcodebuild -list`. class XcodeProjectInfo { XcodeProjectInfo(this.targets, this.buildConfigurations, this.schemes); factory XcodeProjectInfo.fromXcodeBuildOutput(String output) { final List<String> targets = <String>[]; final List<String> buildConfigurations = <String>[]; final List<String> schemes = <String>[]; List<String> collector; for (String line in output.split('\n')) { if (line.isEmpty) { collector = null; continue; } else if (line.endsWith('Targets:')) { collector = targets; continue; } else if (line.endsWith('Build Configurations:')) { collector = buildConfigurations; continue; } else if (line.endsWith('Schemes:')) { collector = schemes; continue; } collector?.add(line.trim()); } if (schemes.isEmpty) schemes.add('Runner'); return new XcodeProjectInfo(targets, buildConfigurations, schemes); } final List<String> targets; final List<String> buildConfigurations; final List<String> schemes; bool get definesCustomTargets => !(targets.contains('Runner') && targets.length == 1); bool get definesCustomSchemes => !(schemes.contains('Runner') && schemes.length == 1); bool get definesCustomBuildConfigurations { return !(buildConfigurations.contains('Debug') && buildConfigurations.contains('Release') && buildConfigurations.length == 2); } /// The expected scheme for [buildInfo]. static String expectedSchemeFor(BuildInfo buildInfo) { return toTitleCase(buildInfo.flavor ?? 'runner'); } /// The expected build configuration for [buildInfo] and [scheme]. static String expectedBuildConfigurationFor(BuildInfo buildInfo, String scheme) { final String baseConfiguration = _baseConfigurationFor(buildInfo); if (buildInfo.flavor == null) return baseConfiguration; else return baseConfiguration + '-$scheme'; } /// Returns unique scheme matching [buildInfo], or null, if there is no unique /// best match. String schemeFor(BuildInfo buildInfo) { final String expectedScheme = expectedSchemeFor(buildInfo); if (schemes.contains(expectedScheme)) return expectedScheme; return _uniqueMatch(schemes, (String candidate) { return candidate.toLowerCase() == expectedScheme.toLowerCase(); }); } /// Returns unique build configuration matching [buildInfo] and [scheme], or /// null, if there is no unique best match. String buildConfigurationFor(BuildInfo buildInfo, String scheme) { final String expectedConfiguration = expectedBuildConfigurationFor(buildInfo, scheme); if (buildConfigurations.contains(expectedConfiguration)) return expectedConfiguration; final String baseConfiguration = _baseConfigurationFor(buildInfo); return _uniqueMatch(buildConfigurations, (String candidate) { candidate = candidate.toLowerCase(); if (buildInfo.flavor == null) return candidate == expectedConfiguration.toLowerCase(); else return candidate.contains(baseConfiguration.toLowerCase()) && candidate.contains(scheme.toLowerCase()); }); } static String _baseConfigurationFor(BuildInfo buildInfo) => buildInfo.isDebug ? 'Debug' : 'Release'; static String _uniqueMatch(Iterable<String> strings, bool matches(String s)) { final List<String> options = strings.where(matches).toList(); if (options.length == 1) return options.first; else return null; } @override String toString() { return 'XcodeProjectInfo($targets, $buildConfigurations, $schemes)'; } }