mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
541 lines
19 KiB
Dart
541 lines
19 KiB
Dart
// Copyright 2014 The Flutter 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 'package:platform/platform.dart';
|
|
import 'package:process/process.dart';
|
|
|
|
import '../artifacts.dart';
|
|
import '../base/common.dart';
|
|
import '../base/context.dart';
|
|
import '../base/file_system.dart';
|
|
import '../base/io.dart';
|
|
import '../base/logger.dart';
|
|
import '../base/os.dart';
|
|
import '../base/process.dart';
|
|
import '../base/terminal.dart';
|
|
import '../base/utils.dart';
|
|
import '../build_info.dart';
|
|
import '../cache.dart';
|
|
import '../flutter_manifest.dart';
|
|
import '../globals.dart' as globals;
|
|
import '../project.dart';
|
|
import '../reporting/reporting.dart';
|
|
|
|
final RegExp _settingExpr = RegExp(r'(\w+)\s*=\s*(.*)$');
|
|
final RegExp _varExpr = RegExp(r'\$\(([^)]*)\)');
|
|
|
|
String flutterFrameworkDir(BuildMode mode) {
|
|
return globals.fs.path.normalize(globals.fs.path.dirname(globals.artifacts.getArtifactPath(
|
|
Artifact.flutterFramework, platform: TargetPlatform.ios, mode: mode)));
|
|
}
|
|
|
|
String flutterMacOSFrameworkDir(BuildMode mode) {
|
|
return globals.fs.path.normalize(globals.fs.path.dirname(globals.artifacts.getArtifactPath(
|
|
Artifact.flutterMacOSFramework, platform: TargetPlatform.darwin_x64, mode: mode)));
|
|
}
|
|
|
|
/// Writes or rewrites Xcode property files with the specified information.
|
|
///
|
|
/// useMacOSConfig: Optional parameter that controls whether we use the macOS
|
|
/// project file instead. Defaults to false.
|
|
///
|
|
/// setSymroot: Optional parameter to control whether to set SYMROOT.
|
|
///
|
|
/// 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,
|
|
bool useMacOSConfig = false,
|
|
bool setSymroot = true,
|
|
String buildDirOverride,
|
|
}) async {
|
|
final List<String> xcodeBuildSettings = _xcodeBuildSettingsLines(
|
|
project: project,
|
|
buildInfo: buildInfo,
|
|
targetOverride: targetOverride,
|
|
useMacOSConfig: useMacOSConfig,
|
|
setSymroot: setSymroot,
|
|
buildDirOverride: buildDirOverride,
|
|
);
|
|
|
|
_updateGeneratedXcodePropertiesFile(
|
|
project: project,
|
|
xcodeBuildSettings: xcodeBuildSettings,
|
|
useMacOSConfig: useMacOSConfig,
|
|
);
|
|
|
|
_updateGeneratedEnvironmentVariablesScript(
|
|
project: project,
|
|
xcodeBuildSettings: xcodeBuildSettings,
|
|
useMacOSConfig: useMacOSConfig,
|
|
);
|
|
}
|
|
|
|
/// Generate a xcconfig file to inherit FLUTTER_ build settings
|
|
/// for Xcode targets that need them.
|
|
/// See [XcodeBasedProject.generatedXcodePropertiesFile].
|
|
void _updateGeneratedXcodePropertiesFile({
|
|
@required FlutterProject project,
|
|
@required List<String> xcodeBuildSettings,
|
|
bool useMacOSConfig = false,
|
|
}) {
|
|
final StringBuffer localsBuffer = StringBuffer();
|
|
|
|
localsBuffer.writeln('// This is a generated file; do not edit or check into version control.');
|
|
xcodeBuildSettings.forEach(localsBuffer.writeln);
|
|
final File generatedXcodePropertiesFile = useMacOSConfig
|
|
? project.macos.generatedXcodePropertiesFile
|
|
: project.ios.generatedXcodePropertiesFile;
|
|
|
|
generatedXcodePropertiesFile.createSync(recursive: true);
|
|
generatedXcodePropertiesFile.writeAsStringSync(localsBuffer.toString());
|
|
}
|
|
|
|
/// Generate a script to export all the FLUTTER_ environment variables needed
|
|
/// as flags for Flutter tools.
|
|
/// See [XcodeBasedProject.generatedEnvironmentVariableExportScript].
|
|
void _updateGeneratedEnvironmentVariablesScript({
|
|
@required FlutterProject project,
|
|
@required List<String> xcodeBuildSettings,
|
|
bool useMacOSConfig = false,
|
|
}) {
|
|
final StringBuffer localsBuffer = StringBuffer();
|
|
|
|
localsBuffer.writeln('#!/bin/sh');
|
|
localsBuffer.writeln('# This is a generated file; do not edit or check into version control.');
|
|
for (final String line in xcodeBuildSettings) {
|
|
localsBuffer.writeln('export "$line"');
|
|
}
|
|
|
|
final File generatedModuleBuildPhaseScript = useMacOSConfig
|
|
? project.macos.generatedEnvironmentVariableExportScript
|
|
: project.ios.generatedEnvironmentVariableExportScript;
|
|
generatedModuleBuildPhaseScript.createSync(recursive: true);
|
|
generatedModuleBuildPhaseScript.writeAsStringSync(localsBuffer.toString());
|
|
os.chmod(generatedModuleBuildPhaseScript, '755');
|
|
}
|
|
|
|
/// Build name parsed and validated from build info and manifest. Used for CFBundleShortVersionString.
|
|
String parsedBuildName({
|
|
@required FlutterManifest manifest,
|
|
@required BuildInfo buildInfo,
|
|
}) {
|
|
final String buildNameToParse = buildInfo?.buildName ?? manifest.buildName;
|
|
return validatedBuildNameForPlatform(TargetPlatform.ios, buildNameToParse);
|
|
}
|
|
|
|
/// Build number parsed and validated from build info and manifest. Used for CFBundleVersion.
|
|
String parsedBuildNumber({
|
|
@required FlutterManifest manifest,
|
|
@required BuildInfo buildInfo,
|
|
}) {
|
|
String buildNumberToParse = buildInfo?.buildNumber ?? manifest.buildNumber;
|
|
final String buildNumber = validatedBuildNumberForPlatform(TargetPlatform.ios, buildNumberToParse);
|
|
if (buildNumber != null && buildNumber.isNotEmpty) {
|
|
return buildNumber;
|
|
}
|
|
// Drop back to parsing build name if build number is not present. Build number is optional in the manifest, but
|
|
// FLUTTER_BUILD_NUMBER is required as the backing value for the required CFBundleVersion.
|
|
buildNumberToParse = buildInfo?.buildName ?? manifest.buildName;
|
|
return validatedBuildNumberForPlatform(TargetPlatform.ios, buildNumberToParse);
|
|
}
|
|
|
|
/// List of lines of build settings. Example: 'FLUTTER_BUILD_DIR=build'
|
|
List<String> _xcodeBuildSettingsLines({
|
|
@required FlutterProject project,
|
|
@required BuildInfo buildInfo,
|
|
String targetOverride,
|
|
bool useMacOSConfig = false,
|
|
bool setSymroot = true,
|
|
String buildDirOverride,
|
|
}) {
|
|
final List<String> xcodeBuildSettings = <String>[];
|
|
|
|
final String flutterRoot = globals.fs.path.normalize(Cache.flutterRoot);
|
|
xcodeBuildSettings.add('FLUTTER_ROOT=$flutterRoot');
|
|
|
|
// This holds because requiresProjectRoot is true for this command
|
|
xcodeBuildSettings.add('FLUTTER_APPLICATION_PATH=${globals.fs.path.normalize(project.directory.path)}');
|
|
|
|
// Relative to FLUTTER_APPLICATION_PATH, which is [Directory.current].
|
|
if (targetOverride != null) {
|
|
xcodeBuildSettings.add('FLUTTER_TARGET=$targetOverride');
|
|
}
|
|
|
|
// The build outputs directory, relative to FLUTTER_APPLICATION_PATH.
|
|
xcodeBuildSettings.add('FLUTTER_BUILD_DIR=${buildDirOverride ?? getBuildDirectory()}');
|
|
|
|
if (setSymroot) {
|
|
xcodeBuildSettings.add('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.
|
|
// However, this is necessary for regular projects using Cocoapods.
|
|
final String frameworkDir = useMacOSConfig
|
|
? flutterMacOSFrameworkDir(buildInfo.mode)
|
|
: flutterFrameworkDir(buildInfo.mode);
|
|
xcodeBuildSettings.add('FLUTTER_FRAMEWORK_DIR=$frameworkDir');
|
|
}
|
|
|
|
|
|
final String buildName = parsedBuildName(manifest: project.manifest, buildInfo: buildInfo) ?? '1.0.0';
|
|
xcodeBuildSettings.add('FLUTTER_BUILD_NAME=$buildName');
|
|
|
|
final String buildNumber = parsedBuildNumber(manifest: project.manifest, buildInfo: buildInfo) ?? '1';
|
|
xcodeBuildSettings.add('FLUTTER_BUILD_NUMBER=$buildNumber');
|
|
|
|
if (globals.artifacts is LocalEngineArtifacts) {
|
|
final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts;
|
|
final String engineOutPath = localEngineArtifacts.engineOutPath;
|
|
xcodeBuildSettings.add('FLUTTER_ENGINE=${globals.fs.path.dirname(globals.fs.path.dirname(engineOutPath))}');
|
|
xcodeBuildSettings.add('LOCAL_ENGINE=${globals.fs.path.basename(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.
|
|
//
|
|
// Skip this step for macOS builds.
|
|
if (!useMacOSConfig) {
|
|
final String arch = engineOutPath.endsWith('_arm') ? 'armv7' : 'arm64';
|
|
xcodeBuildSettings.add('ARCHS=$arch');
|
|
}
|
|
}
|
|
|
|
if (buildInfo.trackWidgetCreation) {
|
|
xcodeBuildSettings.add('TRACK_WIDGET_CREATION=true');
|
|
}
|
|
|
|
return xcodeBuildSettings;
|
|
}
|
|
|
|
XcodeProjectInterpreter get xcodeProjectInterpreter => context.get<XcodeProjectInterpreter>();
|
|
|
|
/// Interpreter of Xcode projects.
|
|
class XcodeProjectInterpreter {
|
|
XcodeProjectInterpreter({
|
|
@required Platform platform,
|
|
@required ProcessManager processManager,
|
|
@required Logger logger,
|
|
@required FileSystem fileSystem,
|
|
@required AnsiTerminal terminal,
|
|
}) : _platform = platform,
|
|
_fileSystem = fileSystem,
|
|
_terminal = terminal,
|
|
_logger = logger,
|
|
_processUtils = ProcessUtils(logger: logger, processManager: processManager);
|
|
|
|
final Platform _platform;
|
|
final FileSystem _fileSystem;
|
|
final ProcessUtils _processUtils;
|
|
final AnsiTerminal _terminal;
|
|
final Logger _logger;
|
|
|
|
static const String _executable = '/usr/bin/xcodebuild';
|
|
static final RegExp _versionRegex = RegExp(r'Xcode ([0-9.]+)');
|
|
|
|
void _updateVersion() {
|
|
if (!_platform.isMacOS || !_fileSystem.file(_executable).existsSync()) {
|
|
return;
|
|
}
|
|
try {
|
|
final RunResult result = _processUtils.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 {
|
|
// Ignored, 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;
|
|
}
|
|
|
|
/// Asynchronously retrieve xcode build settings. This one is preferred for
|
|
/// new call-sites.
|
|
Future<Map<String, String>> getBuildSettings(
|
|
String projectPath,
|
|
String target, {
|
|
Duration timeout = const Duration(minutes: 1),
|
|
}) async {
|
|
final Status status = Status.withSpinner(
|
|
timeout: const TimeoutConfiguration().fastOperation,
|
|
timeoutConfiguration: const TimeoutConfiguration(),
|
|
platform: _platform,
|
|
stopwatch: Stopwatch(),
|
|
supportsColor: _terminal.supportsColor,
|
|
);
|
|
final List<String> showBuildSettingsCommand = <String>[
|
|
_executable,
|
|
'-project',
|
|
_fileSystem.path.absolute(projectPath),
|
|
'-target',
|
|
target,
|
|
'-showBuildSettings',
|
|
...environmentVariablesAsXcodeBuildSettings(_platform)
|
|
];
|
|
try {
|
|
// showBuildSettings is reported to occasionally timeout. Here, we give it
|
|
// a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
|
|
// When there is a timeout, we retry once.
|
|
final RunResult result = await _processUtils.run(
|
|
showBuildSettingsCommand,
|
|
throwOnError: true,
|
|
workingDirectory: projectPath,
|
|
timeout: timeout,
|
|
timeoutRetries: 1,
|
|
);
|
|
final String out = result.stdout.trim();
|
|
return parseXcodeBuildSettings(out);
|
|
} catch(error) {
|
|
if (error is ProcessException && error.toString().contains('timed out')) {
|
|
BuildEvent('xcode-show-build-settings-timeout',
|
|
command: showBuildSettingsCommand.join(' '),
|
|
).send();
|
|
}
|
|
_logger.printTrace('Unexpected failure to get the build settings: $error.');
|
|
return const <String, String>{};
|
|
} finally {
|
|
status.stop();
|
|
}
|
|
}
|
|
|
|
void cleanWorkspace(String workspacePath, String scheme) {
|
|
_processUtils.runSync(<String>[
|
|
_executable,
|
|
'-workspace',
|
|
workspacePath,
|
|
'-scheme',
|
|
scheme,
|
|
'-quiet',
|
|
'clean',
|
|
...environmentVariablesAsXcodeBuildSettings(_platform)
|
|
], workingDirectory: _fileSystem.currentDirectory.path);
|
|
}
|
|
|
|
Future<XcodeProjectInfo> getInfo(String projectPath, {String projectFilename}) async {
|
|
// The exit code returned by 'xcodebuild -list' when either:
|
|
// * -project is passed and the given project isn't there, or
|
|
// * no -project is passed and there isn't a project.
|
|
const int missingProjectExitCode = 66;
|
|
final RunResult result = await _processUtils.run(
|
|
<String>[
|
|
_executable,
|
|
'-list',
|
|
if (projectFilename != null) ...<String>['-project', projectFilename],
|
|
],
|
|
throwOnError: true,
|
|
whiteListFailures: (int c) => c == missingProjectExitCode,
|
|
workingDirectory: projectPath,
|
|
);
|
|
if (result.exitCode == missingProjectExitCode) {
|
|
throwToolExit('Unable to get Xcode project information:\n ${result.stderr}');
|
|
}
|
|
return XcodeProjectInfo.fromXcodeBuildOutput(result.toString());
|
|
}
|
|
}
|
|
|
|
/// Environment variables prefixed by FLUTTER_XCODE_ will be passed as build configurations to xcodebuild.
|
|
/// This allows developers to pass arbitrary build settings in without the tool needing to make a flag
|
|
/// for or be aware of each one. This could be used to set code signing build settings in a CI
|
|
/// environment without requiring settings changes in the Xcode project.
|
|
List<String> environmentVariablesAsXcodeBuildSettings(Platform platform) {
|
|
const String xcodeBuildSettingPrefix = 'FLUTTER_XCODE_';
|
|
return platform.environment.entries.where((MapEntry<String, String> mapEntry) {
|
|
return mapEntry.key.startsWith(xcodeBuildSettingPrefix);
|
|
}).expand<String>((MapEntry<String, String> mapEntry) {
|
|
// Remove FLUTTER_XCODE_ prefix from the environment variable to get the build setting.
|
|
final String trimmedBuildSettingKey = mapEntry.key.substring(xcodeBuildSettingPrefix.length);
|
|
return <String>['$trimmedBuildSettingKey=${mapEntry.value}'];
|
|
}).toList();
|
|
}
|
|
|
|
Map<String, String> parseXcodeBuildSettings(String showBuildSettingsOutput) {
|
|
final Map<String, String> settings = <String, String>{};
|
|
for (final Match match in showBuildSettingsOutput.split('\n').map<Match>(_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 (final 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 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;
|
|
}
|
|
return baseConfiguration + '-$scheme';
|
|
}
|
|
|
|
/// Checks whether the [buildConfigurations] contains the specified string, without
|
|
/// regard to case.
|
|
bool hasBuildConfiguratinForBuildMode(String buildMode) {
|
|
buildMode = buildMode.toLowerCase();
|
|
for (final String name in buildConfigurations) {
|
|
if (name.toLowerCase() == buildMode) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/// 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 (hasBuildConfiguratinForBuildMode(expectedConfiguration)) {
|
|
return expectedConfiguration;
|
|
}
|
|
final String baseConfiguration = _baseConfigurationFor(buildInfo);
|
|
return _uniqueMatch(buildConfigurations, (String candidate) {
|
|
candidate = candidate.toLowerCase();
|
|
if (buildInfo.flavor == null) {
|
|
return candidate == expectedConfiguration.toLowerCase();
|
|
}
|
|
return candidate.contains(baseConfiguration.toLowerCase()) && candidate.contains(scheme.toLowerCase());
|
|
});
|
|
}
|
|
|
|
static String _baseConfigurationFor(BuildInfo buildInfo) {
|
|
if (buildInfo.isDebug) {
|
|
return 'Debug';
|
|
}
|
|
if (buildInfo.isProfile) {
|
|
return 'Profile';
|
|
}
|
|
return '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;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'XcodeProjectInfo($targets, $buildConfigurations, $schemes)';
|
|
}
|
|
}
|