From 3ebebebb8dfb1825dc3547fbacacfd9d26d62e0f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 31 May 2019 13:19:44 -0700 Subject: [PATCH] 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. --- packages/flutter_tools/lib/src/ios/mac.dart | 28 +---- .../lib/src/macos/build_macos.dart | 3 + .../lib/src/macos/cocoapod_utils.dart | 46 +++++++ .../lib/src/macos/cocoapods.dart | 68 +++++----- packages/flutter_tools/lib/src/plugins.dart | 83 ++++++++---- packages/flutter_tools/lib/src/project.dart | 118 +++++++++++++++--- .../{Podfile-objc => Podfile-ios-objc} | 0 .../{Podfile-swift => Podfile-ios-swift} | 0 .../templates/cocoapods/Podfile-macos | 79 ++++++++++++ .../test/macos/cocoapods_test.dart | 75 ++++++----- 10 files changed, 373 insertions(+), 127 deletions(-) create mode 100644 packages/flutter_tools/lib/src/macos/cocoapod_utils.dart rename packages/flutter_tools/templates/cocoapods/{Podfile-objc => Podfile-ios-objc} (100%) rename packages/flutter_tools/templates/cocoapods/{Podfile-swift => Podfile-ios-swift} (100%) create mode 100644 packages/flutter_tools/templates/cocoapods/Podfile-macos diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 1b7a2e27108..59a0153b9c2 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -10,7 +10,6 @@ import '../application_package.dart'; 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 '../base/utils.dart'; 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 @@ Future buildXcodeProject({ 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: [ - app.project.xcodeProjectInfoFile.path, - app.project.podfile.path, - app.project.generatedXcodePropertiesFile.path, - ], - properties: {}, - ); - - 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 buildCommands = [ '/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 07c2c8c3265..12d2787aca0 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 '../convert.dart'; 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 @@ Future buildMacOS(FlutterProject flutterProject, BuildInfo buildInfo) asyn 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 00000000000..aa7f70106eb --- /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 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: [ + xcodeProject.xcodeProjectInfoFile.path, + xcodeProject.podfile.path, + xcodeProject.generatedXcodePropertiesFile.path, + ], + properties: {}, + ); + + 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 c85555ceec2..868b0e98c49 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart @@ -100,18 +100,18 @@ class CocoaPods { } Future 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 @@ class CocoaPods { 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 @@ class CocoaPods { } /// 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 @@ class CocoaPods { // 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 @@ class CocoaPods { || podfileLockFile.readAsStringSync() != manifestLockFile.readAsStringSync(); } - Future _runPodInstall(IosProject iosProject, String engineDirectory) async { + Future _runPodInstall(XcodeBasedProject xcodeProject, String engineDirectory) async { final Status status = logger.startProgress('Running pod install...', timeout: timeoutConfiguration.slowOperation); final ProcessResult result = await processManager.run( ['pod', 'install', '--verbose'], - workingDirectory: iosProject.hostAppRoot.path, + workingDirectory: fs.path.dirname(xcodeProject.podfile.path), environment: { // For backward compatibility with previously created Podfile only. 'FLUTTER_FRAMEWORK_DIR': engineDirectory, @@ -278,7 +284,7 @@ class CocoaPods { } } 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 e8c09058aae..5883d36bb3d 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -9,6 +9,7 @@ import 'package:yaml/yaml.dart'; 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 @@ class Plugin { 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 @@ Future _writeAndroidPluginRegistrant(FlutterProject project, List _renderTemplateToFile(_androidPluginRegistryTemplate, context, registryPath); } -const String _iosPluginRegistryHeaderTemplate = '''// +const String _cocoaPluginRegistryHeaderTemplate = '''// // Generated file. Do not edit. // #ifndef GeneratedPluginRegistrant_h #define GeneratedPluginRegistrant_h -#import +#import <{{framework}}/{{framework}}.h> @interface GeneratedPluginRegistrant : NSObject + (void)registerWithRegistry:(NSObject*)registry; @@ -195,7 +198,7 @@ const String _iosPluginRegistryHeaderTemplate = '''// #endif /* GeneratedPluginRegistrant_h */ '''; -const String _iosPluginRegistryImplementationTemplate = '''// +const String _cocoaPluginRegistryImplementationTemplate = '''// // Generated file. Do not edit. // @@ -215,7 +218,7 @@ const String _iosPluginRegistryImplementationTemplate = '''// @end '''; -const String _iosPluginRegistrantPodspecTemplate = ''' +const String _pluginRegistrantPodspecTemplate = ''' # # Generated file, do not edit. # @@ -230,11 +233,11 @@ Depends on all your plugins, and provides a function to register them. 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 @@ Future _writeIOSPluginRegistrant(FlutterProject project, List plug 'class': p.pluginClass, }).toList(); final Map context = { + 'os': 'ios', + 'deploymentTarget': '8.0', + 'framework': 'Flutter', 'plugins': iosPlugins, }; - final String registryDirectory = project.ios.pluginRegistrantHost.path; + return await _writeCocoaPluginRegistrant(project, context, registryDirectory); +} + +Future _writeMacOSPluginRegistrant(FlutterProject project, List plugins) async { + // TODO(stuartmorgan): Replace macosPrefix check with formal metadata check, + // see https://github.com/flutter/flutter/issues/33597. + final List> macosPlugins = plugins + .where((Plugin p) => p.pluginClass != null && p.macosPrefix != null) + .map>((Plugin p) => { + 'name': p.name, + 'prefix': p.macosPrefix, + 'class': p.pluginClass, + }).toList(); + final Map context = { + 'os': 'macos', + 'deploymentTarget': '10.13', + 'framework': 'FlutterMacOS', + 'plugins': macosPlugins, + }; + final String registryDirectory = project.macos.managedDirectory.path; + return await _writeCocoaPluginRegistrant(project, context, registryDirectory); +} + +Future _writeCocoaPluginRegistrant(FlutterProject project, + Map 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 @@ Future injectPlugins(FlutterProject project, {bool checkProjects = false}) 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 [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 970d9eeff60..26353ef1c24 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -13,6 +13,7 @@ import 'base/file_system.dart'; 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 @@ class FlutterProject { 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 @@ class FlutterProject { } } +/// 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 @@ class IosProject { /// 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 @@ class IosProject { /// 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 @@ class IosProject { 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 @@ class IosProject { await injectPlugins(parent); } + @override File get generatedXcodePropertiesFile => _flutterLibRoot.childDirectory('Flutter').childFile('Generated.xcconfig'); Directory get pluginRegistrantHost { @@ -573,16 +619,18 @@ Match _firstMatchInFile(File file, RegExp regExp) { } /// 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 @@ class MacOSProject { /// 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 ensureReadyForPlatformSpecificTooling() async { + // TODO(stuartmorgan): Add create-from-template logic here. + await _updateGeneratedXcodeConfigIfNeeded(); + } + + Future _updateGeneratedXcodeConfigIfNeeded() async { + if (Cache.instance.isOlderThanToolsStamp(generatedXcodePropertiesFile)) { + await xcode.updateGeneratedXcodeProperties( + project: parent, + buildInfo: BuildInfo.debug, + useMacOSConfig: true, + setSymroot: false, + ); + } + } } /// The Windows sub project diff --git a/packages/flutter_tools/templates/cocoapods/Podfile-objc b/packages/flutter_tools/templates/cocoapods/Podfile-ios-objc similarity index 100% rename from packages/flutter_tools/templates/cocoapods/Podfile-objc rename to packages/flutter_tools/templates/cocoapods/Podfile-ios-objc diff --git a/packages/flutter_tools/templates/cocoapods/Podfile-swift b/packages/flutter_tools/templates/cocoapods/Podfile-ios-swift similarity index 100% rename from packages/flutter_tools/templates/cocoapods/Podfile-swift rename to packages/flutter_tools/templates/cocoapods/Podfile-ios-swift diff --git a/packages/flutter_tools/templates/cocoapods/Podfile-macos b/packages/flutter_tools/templates/cocoapods/Podfile-macos new file mode 100644 index 00000000000..ec3967f31cf --- /dev/null +++ b/packages/flutter_tools/templates/cocoapods/Podfile-macos @@ -0,0 +1,79 @@ +platform :osx, '10.13' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + # TODO: Add Profile support to projects. + #'Profile' => :release, + 'Release' => :release, +} + +def parse_KV_file(file, separator='=') + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return []; + end + pods_ary = [] + skip_line_start_symbols = ["#", "/"] + File.foreach(file_abs_path) { |line| + next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } + plugin = line.split(pattern=separator) + if plugin.length == 2 + podname = plugin[0].strip() + path = plugin[1].strip() + podpath = File.expand_path("#{path}", file_abs_path) + pods_ary.push({:name => podname, :path => podpath}); + else + puts "Invalid plugin specification: #{line}" + end + } + return pods_ary +end + +def pubspec_supports_macos(file) + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return false; + end + File.foreach(file_abs_path) { |line| + # TODO(stuartmorgan): Use formal platform declaration once it exists, + # see https://github.com/flutter/flutter/issues/33597. + return true if line =~ /^\s*macosPrefix:/ + } + return false +end + +target 'Runner' do + # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock + # referring to absolute paths on developers' machines. + ephemeral_dir = File.join('Flutter', 'ephemeral') + symlink_dir = File.join(ephemeral_dir, '.symlinks') + symlink_plugins_dir = File.join(symlink_dir, 'plugins') + system("rm -rf #{symlink_dir}") + system("mkdir -p #{symlink_plugins_dir}") + + # Flutter Pods + generated_xcconfig = parse_KV_file(File.join(ephemeral_dir, 'Flutter-Generated.xcconfig')) + if generated_xcconfig.empty? + puts "Flutter-Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." + end + generated_xcconfig.map { |p| + if p[:name] == 'FLUTTER_FRAMEWORK_DIR' + symlink = File.join(symlink_dir, 'flutter') + File.symlink(File.dirname(p[:path]), symlink) + pod 'FlutterMacOS', :path => File.join(symlink, File.basename(p[:path])) + end + } + + # Plugin Pods + plugin_pods = parse_KV_file('../.flutter-plugins') + plugin_pods.map { |p| + symlink = File.join(symlink_plugins_dir, p[:name]) + File.symlink(p[:path], symlink) + if pubspec_supports_macos(File.join(symlink, 'pubspec.yaml')) + pod p[:name], :path => File.join(symlink, 'macos') + end + } +end diff --git a/packages/flutter_tools/test/macos/cocoapods_test.dart b/packages/flutter_tools/test/macos/cocoapods_test.dart index 1de1adf323f..82b8aa9a9e7 100644 --- a/packages/flutter_tools/test/macos/cocoapods_test.dart +++ b/packages/flutter_tools/test/macos/cocoapods_test.dart @@ -62,15 +62,20 @@ void main() { cocoaPodsUnderTest = CocoaPods(); pretendPodVersionIs('1.5.0'); fs.file(fs.path.join( - Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-objc', + Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-objc', )) ..createSync(recursive: true) - ..writeAsStringSync('Objective-C podfile template'); + ..writeAsStringSync('Objective-C iOS podfile template'); fs.file(fs.path.join( - Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-swift', + Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-swift', )) ..createSync(recursive: true) - ..writeAsStringSync('Swift podfile template'); + ..writeAsStringSync('Swift iOS podfile template'); + fs.file(fs.path.join( + Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-macos', + )) + ..createSync(recursive: true) + ..writeAsStringSync('macOS podfile template'); when(mockProcessManager.run( ['pod', '--version'], workingDirectory: anyNamed('workingDirectory'), @@ -81,6 +86,11 @@ void main() { workingDirectory: 'project/ios', environment: {'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, )).thenAnswer((_) async => exitsHappy()); + when(mockProcessManager.run( + ['pod', 'install', '--verbose'], + workingDirectory: 'project/macos', + environment: {'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, + )).thenAnswer((_) async => exitsHappy()); }); group('Evaluate installation', () { @@ -145,7 +155,7 @@ void main() { testUsingContext('creates objective-c Podfile when not present', () async { cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios); - expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C podfile template'); + expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C iOS podfile template'); }, overrides: { FileSystem: () => fs, }); @@ -159,12 +169,21 @@ void main() { final FlutterProject project = FlutterProject.fromPath('project'); cocoaPodsUnderTest.setupPodfile(project.ios); - expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift podfile template'); + expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift iOS podfile template'); }, overrides: { FileSystem: () => fs, XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, }); + testUsingContext('creates macOS Podfile when not present', () async { + projectUnderTest.macos.xcodeProject.createSync(recursive: true); + cocoaPodsUnderTest.setupPodfile(projectUnderTest.macos); + + expect(projectUnderTest.macos.podfile.readAsStringSync(), 'macOS podfile template'); + }, overrides: { + FileSystem: () => fs, + }); + testUsingContext('does not recreate Podfile when already present', () async { projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile'); @@ -250,8 +269,8 @@ void main() { pretendPodIsNotInstalled(); projectUnderTest.ios.podfile.createSync(); final bool didInstall = await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', ); verifyNever(mockProcessManager.run( argThat(containsAllInOrder(['pod', 'install'])), @@ -269,8 +288,8 @@ void main() { testUsingContext('throws, if Podfile is missing.', () async { try { await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', ); fail('ToolExit expected'); } catch(e) { @@ -316,8 +335,8 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by )); try { await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', ); fail('ToolExit expected'); } catch (e) { @@ -340,8 +359,8 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); final bool didInstall = await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', dependenciesChanged: false, ); expect(didInstall, isTrue); @@ -363,8 +382,8 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by ..createSync() ..writeAsStringSync('Existing lock file.'); final bool didInstall = await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', dependenciesChanged: false, ); expect(didInstall, isTrue); @@ -392,8 +411,8 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by ..createSync(recursive: true) ..writeAsStringSync('Different lock file.'); final bool didInstall = await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', dependenciesChanged: false, ); expect(didInstall, isTrue); @@ -421,8 +440,8 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); final bool didInstall = await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', dependenciesChanged: true, ); expect(didInstall, isTrue); @@ -453,8 +472,8 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by projectUnderTest.ios.podfile ..writeAsStringSync('Updated Podfile'); await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', dependenciesChanged: false, ); verify(mockProcessManager.run( @@ -481,8 +500,8 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); final bool didInstall = await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', dependenciesChanged: false, ); expect(didInstall, isFalse); @@ -520,8 +539,8 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by try { await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', dependenciesChanged: true, ); fail('Tool throw expected when pod install fails'); @@ -557,8 +576,8 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by environment: environment, )).thenAnswer((_) async => exitsHappy()); final bool success = await cocoaPodsUnderTest.processPods( - iosProject: projectUnderTest.ios, - iosEngineDir: 'engine/path', + xcodeProject: projectUnderTest.ios, + engineDir: 'engine/path', ); expect(success, true); }, overrides: {