Implement plugin tooling support for macOS (#33636)
Enables the CocoaPods-based plugin workflow for macOS. This allows a
macOS project to automatically fetch and add native plugin
implementations via CocoaPods for anything in pubspec.yaml, as is done
on iOS.
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 970d9ee..26353ef 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -13,6 +13,7 @@
import 'build_info.dart';
import 'bundle.dart' as bundle;
import 'cache.dart';
+import 'desktop.dart';
import 'flutter_manifest.dart';
import 'ios/ios_workflow.dart';
import 'ios/plist_utils.dart' as plist;
@@ -179,6 +180,11 @@
if ((ios.existsSync() && checkProjects) || !checkProjects) {
await ios.ensureReadyForPlatformSpecificTooling();
}
+ // TODO(stuartmorgan): Add checkProjects logic once a create workflow exists
+ // for macOS. For now, always treat checkProjects as true for macOS.
+ if (flutterDesktopEnabled && macos.existsSync()) {
+ await macos.ensureReadyForPlatformSpecificTooling();
+ }
if (flutterWebEnabled) {
await web.ensureReadyForPlatformSpecificTooling();
}
@@ -205,14 +211,53 @@
}
}
+/// Represents an Xcode-based sub-project.
+///
+/// This defines interfaces common to iOS and macOS projects.
+abstract class XcodeBasedProject {
+ /// The parent of this project.
+ FlutterProject get parent;
+
+ /// Whether the subproject (either iOS or macOS) exists in the Flutter project.
+ bool existsSync();
+
+ /// The Xcode project (.xcodeproj directory) of the host app.
+ Directory get xcodeProject;
+
+ /// The 'project.pbxproj' file of [xcodeProject].
+ File get xcodeProjectInfoFile;
+
+ /// The Xcode workspace (.xcworkspace directory) of the host app.
+ Directory get xcodeWorkspace;
+
+ /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
+ /// the Xcode build.
+ File get generatedXcodePropertiesFile;
+
+ /// The Flutter-managed Xcode config file for [mode].
+ File xcodeConfigFor(String mode);
+
+ /// The CocoaPods 'Podfile'.
+ File get podfile;
+
+ /// The CocoaPods 'Podfile.lock'.
+ File get podfileLock;
+
+ /// The CocoaPods 'Manifest.lock'.
+ File get podManifestLock;
+
+ /// True if the host app project is using Swift.
+ bool get isSwift;
+}
+
/// Represents the iOS sub-project of a Flutter project.
///
/// Instances will reflect the contents of the `ios/` sub-folder of
/// Flutter applications and the `.ios/` sub-folder of Flutter module projects.
-class IosProject {
+class IosProject implements XcodeBasedProject {
IosProject.fromFlutter(this.parent);
- /// The parent of this project.
+ @override
final FlutterProject parent;
static final RegExp _productBundleIdPattern = RegExp(r'''^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(["']?)(.*?)\1;\s*$''');
@@ -246,28 +291,28 @@
/// Whether the flutter application has an iOS project.
bool get exists => hostAppRoot.existsSync();
- /// The xcode config file for [mode].
+ @override
File xcodeConfigFor(String mode) => _flutterLibRoot.childDirectory('Flutter').childFile('$mode.xcconfig');
- /// The 'Podfile'.
+ @override
File get podfile => hostAppRoot.childFile('Podfile');
- /// The 'Podfile.lock'.
+ @override
File get podfileLock => hostAppRoot.childFile('Podfile.lock');
- /// The 'Manifest.lock'.
+ @override
File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock');
/// The 'Info.plist' file of the host app.
File get hostInfoPlist => hostAppRoot.childDirectory(_hostAppBundleName).childFile('Info.plist');
- /// '.xcodeproj' folder of the host app.
+ @override
Directory get xcodeProject => hostAppRoot.childDirectory('$_hostAppBundleName.xcodeproj');
- /// The '.pbxproj' file of the host app.
+ @override
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
- /// Xcode workspace directory of the host app.
+ @override
Directory get xcodeWorkspace => hostAppRoot.childDirectory('$_hostAppBundleName.xcworkspace');
/// Xcode workspace shared data directory for the host app.
@@ -276,7 +321,7 @@
/// Xcode workspace shared workspace settings file for the host app.
File get xcodeWorkspaceSharedSettings => xcodeWorkspaceSharedData.childFile('WorkspaceSettings.xcsettings');
- /// Whether the current flutter project has an iOS subproject.
+ @override
bool existsSync() {
return parent.isModule || _editableDirectory.existsSync();
}
@@ -304,7 +349,7 @@
return null;
}
- /// True, if the host app project is using Swift.
+ @override
bool get isSwift => buildSettings?.containsKey('SWIFT_VERSION') ?? false;
/// The build settings for the host app of this project, as a detached map.
@@ -364,6 +409,7 @@
await injectPlugins(parent);
}
+ @override
File get generatedXcodePropertiesFile => _flutterLibRoot.childDirectory('Flutter').childFile('Generated.xcconfig');
Directory get pluginRegistrantHost {
@@ -573,16 +619,18 @@
}
/// The macOS sub project.
-class MacOSProject {
- MacOSProject._(this.project);
+class MacOSProject implements XcodeBasedProject {
+ MacOSProject._(this.parent);
- final FlutterProject project;
+ @override
+ final FlutterProject parent;
static const String _hostAppBundleName = 'Runner';
+ @override
bool existsSync() => _macOSDirectory.existsSync();
- Directory get _macOSDirectory => project.directory.childDirectory('macos');
+ Directory get _macOSDirectory => parent.directory.childDirectory('macos');
/// The directory in the project that is managed by Flutter. As much as
/// possible, files that are edited by Flutter tooling after initial project
@@ -594,24 +642,54 @@
/// checked in should live here.
Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');
- /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
- /// the Xcode build.
+ @override
File get generatedXcodePropertiesFile => ephemeralDirectory.childFile('Flutter-Generated.xcconfig');
- /// The Flutter-managed Xcode config file for [mode].
+ @override
File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig');
- /// The Xcode project file.
+ @override
+ File get podfile => _macOSDirectory.childFile('Podfile');
+
+ @override
+ File get podfileLock => _macOSDirectory.childFile('Podfile.lock');
+
+ @override
+ File get podManifestLock => _macOSDirectory.childDirectory('Pods').childFile('Manifest.lock');
+
+ @override
Directory get xcodeProject => _macOSDirectory.childDirectory('$_hostAppBundleName.xcodeproj');
- /// The Xcode workspace file.
+ @override
+ File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
+
+ @override
Directory get xcodeWorkspace => _macOSDirectory.childDirectory('$_hostAppBundleName.xcworkspace');
+ @override
+ bool get isSwift => true;
+
/// The file where the Xcode build will write the name of the built app.
///
/// Ideally this will be replaced in the future with inspection of the Runner
/// scheme's target.
File get nameFile => ephemeralDirectory.childFile('.app_filename');
+
+ Future<void> ensureReadyForPlatformSpecificTooling() async {
+ // TODO(stuartmorgan): Add create-from-template logic here.
+ await _updateGeneratedXcodeConfigIfNeeded();
+ }
+
+ Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
+ if (Cache.instance.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
+ await xcode.updateGeneratedXcodeProperties(
+ project: parent,
+ buildInfo: BuildInfo.debug,
+ useMacOSConfig: true,
+ setSymroot: false,
+ );
+ }
+ }
}
/// The Windows sub project