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.
This commit is contained in:
stuartmorgan 2019-05-31 13:19:44 -07:00 committed by GitHub
parent 27876e09be
commit 3ebebebb8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 373 additions and 127 deletions

View file

@ -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<XcodeBuildResult> 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: <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',

View file

@ -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<void> 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) {

View file

@ -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();
}
}

View file

@ -100,18 +100,18 @@ class CocoaPods {
}
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 @@ 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()) {
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<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 @@ class CocoaPods {
}
}
if (result.exitCode != 0) {
invalidatePodInstallOutput(iosProject);
invalidatePodInstallOutput(xcodeProject);
_diagnosePodInstallFailure(result);
throwToolExit('Error running pod install');
}

View file

@ -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<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin>
_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 @@ 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<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug
'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,15 +348,23 @@ Future<void> 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 <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);
}
}
}
}

View file

@ -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<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

View file

@ -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

View file

@ -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(
<String>['pod', '--version'],
workingDirectory: anyNamed('workingDirectory'),
@ -81,6 +86,11 @@ void main() {
workingDirectory: 'project/ios',
environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'},
)).thenAnswer((_) async => exitsHappy());
when(mockProcessManager.run(
<String>['pod', 'install', '--verbose'],
workingDirectory: 'project/macos',
environment: <String, String>{'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: <Type, Generator>{
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: <Type, Generator>{
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: <Type, Generator>{
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(<String>['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: <Type, Generator>{