Skip to content

Commit f8c50ea

Browse files
authored
Use Xcode legacy build system for iOS builds (flutter#21901) (flutter#21994)
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: flutter#20685
1 parent ff9dc22 commit f8c50ea

File tree

4 files changed

+175
-1
lines changed

4 files changed

+175
-1
lines changed

packages/flutter_tools/lib/src/context_runner.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Future<T> runInContext<T>(
7171
KernelCompiler: () => const KernelCompiler(),
7272
Logger: () => platform.isWindows ? WindowsStdoutLogger() : StdoutLogger(),
7373
OperatingSystemUtils: () => OperatingSystemUtils(),
74+
PlistBuddy: () => const PlistBuddy(),
7475
SimControl: () => SimControl(),
7576
Stdio: () => const Stdio(),
7677
Usage: () => Usage(),

packages/flutter_tools/lib/src/ios/mac.dart

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,57 @@ const int kXcodeRequiredVersionMajor = 9;
3232
const int kXcodeRequiredVersionMinor = 0;
3333

3434
IMobileDevice get iMobileDevice => context[IMobileDevice];
35-
35+
PlistBuddy get plistBuddy => context[PlistBuddy];
3636
Xcode get xcode => context[Xcode];
3737

38+
class PlistBuddy {
39+
const PlistBuddy();
40+
41+
static const String path = '/usr/libexec/PlistBuddy';
42+
43+
Future<ProcessResult> run(List<String> args) => processManager.run(<String>[path]..addAll(args));
44+
}
45+
46+
/// A property list is a key-value representation commonly used for
47+
/// configuration on macOS/iOS systems.
48+
class PropertyList {
49+
const PropertyList(this.plistPath);
50+
51+
final String plistPath;
52+
53+
/// Prints the specified key, or returns null if not present.
54+
Future<String> read(String key) async {
55+
final ProcessResult result = await _runCommand('Print $key');
56+
if (result.exitCode == 0)
57+
return result.stdout.trim();
58+
return null;
59+
}
60+
61+
/// Adds [key]. Has no effect if the key already exists.
62+
Future<void> addString(String key, String value) async {
63+
await _runCommand('Add $key string $value');
64+
}
65+
66+
/// Updates [key] with the new [value]. Has no effect if the key does not exist.
67+
Future<void> update(String key, String value) async {
68+
await _runCommand('Set $key $value');
69+
}
70+
71+
/// Deletes [key].
72+
Future<void> delete(String key) async {
73+
await _runCommand('Delete $key');
74+
}
75+
76+
/// Deletes the content of the property list and creates a new root of the specified type.
77+
Future<void> clearToDict() async {
78+
await _runCommand('Clear dict');
79+
}
80+
81+
Future<ProcessResult> _runCommand(String command) async {
82+
return await plistBuddy.run(<String>['-c', command, plistPath]);
83+
}
84+
}
85+
3886
class IMobileDevice {
3987
const IMobileDevice();
4088

@@ -181,6 +229,47 @@ class Xcode {
181229
}
182230
}
183231

232+
/// Sets the Xcode system.
233+
///
234+
/// Xcode 10 added a new (default) build system with better performance and
235+
/// stricter checks. Flutter apps without plugins build fine under the new
236+
/// system, but it causes build breakages in projects with CocoaPods enabled.
237+
/// This affects Flutter apps with plugins.
238+
///
239+
/// Once Flutter has been updated to be fully compliant with the new build
240+
/// system, this can be removed.
241+
//
242+
// TODO(cbracken): remove when https://github.com/flutter/flutter/issues/20685 is fixed.
243+
Future<void> setXcodeWorkspaceBuildSystem({
244+
@required Directory workspaceDirectory,
245+
@required File workspaceSettings,
246+
@required bool modern,
247+
}) async {
248+
// If this isn't a workspace, we're not using CocoaPods and can use the new
249+
// build system.
250+
if (!workspaceDirectory.existsSync())
251+
return;
252+
253+
final PropertyList plist = PropertyList(workspaceSettings.path);
254+
if (!workspaceSettings.existsSync()) {
255+
workspaceSettings.parent.createSync(recursive: true);
256+
await plist.clearToDict();
257+
}
258+
259+
const String kBuildSystemType = 'BuildSystemType';
260+
if (modern) {
261+
printTrace('Using new Xcode build system.');
262+
await plist.delete(kBuildSystemType);
263+
} else {
264+
printTrace('Using legacy Xcode build system.');
265+
if (await plist.read(kBuildSystemType) == null) {
266+
await plist.addString(kBuildSystemType, 'Original');
267+
} else {
268+
await plist.update(kBuildSystemType, 'Original');
269+
}
270+
}
271+
}
272+
184273
Future<XcodeBuildResult> buildXcodeProject({
185274
BuildableIOSApp app,
186275
BuildInfo buildInfo,
@@ -195,6 +284,13 @@ Future<XcodeBuildResult> buildXcodeProject({
195284
if (!_checkXcodeVersion())
196285
return XcodeBuildResult(success: false);
197286

287+
// TODO(cbracken) remove when https://github.com/flutter/flutter/issues/20685 is fixed.
288+
await setXcodeWorkspaceBuildSystem(
289+
workspaceDirectory: app.project.xcodeWorkspace,
290+
workspaceSettings: app.project.xcodeWorkspaceSharedSettings,
291+
modern: false,
292+
);
293+
198294
final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.project.directory.path);
199295
if (!projectInfo.targets.contains('Runner')) {
200296
printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.');

packages/flutter_tools/lib/src/project.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,15 @@ class IosProject {
186186
/// The '.pbxproj' file of the host app.
187187
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
188188

189+
/// Xcode workspace directory of the host app.
190+
Directory get xcodeWorkspace => directory.childDirectory('$_hostAppBundleName.xcworkspace');
191+
192+
/// Xcode workspace shared data directory for the host app.
193+
Directory get xcodeWorkspaceSharedData => xcodeWorkspace.childDirectory('xcshareddata');
194+
195+
/// Xcode workspace shared workspace settings file for the host app.
196+
File get xcodeWorkspaceSharedSettings => xcodeWorkspaceSharedData.childFile('WorkspaceSettings.xcsettings');
197+
189198
/// The product bundle identifier of the host app, or null if not set or if
190199
/// iOS tooling needed to read it is not installed.
191200
String get productBundleIdentifier {

packages/flutter_tools/test/ios/mac_test.dart

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:async';
66

77
import 'package:file/file.dart';
8+
import 'package:file/memory.dart';
89
import 'package:flutter_tools/src/base/file_system.dart';
910
import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
1011
import 'package:flutter_tools/src/ios/mac.dart';
@@ -21,6 +22,73 @@ class MockFile extends Mock implements File {}
2122
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
2223

2324
void main() {
25+
group('PropertyList', () {
26+
MockProcessManager mockProcessManager;
27+
MemoryFileSystem fs;
28+
Directory workspaceDirectory;
29+
File workspaceSettingsFile;
30+
31+
setUp(() {
32+
mockProcessManager = MockProcessManager();
33+
fs = MemoryFileSystem();
34+
workspaceDirectory = fs.directory('Runner.xcworkspace');
35+
workspaceSettingsFile = workspaceDirectory.childDirectory('xcshareddata').childFile('WorkspaceSettings.xcsettings');
36+
});
37+
38+
testUsingContext('does nothing if workspace directory does not exist', () async {
39+
await setXcodeWorkspaceBuildSystem(workspaceDirectory: workspaceDirectory, workspaceSettings: workspaceSettingsFile, modern: false);
40+
verifyNever(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path]));
41+
}, overrides: <Type, Generator>{
42+
FileSystem: () => fs,
43+
ProcessManager: () => mockProcessManager,
44+
});
45+
46+
testUsingContext('creates dict-based plist if settings file does not exist', () async {
47+
workspaceSettingsFile.parent.createSync(recursive: true);
48+
when(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path]))
49+
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 1, '', '')));
50+
await setXcodeWorkspaceBuildSystem(workspaceDirectory: workspaceDirectory, workspaceSettings: workspaceSettingsFile, modern: false);
51+
verify(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Clear dict', workspaceSettingsFile.path]));
52+
verify(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Add BuildSystemType string Original', workspaceSettingsFile.path]));
53+
}, overrides: <Type, Generator>{
54+
FileSystem: () => fs,
55+
ProcessManager: () => mockProcessManager,
56+
});
57+
58+
testUsingContext('writes legacy build mode settings if requested and not present', () async {
59+
workspaceSettingsFile.createSync(recursive: true);
60+
when(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path]))
61+
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 1, '', '')));
62+
await setXcodeWorkspaceBuildSystem(workspaceDirectory: workspaceDirectory, workspaceSettings: workspaceSettingsFile, modern: false);
63+
verify(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Add BuildSystemType string Original', workspaceSettingsFile.path]));
64+
}, overrides: <Type, Generator>{
65+
FileSystem: () => fs,
66+
ProcessManager: () => mockProcessManager,
67+
});
68+
69+
testUsingContext('updates legacy build mode setting if requested and existing setting is present', () async {
70+
workspaceSettingsFile.createSync(recursive: true);
71+
when(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path]))
72+
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 0, 'FancyNewOne', '')));
73+
await setXcodeWorkspaceBuildSystem(workspaceDirectory: workspaceDirectory, workspaceSettings: workspaceSettingsFile, modern: false);
74+
verify(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Set BuildSystemType Original', workspaceSettingsFile.path]));
75+
}, overrides: <Type, Generator>{
76+
FileSystem: () => fs,
77+
ProcessManager: () => mockProcessManager,
78+
});
79+
80+
testUsingContext('deletes legacy build mode setting if modern build mode requested', () async {
81+
workspaceSettingsFile.createSync(recursive: true);
82+
when(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path]))
83+
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 0, 'Original', '')));
84+
await setXcodeWorkspaceBuildSystem(workspaceDirectory: workspaceDirectory, workspaceSettings: workspaceSettingsFile, modern: true);
85+
verify(mockProcessManager.run(<String>[PlistBuddy.path, '-c', 'Delete BuildSystemType', workspaceSettingsFile.path]));
86+
}, overrides: <Type, Generator>{
87+
FileSystem: () => fs,
88+
ProcessManager: () => mockProcessManager,
89+
});
90+
});
91+
2492
group('IMobileDevice', () {
2593
final FakePlatform osx = FakePlatform.fromPlatform(const LocalPlatform())
2694
..operatingSystem = 'macos';

0 commit comments

Comments
 (0)