diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 86c8ab5844..780747e0bc 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -71,6 +71,7 @@ Future runInContext( KernelCompiler: () => const KernelCompiler(), Logger: () => platform.isWindows ? WindowsStdoutLogger() : StdoutLogger(), OperatingSystemUtils: () => OperatingSystemUtils(), + PlistBuddy: () => const PlistBuddy(), SimControl: () => SimControl(), Stdio: () => const Stdio(), Usage: () => Usage(), diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 294d7857dc..b51f8b4ddc 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -32,9 +32,57 @@ const int kXcodeRequiredVersionMajor = 9; const int kXcodeRequiredVersionMinor = 0; IMobileDevice get iMobileDevice => context[IMobileDevice]; - +PlistBuddy get plistBuddy => context[PlistBuddy]; Xcode get xcode => context[Xcode]; +class PlistBuddy { + const PlistBuddy(); + + static const String path = '/usr/libexec/PlistBuddy'; + + Future run(List args) => processManager.run([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 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 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 update(String key, String value) async { + await _runCommand('Set $key $value'); + } + + /// Deletes [key]. + Future 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 clearToDict() async { + await _runCommand('Clear dict'); + } + + Future _runCommand(String command) async { + return await plistBuddy.run(['-c', command, plistPath]); + } +} + class IMobileDevice { const IMobileDevice(); @@ -181,6 +229,47 @@ 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 setXcodeWorkspaceBuildSystem({ + @required Directory workspaceDirectory, + @required File workspaceSettings, + @required bool modern, +}) async { + // If this isn't a workspace, we're not using CocoaPods and can use the new + // build system. + if (!workspaceDirectory.existsSync()) + return; + + 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 buildXcodeProject({ BuildableIOSApp app, BuildInfo buildInfo, @@ -195,6 +284,13 @@ Future buildXcodeProject({ if (!_checkXcodeVersion()) return XcodeBuildResult(success: false); + // TODO(cbracken) remove when https://github.com/flutter/flutter/issues/20685 is fixed. + await setXcodeWorkspaceBuildSystem( + workspaceDirectory: app.project.xcodeWorkspace, + workspaceSettings: app.project.xcodeWorkspaceSharedSettings, + modern: false, + ); + final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.project.directory.path); if (!projectInfo.targets.contains('Runner')) { printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.'); diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 6f11e0505e..6f41908e9a 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -186,6 +186,15 @@ class IosProject { /// The '.pbxproj' file of the host app. 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 /// iOS tooling needed to read it is not installed. String get productBundleIdentifier { diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart index 8e3ccc3cfe..85d9926452 100644 --- a/packages/flutter_tools/test/ios/mac_test.dart +++ b/packages/flutter_tools/test/ios/mac_test.dart @@ -5,6 +5,7 @@ import 'dart:async'; 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/io.dart' show ProcessException, ProcessResult; import 'package:flutter_tools/src/ios/mac.dart'; @@ -21,6 +22,73 @@ class MockFile extends Mock implements File {} class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {} void main() { + group('PropertyList', () { + MockProcessManager mockProcessManager; + MemoryFileSystem fs; + Directory workspaceDirectory; + File workspaceSettingsFile; + + setUp(() { + mockProcessManager = MockProcessManager(); + fs = MemoryFileSystem(); + workspaceDirectory = fs.directory('Runner.xcworkspace'); + workspaceSettingsFile = workspaceDirectory.childDirectory('xcshareddata').childFile('WorkspaceSettings.xcsettings'); + }); + + testUsingContext('does nothing if workspace directory does not exist', () async { + await setXcodeWorkspaceBuildSystem(workspaceDirectory: workspaceDirectory, workspaceSettings: workspaceSettingsFile, modern: false); + verifyNever(mockProcessManager.run([PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path])); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('creates dict-based plist if settings file does not exist', () async { + workspaceSettingsFile.parent.createSync(recursive: true); + when(mockProcessManager.run([PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path])) + .thenAnswer((_) => Future.value(ProcessResult(1, 1, '', ''))); + await setXcodeWorkspaceBuildSystem(workspaceDirectory: workspaceDirectory, workspaceSettings: workspaceSettingsFile, modern: false); + verify(mockProcessManager.run([PlistBuddy.path, '-c', 'Clear dict', workspaceSettingsFile.path])); + verify(mockProcessManager.run([PlistBuddy.path, '-c', 'Add BuildSystemType string Original', workspaceSettingsFile.path])); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('writes legacy build mode settings if requested and not present', () async { + workspaceSettingsFile.createSync(recursive: true); + when(mockProcessManager.run([PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path])) + .thenAnswer((_) => Future.value(ProcessResult(1, 1, '', ''))); + await setXcodeWorkspaceBuildSystem(workspaceDirectory: workspaceDirectory, workspaceSettings: workspaceSettingsFile, modern: false); + verify(mockProcessManager.run([PlistBuddy.path, '-c', 'Add BuildSystemType string Original', workspaceSettingsFile.path])); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('updates legacy build mode setting if requested and existing setting is present', () async { + workspaceSettingsFile.createSync(recursive: true); + when(mockProcessManager.run([PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path])) + .thenAnswer((_) => Future.value(ProcessResult(1, 0, 'FancyNewOne', ''))); + await setXcodeWorkspaceBuildSystem(workspaceDirectory: workspaceDirectory, workspaceSettings: workspaceSettingsFile, modern: false); + verify(mockProcessManager.run([PlistBuddy.path, '-c', 'Set BuildSystemType Original', workspaceSettingsFile.path])); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('deletes legacy build mode setting if modern build mode requested', () async { + workspaceSettingsFile.createSync(recursive: true); + when(mockProcessManager.run([PlistBuddy.path, '-c', 'Print BuildSystemType', workspaceSettingsFile.path])) + .thenAnswer((_) => Future.value(ProcessResult(1, 0, 'Original', ''))); + await setXcodeWorkspaceBuildSystem(workspaceDirectory: workspaceDirectory, workspaceSettings: workspaceSettingsFile, modern: true); + verify(mockProcessManager.run([PlistBuddy.path, '-c', 'Delete BuildSystemType', workspaceSettingsFile.path])); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => mockProcessManager, + }); + }); + group('IMobileDevice', () { final FakePlatform osx = FakePlatform.fromPlatform(const LocalPlatform()) ..operatingSystem = 'macos';