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/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 1b7a2e2..59a0153 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -10,7 +10,6 @@
 import '../base/common.dart';
 import '../base/context.dart';
 import '../base/file_system.dart';
-import '../base/fingerprint.dart';
 import '../base/io.dart';
 import '../base/logger.dart';
 import '../base/os.dart';
@@ -21,9 +20,8 @@
 import '../build_info.dart';
 import '../convert.dart';
 import '../globals.dart';
-import '../macos/cocoapods.dart';
+import '../macos/cocoapod_utils.dart';
 import '../macos/xcode.dart';
-import '../plugins.dart';
 import '../project.dart';
 import '../services.dart';
 import 'code_signing.dart';
@@ -274,29 +272,7 @@
     targetOverride: targetOverride,
     buildInfo: buildInfo,
   );
-  refreshPluginsList(project);
-  if (hasPlugins(project) || (project.isModule && project.ios.podfile.existsSync())) {
-    // If the Xcode project, Podfile, or Generated.xcconfig have changed since
-    // last run, pods should be updated.
-    final Fingerprinter fingerprinter = Fingerprinter(
-      fingerprintPath: fs.path.join(getIosBuildDirectory(), 'pod_inputs.fingerprint'),
-      paths: <String>[
-        app.project.xcodeProjectInfoFile.path,
-        app.project.podfile.path,
-        app.project.generatedXcodePropertiesFile.path,
-      ],
-      properties: <String, String>{},
-    );
-
-    final bool didPodInstall = await cocoaPods.processPods(
-      iosProject: project.ios,
-      iosEngineDir: flutterFrameworkDir(buildInfo.mode),
-      isSwift: project.ios.isSwift,
-      dependenciesChanged: !await fingerprinter.doesFingerprintMatch(),
-    );
-    if (didPodInstall)
-      await fingerprinter.writeFingerprint();
-  }
+  await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
 
   final List<String> buildCommands = <String>[
     '/usr/bin/env',
diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart
index 07c2c8c..12d2787 100644
--- a/packages/flutter_tools/lib/src/macos/build_macos.dart
+++ b/packages/flutter_tools/lib/src/macos/build_macos.dart
@@ -12,6 +12,7 @@
 import '../globals.dart';
 import '../ios/xcodeproj.dart';
 import '../project.dart';
+import 'cocoapod_utils.dart';
 
 /// Builds the macOS project through xcode build.
 // TODO(jonahwilliams): support target option.
@@ -28,6 +29,8 @@
     useMacOSConfig: true,
     setSymroot: false,
   );
