Unverified Commit 021f472e authored by Chris Bracken's avatar Chris Bracken Committed by GitHub

Use Xcode legacy build system for iOS builds (#21901)

Xcode 10 introduces a new build system which includes stricter checks on
duplicate build outputs.

When plugins are in use, there are two competing build actions that copy
Flutter.framework into the build application Frameworks directory:

  1. The Embed Frameworks build phase for the Runner project
  2. The [CP] Embed Pods Frameworks build phase that pod install creates
     in the project.

Item (1) is there to ensure the framework is copied into the built app
in the case where there are no plugins (and therefore no CocoaPods
integration in the Xcode project). Item (2) is there because Flutter's
podspec declares Flutter.framework as a vended_framework, and CocoaPods
automatically adds a copy step for each such vended_framework in the
transitive closure of CocoaPods dependencies.

As an immediate fix, we opt back into the build system used by Xcode 9
and earlier. Longer term, we need to update our templates and
flutter_tools to correctly handle this situation.

See: https://github.com/flutter/flutter/issues/20685
parent 9f359aee
...@@ -71,6 +71,7 @@ Future<T> runInContext<T>( ...@@ -71,6 +71,7 @@ Future<T> runInContext<T>(
KernelCompiler: () => const KernelCompiler(), KernelCompiler: () => const KernelCompiler(),
Logger: () => platform.isWindows ? WindowsStdoutLogger() : StdoutLogger(), Logger: () => platform.isWindows ? WindowsStdoutLogger() : StdoutLogger(),
OperatingSystemUtils: () => OperatingSystemUtils(), OperatingSystemUtils: () => OperatingSystemUtils(),
PlistBuddy: () => const PlistBuddy(),
SimControl: () => SimControl(), SimControl: () => SimControl(),
Stdio: () => const Stdio(), Stdio: () => const Stdio(),
Usage: () => Usage(), Usage: () => Usage(),
......
...@@ -32,9 +32,57 @@ const int kXcodeRequiredVersionMajor = 9; ...@@ -32,9 +32,57 @@ const int kXcodeRequiredVersionMajor = 9;
const int kXcodeRequiredVersionMinor = 0; const int kXcodeRequiredVersionMinor = 0;
IMobileDevice get iMobileDevice => context[IMobileDevice]; IMobileDevice get iMobileDevice => context[IMobileDevice];
PlistBuddy get plistBuddy => context[PlistBuddy];
Xcode get xcode => context[Xcode]; Xcode get xcode => context[Xcode];
class PlistBuddy {
const PlistBuddy();
static const String path = '/usr/libexec/PlistBuddy';
Future<ProcessResult> run(List<String> args) => processManager.run(<String>[path]..addAll(args));
}
/// A property list is a key-value representation commonly used for
/// configuration on macOS/iOS systems.
class PropertyList {
const PropertyList(this.plistPath);
final String plistPath;
/// Prints the specified key, or returns null if not present.
Future<String> read(String key) async {
final ProcessResult result = await _runCommand('Print $key');
if (result.exitCode == 0)
return result.stdout.trim();
return null;
}
/// Adds [key]. Has no effect if the key already exists.
Future<void> addString(String key, String value) async {
await _runCommand('Add $key string $value');
}
/// Updates [key] with the new [value]. Has no effect if the key does not exist.
Future<void> update(String key, String value) async {
await _runCommand('Set $key $value');
}
/// Deletes [key].
Future<void> delete(String key) async {
await _runCommand('Delete $key');
}
/// Deletes the content of the property list and creates a new root of the specified type.
Future<void> clearToDict() async {
await _runCommand('Clear dict');
}
Future<ProcessResult> _runCommand(String command) async {
return await plistBuddy.run(<String>['-c', command, plistPath]);
}
}
class IMobileDevice { class IMobileDevice {
const IMobileDevice(); const IMobileDevice();
...@@ -181,6 +229,41 @@ class Xcode { ...@@ -181,6 +229,41 @@ class Xcode {
} }
} }
/// Sets the Xcode system.
///
/// Xcode 10 added a new (default) build system with better performance and
/// stricter checks. Flutter apps without plugins build fine under the new
/// system, but it causes build breakages in projects with CocoaPods enabled.
/// This affects Flutter apps with plugins.
///
/// Once Flutter has been updated to be fully compliant with the new build
/// system, this can be removed.
//
// TODO(cbracken): remove when https://github.com/flutter/flutter/issues/20685 is fixed.
Future<void> setXcodeWorkspaceBuildSystem({
@required File workspaceSettings,
@required bool modern,
}) async {
final PropertyList plist = PropertyList(workspaceSettings.path);
if (!workspaceSettings.existsSync()) {
workspaceSettings.parent.createSync(recursive: true);
await plist.clearToDict();
}
const String kBuildSystemType = 'BuildSystemType';
if (modern) {
printTrace('Using new Xcode build system.');
await plist.delete(kBuildSystemType);
} else {
printTrace('Using legacy Xcode build system.');
if (await plist.read(kBuildSystemType) == null) {
await plist.addString(kBuildSystemType, 'Original');
} else {
await plist.update(kBuildSystemType, 'Original');
}
}
}
Future<XcodeBuildResult> buildXcodeProject({ Future<XcodeBuildResult> buildXcodeProject({
BuildableIOSApp app, BuildableIOSApp app,
BuildInfo buildInfo, BuildInfo buildInfo,
...@@ -195,6 +278,12 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -195,6 +278,12 @@ Future<XcodeBuildResult> buildXcodeProject({
if (!_checkXcodeVersion()) if (!_checkXcodeVersion())
return XcodeBuildResult(success: false); return XcodeBuildResult(success: false);
// TODO(cbracken) remove when https://github.com/flutter/flutter/issues/20685 is fixed.
await setXcodeWorkspaceBuildSystem(
workspaceSettings: app.project.xcodeWorkspaceSharedSettings,
modern: false,
);
final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.project.directory.path); final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.project.directory.path);
if (!projectInfo.targets.contains('Runner')) { if (!projectInfo.targets.contains('Runner')) {
printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.'); printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.');
......
...@@ -186,6 +186,15 @@ class IosProject { ...@@ -186,6 +186,15 @@ class IosProject {
/// The '.pbxproj' file of the host app. /// The '.pbxproj' file of the host app.
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
/// Xcode workspace directory of the host app.
Directory get xcodeWorkspace => directory.childDirectory('$_hostAppBundleName.xcworkspace');
/// Xcode workspace shared data directory for the host app.
Directory get xcodeWorkspaceSharedData => xcodeWorkspace.childDirectory('xcshareddata');
/// Xcode workspace shared workspace settings file for the host app.
File get xcodeWorkspaceSharedSettings => xcodeWorkspaceSharedData.childFile('WorkspaceSettings.xcsettings');
/// The product bundle identifier of the host app, or null if not set or if /// The product bundle identifier of the host app, or null if not set or if
/// iOS tooling needed to read it is not installed. /// iOS tooling needed to read it is not installed.
String get productBundleIdentifier { String get productBundleIdentifier {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult; import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/mac.dart';
...@@ -21,6 +22,63 @@ class MockFile extends Mock implements File {} ...@@ -21,6 +22,63 @@ class MockFile extends Mock implements File {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {} class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
void main() { void main() {
group('PropertyList', () {
MockProcessManager mockProcessManager;
MemoryFileSystem fs;
File workspaceSettingsFile;
setUp(() {
mockProcessManager = MockProcessManager();
fs = MemoryFileSystem();
workspaceSettingsFile = fs.file('WorkspaceSettings.xcsettings');
});
testUsingContext('creates dict-based plist if settings file does not exist', () async {
workspaceSettingsFile.parent.createSync(recursive: true);
when(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path]))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 1, '', '')));
await setXcodeWorkspaceBuildSystem(workspaceSettings: workspaceSettingsFile, modern: false);
verify(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Clear dict', workspaceSettingsFile.path]));
verify(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Add BuildSystemType string Original', workspaceSettingsFile.path]));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
testUsingContext('writes legacy build mode settings if requested and not present', () async {
workspaceSettingsFile.createSync();
when(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path]))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 1, '', '')));
await setXcodeWorkspaceBuildSystem(workspaceSettings: workspaceSettingsFile, modern: false);
verify(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Add BuildSystemType string Original', workspaceSettingsFile.path]));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
testUsingContext('updates legacy build mode setting if requested and existing setting is present', () async {
workspaceSettingsFile.createSync();
when(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path]))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 0, 'FancyNewOne', '')));
await setXcodeWorkspaceBuildSystem(workspaceSettings: workspaceSettingsFile, modern: false);
verify(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Set BuildSystemType Original', workspaceSettingsFile.path]));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
testUsingContext('deletes legacy build mode setting if modern build mode requested', () async {
workspaceSettingsFile.createSync();
when(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path]))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 0, 'Original', '')));
await setXcodeWorkspaceBuildSystem(workspaceSettings: workspaceSettingsFile, modern: true);
verify(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Delete BuildSystemType', workspaceSettingsFile.path]));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
});
group('IMobileDevice', () { group('IMobileDevice', () {
final FakePlatform osx = FakePlatform.fromPlatform(const LocalPlatform()) final FakePlatform osx = FakePlatform.fromPlatform(const LocalPlatform())
..operatingSystem = 'macos'; ..operatingSystem = 'macos';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment