mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
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:
parent
27876e09be
commit
3ebebebb8d
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
46
packages/flutter_tools/lib/src/macos/cocoapod_utils.dart
Normal file
46
packages/flutter_tools/lib/src/macos/cocoapod_utils.dart
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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()) {
|
||||
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<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');
|
||||
}
|
||||
|
|
|
@ -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,17 +348,25 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the specified Flutter [project] has any plugin dependencies.
|
||||
|
|
|
@ -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
|
||||
|
|
79
packages/flutter_tools/templates/cocoapods/Podfile-macos
Normal file
79
packages/flutter_tools/templates/cocoapods/Podfile-macos
Normal 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
|
|
@ -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>{
|
||||
|
|
Loading…
Reference in a new issue