+  await processPodsIfNeeded(flutterProject.macos, getMacOSBuildDirectory(), buildInfo.mode);
+
   // Set debug or release mode.
   String config = 'Debug';
   if (buildInfo.isRelease) {
diff --git a/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart b/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart
new file mode 100644
index 0000000..aa7f701
--- /dev/null
+++ b/packages/flutter_tools/lib/src/macos/cocoapod_utils.dart
@@ -0,0 +1,46 @@
+// Copyright 2019 The Chromium 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 '../base/file_system.dart';
+import '../base/fingerprint.dart';
+import '../build_info.dart';
+import '../ios/xcodeproj.dart';
+import '../plugins.dart';
+import '../project.dart';
+import 'cocoapods.dart';
+
+/// For a given build, determines whether dependencies have changed since the
+/// last call to processPods, then calls processPods with that information.
+Future<void> processPodsIfNeeded(XcodeBasedProject xcodeProject,
+    String buildDirectory, BuildMode buildMode) async {
+  final FlutterProject project = xcodeProject.parent;
+  // Ensure that the plugin list is up to date, since hasPlugins relies on it.
+  refreshPluginsList(project);
+  if (!(hasPlugins(project) || (project.isModule && xcodeProject.podfile.existsSync()))) {
+    return;
+  }
+  // If the Xcode project, Podfile, or generated xcconfig have changed since
+  // last run, pods should be updated.
+  final Fingerprinter fingerprinter = Fingerprinter(
+    fingerprintPath: fs.path.join(buildDirectory, 'pod_inputs.fingerprint'),
+    paths: <String>[
+      xcodeProject.xcodeProjectInfoFile.path,
+      xcodeProject.podfile.path,
+      xcodeProject.generatedXcodePropertiesFile.path,
+    ],
+    properties: <String, String>{},
+  );
+
+  final bool didPodInstall = await cocoaPods.processPods(
+    xcodeProject: xcodeProject,
+    engineDir: flutterFrameworkDir(buildMode),
+    isSwift: xcodeProject.isSwift,
+    dependenciesChanged: !await fingerprinter.doesFingerprintMatch(),
+  );
+  if (didPodInstall) {
+    await fingerprinter.writeFingerprint();
+  }
+}
diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart
index c85555c..868b0e9 100644
--- a/packages/flutter_tools/lib/src/macos/cocoapods.dart
+++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart
@@ -100,18 +100,18 @@
   }
 
   Future<bool> processPods({
-    @required IosProject iosProject,
+    @required XcodeBasedProject xcodeProject,
     // For backward compatibility with previously created Podfile only.
-    @required String iosEngineDir,
+    @required String engineDir,
     bool isSwift = false,
     bool dependenciesChanged = true,
   }) async {
-    if (!(await iosProject.podfile.exists())) {
+    if (!(await xcodeProject.podfile.exists())) {
       throwToolExit('Podfile missing');
     }
     if (await _checkPodCondition()) {
-      if (_shouldRunPodInstall(iosProject, dependenciesChanged)) {
-        await _runPodInstall(iosProject, iosEngineDir);
+      if (_shouldRunPodInstall(xcodeProject, dependenciesChanged)) {
+        await _runPodInstall(xcodeProject, engineDir);
         return true;
       }
     }
@@ -176,46 +176,52 @@
     return true;
   }
 
-  /// Ensures the given iOS sub-project of a parent Flutter project
+  /// Ensures the given Xcode-based sub-project of a parent Flutter project
   /// contains a suitable `Podfile` and that its `Flutter/Xxx.xcconfig` files
   /// include pods configuration.
-  void setupPodfile(IosProject iosProject) {
+  void setupPodfile(XcodeBasedProject xcodeProject) {
     if (!xcodeProjectInterpreter.isInstalled) {
       // Don't do anything for iOS when host platform doesn't support it.
       return;
     }
-    final Directory runnerProject = iosProject.xcodeProject;
+    final Directory runnerProject = xcodeProject.xcodeProject;
     if (!runnerProject.existsSync()) {
       return;
     }
-    final File podfile = iosProject.podfile;
+    final File podfile = xcodeProject.podfile;
     if (!podfile.existsSync()) {
-      final bool isSwift = xcodeProjectInterpreter.getBuildSettings(
-        runnerProject.path,
-        'Runner',
-      ).containsKey('SWIFT_VERSION');
+      String podfileTemplateName;
+      if (xcodeProject is MacOSProject) {
+        podfileTemplateName = 'Podfile-macos';
+      } else {
+        final bool isSwift = xcodeProjectInterpreter.getBuildSettings(
+          runnerProject.path,
+          'Runner',
+        ).containsKey('SWIFT_VERSION');
+        podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc';
+      }
       final File podfileTemplate = fs.file(fs.path.join(
         Cache.flutterRoot,
         'packages',
         'flutter_tools',
         'templates',
         'cocoapods',
-        isSwift ? 'Podfile-swift' : 'Podfile-objc',
+        podfileTemplateName,
       ));
       podfileTemplate.copySync(podfile.path);
     }
-    addPodsDependencyToFlutterXcconfig(iosProject);
+    addPodsDependencyToFlutterXcconfig(xcodeProject);
   }
 
-  /// Ensures all `Flutter/Xxx.xcconfig` files for the given iOS sub-project of
-  /// a parent Flutter project include pods configuration.
-  void addPodsDependencyToFlutterXcconfig(IosProject iosProject) {
-    _addPodsDependencyToFlutterXcconfig(iosProject, 'Debug');
-    _addPodsDependencyToFlutterXcconfig(iosProject, 'Release');
+  /// Ensures all `Flutter/Xxx.xcconfig` files for the given Xcode-based
+  /// sub-project of a parent Flutter project include pods configuration.
+  void addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject) {
+    _addPodsDependencyToFlutterXcconfig(xcodeProject, 'Debug');
+    _addPodsDependencyToFlutterXcconfig(xcodeProject, 'Release');
   }
 
-  void _addPodsDependencyToFlutterXcconfig(IosProject iosProject, String mode) {
-    final File file = iosProject.xcodeConfigFor(mode);
+  void _addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject, String mode) {
+    final File file = xcodeProject.xcodeConfigFor(mode);
     if (file.existsSync()) {
       final String content = file.readAsStringSync();
       final String include = '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode
@@ -226,8 +232,8 @@
   }
 
   /// Ensures that pod install is deemed needed on next check.
-  void invalidatePodInstallOutput(IosProject iosProject) {
-    final File manifestLock = iosProject.podManifestLock;
+  void invalidatePodInstallOutput(XcodeBasedProject xcodeProject) {
+    final File manifestLock = xcodeProject.podManifestLock;
     if (manifestLock.existsSync()) {
       manifestLock.deleteSync();
     }
@@ -239,13 +245,13 @@
   // 2. Podfile.lock doesn't exist or is older than Podfile
   // 3. Pods/Manifest.lock doesn't exist (It is deleted when plugins change)
   // 4. Podfile.lock doesn't match Pods/Manifest.lock.
-  bool _shouldRunPodInstall(IosProject iosProject, bool dependenciesChanged) {
+  bool _shouldRunPodInstall(XcodeBasedProject xcodeProject, bool dependenciesChanged) {
     if (dependenciesChanged)
       return true;
 
-    final File podfileFile = iosProject.podfile;
-    final File podfileLockFile = iosProject.podfileLock;
-    final File manifestLockFile = iosProject.podManifestLock;
+    final File podfileFile = xcodeProject.podfile;
+    final File podfileLockFile = xcodeProject.podfileLock;
+    final File manifestLockFile = xcodeProject.podManifestLock;
 
     return !podfileLockFile.existsSync()
         || !manifestLockFile.existsSync()
@@ -253,11 +259,11 @@
         || podfileLockFile.readAsStringSync() != manifestLockFile.readAsStringSync();
   }
 
-  Future<void> _runPodInstall(IosProject iosProject, String engineDirectory) async {
+  Future<void> _runPodInstall(XcodeBasedProject xcodeProject, String engineDirectory) async {
     final Status status = logger.startProgress('Running pod install...', timeout: timeoutConfiguration.slowOperation);
     final ProcessResult result = await processManager.run(
       <String>['pod', 'install', '--verbose'],
-      workingDirectory: iosProject.hostAppRoot.path,
+      workingDirectory: fs.path.dirname(xcodeProject.podfile.path),
       environment: <String, String>{
         // For backward compatibility with previously created Podfile only.
         'FLUTTER_FRAMEWORK_DIR': engineDirectory,
@@ -278,7 +284,7 @@
       }
     }
     if (result.exitCode != 0) {
-      invalidatePodInstallOutput(iosProject);
+      invalidatePodInstallOutput(xcodeProject);
       _diagnosePodInstallFailure(result);
       throwToolExit('Error running pod install');
     }
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index e8c0905..5883d36 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -9,6 +9,7 @@
 
 import 'base/file_system.dart';
 import 'dart/package_map.dart';
+import 'desktop.dart';
 import 'globals.dart';
 import 'macos/cocoapods.dart';
 import 'project.dart';
@@ -39,7 +40,9 @@
     if (pluginYaml != null) {
       androidPackage = pluginYaml['androidPackage'];
       iosPrefix = pluginYaml['iosPrefix'] ?? '';
-      macosPrefix = pluginYaml['macosPrefix'] ?? '';
+      // TODO(stuartmorgan): Add |?? ''| here as well once this isn't used as
+      // an indicator of macOS support, see https://github.com/flutter/flutter/issues/33597
+      macosPrefix = pluginYaml['macosPrefix'];
       pluginClass = pluginYaml['pluginClass'];
     }
     return Plugin(
@@ -179,14 +182,14 @@
   _renderTemplateToFile(_androidPluginRegistryTemplate, context, registryPath);
 }
 
-const String _iosPluginRegistryHeaderTemplate = '''//
+const String _cocoaPluginRegistryHeaderTemplate = '''//
 //  Generated file. Do not edit.
 //
 
 #ifndef GeneratedPluginRegistrant_h
 #define GeneratedPluginRegistrant_h
 
-#import <Flutter/Flutter.h>
+#import <{{framework}}/{{framework}}.h>
 
 @interface GeneratedPluginRegistrant : NSObject
 + (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
@@ -195,7 +198,7 @@
 #endif /* GeneratedPluginRegistrant_h */
 ''';
 
-const String _iosPluginRegistryImplementationTemplate = '''//
+const String _cocoaPluginRegistryImplementationTemplate = '''//
 //  Generated file. Do not edit.
 //
 
@@ -215,7 +218,7 @@
 @end
 ''';
 
-const String _iosPluginRegistrantPodspecTemplate = '''
+const String _pluginRegistrantPodspecTemplate = '''
 #
 # Generated file, do not edit.
 #
@@ -230,11 +233,11 @@
   s.homepage         = 'https://flutter.dev'
   s.license          = { :type => 'BSD' }
   s.author           = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
-  s.ios.deployment_target = '8.0'
+  s.{{os}}.deployment_target = '{{deploymentTarget}}'
   s.source_files =  "Classes", "Classes/**/*.{h,m}"
   s.source           = { :path => '.' }
   s.public_header_files = './Classes/**/*.h'
-  s.dependency 'Flutter'
+  s.dependency '{{framework}}'
   {{#plugins}}
   s.dependency '{{name}}'
   {{/plugins}}
@@ -250,36 +253,64 @@
     'class': p.pluginClass,
   }).toList();
   final Map<String, dynamic> context = <String, dynamic>{
+    'os': 'ios',
+    'deploymentTarget': '8.0',
+    'framework': 'Flutter',
     'plugins': iosPlugins,
   };
-
   final String registryDirectory = project.ios.pluginRegistrantHost.path;
+  return await _writeCocoaPluginRegistrant(project, context, registryDirectory);
+}
+
+Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
+  // TODO(stuartmorgan): Replace macosPrefix check with formal metadata check,
+  // see https://github.com/flutter/flutter/issues/33597.
+  final List<Map<String, dynamic>> macosPlugins = plugins
+      .where((Plugin p) => p.pluginClass != null && p.macosPrefix != null)
+      .map<Map<String, dynamic>>((Plugin p) => <String, dynamic>{
+    'name': p.name,
+    'prefix': p.macosPrefix,
+    'class': p.pluginClass,
+  }).toList();
+  final Map<String, dynamic> context = <String, dynamic>{
+    'os': 'macos',
+    'deploymentTarget': '10.13',
+    'framework': 'FlutterMacOS',
+    'plugins': macosPlugins,
+  };
+  final String registryDirectory = project.macos.managedDirectory.path;
+  return await _writeCocoaPluginRegistrant(project, context, registryDirectory);
+}
+
+Future<void> _writeCocoaPluginRegistrant(FlutterProject project,
+    Map<String, dynamic> templateContext, String registryDirectory) async {
+
   if (project.isModule) {
     final String registryClassesDirectory = fs.path.join(registryDirectory, 'Classes');
     _renderTemplateToFile(
-      _iosPluginRegistrantPodspecTemplate,
-      context,
+      _pluginRegistrantPodspecTemplate,
+      templateContext,
       fs.path.join(registryDirectory, 'FlutterPluginRegistrant.podspec'),
     );
     _renderTemplateToFile(
-      _iosPluginRegistryHeaderTemplate,
-      context,
+      _cocoaPluginRegistryHeaderTemplate,
+      templateContext,
       fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.h'),
     );
     _renderTemplateToFile(
-      _iosPluginRegistryImplementationTemplate,
-      context,
+      _cocoaPluginRegistryImplementationTemplate,
+      templateContext,
       fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.m'),
     );
   } else {
     _renderTemplateToFile(
-      _iosPluginRegistryHeaderTemplate,
-      context,
+      _cocoaPluginRegistryHeaderTemplate,
+      templateContext,
       fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.h'),
     );
     _renderTemplateToFile(
-      _iosPluginRegistryImplementationTemplate,
-      context,
+      _cocoaPluginRegistryImplementationTemplate,
+      templateContext,
       fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.m'),
     );
   }
@@ -317,17 +348,25 @@
   if ((checkProjects && project.ios.existsSync()) || !checkProjects) {
     await _writeIOSPluginRegistrant(project, plugins);
   }
-  if (!project.isModule && ((project.ios.hostAppRoot.existsSync() && checkProjects) || !checkProjects)) {
+  // TODO(stuartmorgan): Revisit the condition here once the plans for handling
+  // desktop in existing projects are in place. For now, ignore checkProjects
+  // on desktop and always treat it as true.
+  if (flutterDesktopEnabled && project.macos.existsSync()) {
+    await _writeMacOSPluginRegistrant(project, plugins);
+  }
+  for (final XcodeBasedProject subproject in <XcodeBasedProject>[project.ios, project.macos]) {
+  if (!project.isModule && (!checkProjects || subproject.existsSync())) {
     final CocoaPods cocoaPods = CocoaPods();
     if (plugins.isNotEmpty) {
-      cocoaPods.setupPodfile(project.ios);
+      cocoaPods.setupPodfile(subproject);
     }
     /// The user may have a custom maintained Podfile that they're running `pod install`
     /// on themselves.
-    else if (project.ios.podfile.existsSync() && project.ios.podfileLock.existsSync()) {
-      cocoaPods.addPodsDependencyToFlutterXcconfig(project.ios);
+    else if (subproject.podfile.existsSync() && subproject.podfileLock.existsSync()) {
+      cocoaPods.addPodsDependencyToFlutterXcconfig(subproject);
     }
   }
+  }
 }
 
 /// Returns whether the specified Flutter [project] has any plugin dependencies.
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