From 8be198d8a086bc26a9b73536f5ebd7ee327a4f01 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 28 Jun 2018 10:03:16 +0200 Subject: [PATCH] Add iOS module template (#18830) Add iOS module template This will enable integration of flutter-views into existing iOS project. --- .../ios/Runner.xcodeproj/project.pbxproj | 10 ++ packages/flutter_tools/bin/xcode_backend.sh | 24 +++- packages/flutter_tools/lib/src/cache.dart | 13 ++ .../lib/src/commands/create.dart | 22 ++-- .../lib/src/commands/inject_plugins.dart | 6 +- .../lib/src/commands/packages.dart | 3 +- .../flutter_tools/lib/src/ios/cocoapods.dart | 7 +- packages/flutter_tools/lib/src/ios/mac.dart | 7 +- .../flutter_tools/lib/src/ios/xcodeproj.dart | 50 ++++---- packages/flutter_tools/lib/src/plugins.dart | 117 +++++++++++++----- packages/flutter_tools/lib/src/project.dart | 55 ++++++-- .../ios/AppFrameworkInfo.plist.copy.tmpl | 26 ++++ .../templates/module/ios/README.md.copy.tmpl | 2 + .../module/ios/podhelper.rb.copy.tmpl | 77 ++++++++++++ .../test/ios/cocoapods_test.dart | 32 +++-- .../test/ios/xcodeproj_test.dart | 47 ++++--- packages/flutter_tools/test/project_test.dart | 11 +- 17 files changed, 395 insertions(+), 114 deletions(-) create mode 100644 packages/flutter_tools/templates/module/ios/AppFrameworkInfo.plist.copy.tmpl create mode 100644 packages/flutter_tools/templates/module/ios/README.md.copy.tmpl create mode 100644 packages/flutter_tools/templates/module/ios/podhelper.rb.copy.tmpl diff --git a/examples/flutter_gallery/ios/Runner.xcodeproj/project.pbxproj b/examples/flutter_gallery/ios/Runner.xcodeproj/project.pbxproj index edbd91c91ee..d35899101c7 100644 --- a/examples/flutter_gallery/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/flutter_gallery/ios/Runner.xcodeproj/project.pbxproj @@ -182,6 +182,8 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = EQHXZ8M8AV; + ProvisioningStyle = Manual; }; }; }; @@ -427,6 +429,8 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = EQHXZ8M8AV; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -440,6 +444,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.flutter.examples.gallery; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "89aa7622-6c03-475d-baed-b7ebcc3c63b2"; + PROVISIONING_PROFILE_SPECIFIER = "Google Development"; }; name = Debug; }; @@ -448,6 +454,8 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = EQHXZ8M8AV; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -461,6 +469,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.flutter.examples.gallery; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "89aa7622-6c03-475d-baed-b7ebcc3c63b2"; + PROVISIONING_PROFILE_SPECIFIER = "Google Development"; }; name = Release; }; diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh index 69770b06cf7..2041c4d41fb 100755 --- a/packages/flutter_tools/bin/xcode_backend.sh +++ b/packages/flutter_tools/bin/xcode_backend.sh @@ -83,10 +83,20 @@ BuildApp() { RunCommand mkdir -p -- "$derived_dir" AssertExists "$derived_dir" - RunCommand rm -rf -- "${derived_dir}/Flutter.framework" RunCommand rm -rf -- "${derived_dir}/App.framework" - RunCommand cp -r -- "${framework_path}/Flutter.framework" "${derived_dir}" - RunCommand find "${derived_dir}/Flutter.framework" -type f -exec chmod a-w "{}" \; + + if [[ -e "${project_path}/.ios" ]]; then + RunCommand rm -rf -- "${derived_dir}/engine" + mkdir "${derived_dir}/engine" + RunCommand cp -r -- "${framework_path}/Flutter.podspec" "${derived_dir}/engine" + RunCommand cp -r -- "${framework_path}/Flutter.framework" "${derived_dir}/engine" + RunCommand find "${derived_dir}/engine/Flutter.framework" -type f -exec chmod a-w "{}" \; + else + RunCommand rm -rf -- "${derived_dir}/Flutter.framework" + RunCommand cp -r -- "${framework_path}/Flutter.framework" "${derived_dir}" + RunCommand find "${derived_dir}/Flutter.framework" -type f -exec chmod a-w "{}" \; + fi + RunCommand pushd "${project_path}" > /dev/null AssertExists "${target_path}" @@ -155,7 +165,13 @@ BuildApp() { -install_name '@rpath/App.framework/App' \ -o "${derived_dir}/App.framework/App" -)" fi - RunCommand cp -- "${project_path}/ios/Flutter/AppFrameworkInfo.plist" "${derived_dir}/App.framework/Info.plist" + + local plistPath="${project_path}/ios/Flutter/AppFrameworkInfo.plist" + if [[ -e "${project_path}/.ios" ]]; then + plistPath="${project_path}/.ios/Flutter/AppFrameworkInfo.plist" + fi + + RunCommand cp -- "$plistPath" "${derived_dir}/App.framework/Info.plist" local precompilation_flag="" if [[ "$CURRENT_ARCH" != "x86_64" ]] && [[ "$build_mode" != "debug" ]]; then diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index 84915aadfb1..b004f2067cb 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart @@ -169,6 +169,19 @@ class Cache { return fs.file(fs.path.join(getRoot().path, '$artifactName.stamp')); } + /// Returns `true` if either [file] is older than the tools stamp or if + /// [file] doesn't exist. + bool fileOlderThanToolsStamp(File file) { + if (!file.existsSync()) { + return true; + } + final File flutterToolsStamp = getStampFileFor('flutter_tools'); + return flutterToolsStamp.existsSync() && + flutterToolsStamp + .lastModifiedSync() + .isAfter(file.lastModifiedSync()); + } + bool isUpToDate() => _artifacts.every((CachedArtifact artifact) => artifact.isUpToDate()); Future getThirdPartyFile(String urlStr, String serviceName) async { diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 3b3aefb804b..0c69f1891cb 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -233,20 +233,20 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit } } - Future _generateModule(String dirPath, Map templateContext) async { + Future _generateModule(String path, Map templateContext) async { int generatedCount = 0; final String description = argResults.wasParsed('description') ? argResults['description'] : 'A new flutter module project.'; templateContext['description'] = description; - generatedCount += _renderTemplate(fs.path.join('module', 'common'), dirPath, templateContext); + generatedCount += _renderTemplate(fs.path.join('module', 'common'), path, templateContext); if (argResults['pub']) { await pubGet( context: PubContext.create, - directory: dirPath, + directory: path, offline: argResults['offline'], ); - final FlutterProject project = new FlutterProject(fs.directory(dirPath)); + final FlutterProject project = new FlutterProject.fromPath(path); await project.ensureReadyForPlatformSpecificTooling(); } return generatedCount; @@ -303,23 +303,23 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit return generatedCount; } - Future _generateApp(String appPath, Map templateContext) async { + Future _generateApp(String projectPath, Map templateContext) async { int generatedCount = 0; - generatedCount += _renderTemplate('create', appPath, templateContext); - generatedCount += _injectGradleWrapper(appPath); + generatedCount += _renderTemplate('create', projectPath, templateContext); + generatedCount += _injectGradleWrapper(projectPath); if (argResults['with-driver-test']) { - final String testPath = fs.path.join(appPath, 'test_driver'); + final String testPath = fs.path.join(projectPath, 'test_driver'); generatedCount += _renderTemplate('driver', testPath, templateContext); } if (argResults['pub']) { - await pubGet(context: PubContext.create, directory: appPath, offline: argResults['offline']); - await new FlutterProject(fs.directory(appPath)).ensureReadyForPlatformSpecificTooling(); + await pubGet(context: PubContext.create, directory: projectPath, offline: argResults['offline']); + await new FlutterProject.fromPath(projectPath).ensureReadyForPlatformSpecificTooling(); } if (android_sdk.androidSdk != null) - await gradle.updateLocalProperties(projectPath: appPath); + await gradle.updateLocalProperties(projectPath: projectPath); return generatedCount; } diff --git a/packages/flutter_tools/lib/src/commands/inject_plugins.dart b/packages/flutter_tools/lib/src/commands/inject_plugins.dart index 8d8b47cfebf..832b64256dd 100644 --- a/packages/flutter_tools/lib/src/commands/inject_plugins.dart +++ b/packages/flutter_tools/lib/src/commands/inject_plugins.dart @@ -4,6 +4,8 @@ import 'dart:async'; +import '../base/file_system.dart'; +import '../flutter_manifest.dart'; import '../globals.dart'; import '../plugins.dart'; import '../runner/flutter_command.dart'; @@ -24,7 +26,9 @@ class InjectPluginsCommand extends FlutterCommand { @override Future runCommand() async { - injectPlugins(); + final String projectPath = fs.currentDirectory.path; + final FlutterManifest manifest = await FlutterManifest.createFromPath(projectPath); + injectPlugins(projectPath: projectPath, manifest: manifest); final bool result = hasPlugins(); if (result) { printStatus('GeneratedPluginRegistrants successfully written.'); diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart index 471e0a6a080..ff7144680bf 100644 --- a/packages/flutter_tools/lib/src/commands/packages.dart +++ b/packages/flutter_tools/lib/src/commands/packages.dart @@ -5,7 +5,6 @@ import 'dart:async'; import '../base/common.dart'; -import '../base/file_system.dart'; import '../base/os.dart'; import '../dart/pub.dart'; import '../project.dart'; @@ -81,7 +80,7 @@ class PackagesGetCommand extends FlutterCommand { } await _runPubGet(target); - final FlutterProject rootProject = new FlutterProject(fs.directory(target)); + final FlutterProject rootProject = new FlutterProject.fromPath(target); await rootProject.ensureReadyForPlatformSpecificTooling(); // Get/upgrade packages in example app as well diff --git a/packages/flutter_tools/lib/src/ios/cocoapods.dart b/packages/flutter_tools/lib/src/ios/cocoapods.dart index 40b6e2ba622..99048129e35 100644 --- a/packages/flutter_tools/lib/src/ios/cocoapods.dart +++ b/packages/flutter_tools/lib/src/ios/cocoapods.dart @@ -15,6 +15,7 @@ import '../base/process.dart'; import '../base/process_manager.dart'; import '../base/version.dart'; import '../cache.dart'; +import '../flutter_manifest.dart'; import '../globals.dart'; import 'xcodeproj.dart'; @@ -150,11 +151,14 @@ class CocoaPods { /// Ensures the `ios` sub-project of the Flutter project at [appDirectory] /// contains a suitable `Podfile` and that its `Flutter/Xxx.xcconfig` files /// include pods configuration. - void setupPodfile(String appDirectory) { + void setupPodfile(String appDirectory, FlutterManifest manifest) { if (!xcodeProjectInterpreter.isInstalled) { // Don't do anything for iOS when host platform doesn't support it. return; } + if (!fs.directory(fs.path.join(appDirectory, 'ios')).existsSync()) { + return; + } final String podfilePath = fs.path.join(appDirectory, 'ios', 'Podfile'); if (!fs.file(podfilePath).existsSync()) { final bool isSwift = xcodeProjectInterpreter.getBuildSettings( @@ -171,6 +175,7 @@ class CocoaPods { )); podfileTemplate.copySync(podfilePath); } + _addPodsDependencyToFlutterXcconfig(appDirectory, 'Debug'); _addPodsDependencyToFlutterXcconfig(appDirectory, 'Release'); } diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 44c4f144327..2860303a8b7 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -20,6 +20,7 @@ import '../base/process.dart'; import '../base/process_manager.dart'; import '../base/utils.dart'; import '../build_info.dart'; +import '../flutter_manifest.dart'; import '../globals.dart'; import '../plugins.dart'; import '../services.dart'; @@ -242,11 +243,15 @@ Future buildXcodeProject({ final Directory appDirectory = fs.directory(app.appDirectory); await _addServicesToBundle(appDirectory); - await updateGeneratedXcodeProperties( + final FlutterManifest manifest = await FlutterManifest.createFromPath( + fs.currentDirectory.childFile('pubspec.yaml').path, + ); + updateGeneratedXcodeProperties( projectPath: fs.currentDirectory.path, buildInfo: buildInfo, targetOverride: targetOverride, previewDart2: buildInfo.previewDart2, + manifest: manifest, ); if (hasPlugins()) { diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 8099b894177..b9bf73d0c7a 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -2,12 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:meta/meta.dart'; import '../artifacts.dart'; -import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; @@ -28,21 +25,30 @@ String flutterFrameworkDir(BuildMode mode) { return fs.path.normalize(fs.path.dirname(artifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, mode))); } -String _generatedXcodePropertiesPath(String projectPath) { - return fs.path.join(projectPath, 'ios', 'Flutter', 'Generated.xcconfig'); +String _generatedXcodePropertiesPath({@required String projectPath, @required FlutterManifest manifest}) { + if (manifest.isModule) { + return fs.path.join(projectPath, '.ios', 'Flutter', 'Generated.xcconfig'); + } else { + return fs.path.join(projectPath, 'ios', 'Flutter', 'Generated.xcconfig'); + } } /// Writes default Xcode properties files in the Flutter project at [projectPath], -/// if project is an iOS project and such files do not already exist. -Future generateXcodeProperties(String projectPath) async { - if (fs.isDirectorySync(fs.path.join(projectPath, 'ios'))) { - if (fs.file(_generatedXcodePropertiesPath(projectPath)).existsSync()) +/// if project is an iOS project and such files are out of date or do not +/// already exist. +void generateXcodeProperties({String projectPath, FlutterManifest manifest}) { + if (manifest.isModule || fs.isDirectorySync(fs.path.join(projectPath, 'ios'))) { + final File propertiesFile = fs.file(_generatedXcodePropertiesPath(projectPath: projectPath, manifest: manifest)); + if (!Cache.instance.fileOlderThanToolsStamp(propertiesFile)) { return; - await updateGeneratedXcodeProperties( + } + + updateGeneratedXcodeProperties( projectPath: projectPath, + manifest: manifest, buildInfo: BuildInfo.debug, targetOverride: bundle.defaultMainPath, - previewDart2: false, + previewDart2: true, ); } } @@ -51,14 +57,13 @@ Future generateXcodeProperties(String projectPath) async { /// /// targetOverride: Optional parameter, if null or unspecified the default value /// from xcode_backend.sh is used 'lib/main.dart'. -/// -/// Returns the number of files written. -Future updateGeneratedXcodeProperties({ +void updateGeneratedXcodeProperties({ @required String projectPath, + @required FlutterManifest manifest, @required BuildInfo buildInfo, String targetOverride, @required bool previewDart2, -}) async { +}) { final StringBuffer localsBuffer = new StringBuffer(); localsBuffer.writeln('// This is a generated file; do not edit or check into version control.'); @@ -81,14 +86,11 @@ Future updateGeneratedXcodeProperties({ localsBuffer.writeln('SYMROOT=\${SOURCE_ROOT}/../${getIosBuildDirectory()}'); - localsBuffer.writeln('FLUTTER_FRAMEWORK_DIR=${flutterFrameworkDir(buildInfo.mode)}'); - - final String flutterManifest = fs.path.join(projectPath, bundle.defaultManifestPath); - FlutterManifest manifest; - try { - manifest = await FlutterManifest.createFromPath(flutterManifest); - } catch (error) { - throwToolExit('Failed to load pubspec.yaml: $error'); + if (!manifest.isModule) { + // For module projects we do not want to write the FLUTTER_FRAMEWORK_DIR + // explicitly. Rather we rely on the xcode backend script and the Podfile + // logic to derive it from FLUTTER_ROOT and FLUTTER_BUILD_MODE. + localsBuffer.writeln('FLUTTER_FRAMEWORK_DIR=${flutterFrameworkDir(buildInfo.mode)}'); } final String buildName = buildInfo?.buildName ?? manifest.buildName; @@ -123,7 +125,7 @@ Future updateGeneratedXcodeProperties({ localsBuffer.writeln('TRACK_WIDGET_CREATION=true'); } - final File localsFile = fs.file(_generatedXcodePropertiesPath(projectPath)); + final File localsFile = fs.file(_generatedXcodePropertiesPath(projectPath: projectPath, manifest: manifest)); localsFile.createSync(recursive: true); localsFile.writeAsStringSync(localsBuffer.toString()); } diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index f72463c4541..e99c19a4700 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -8,9 +8,18 @@ import 'package:yaml/yaml.dart'; import 'base/file_system.dart'; import 'dart/package_map.dart'; +import 'flutter_manifest.dart'; import 'globals.dart'; import 'ios/cocoapods.dart'; +void _renderTemplateToFile(String template, dynamic context, String filePath) { + final String renderedTemplate = + new mustache.Template(template).renderString(context); + final File file = fs.file(filePath); + file.createSync(recursive: true); + file.writeAsStringSync(renderedTemplate); +} + class Plugin { final String name; final String path; @@ -60,7 +69,7 @@ Plugin _pluginFromPubspec(String name, Uri packageRoot) { return new Plugin.fromYaml(name, packageRootPath, flutterConfig['plugin']); } -List _findPlugins(String directory) { +List findPlugins(String directory) { final List plugins = []; Map packages; try { @@ -146,14 +155,9 @@ void _writeAndroidPluginRegistrant(String directory, List plugins) { 'plugins': androidPlugins, }; - final String pluginRegistry = - new mustache.Template(_androidPluginRegistryTemplate).renderString(context); final String javaSourcePath = fs.path.join(directory, 'src', 'main', 'java'); - final Directory registryDirectory = - fs.directory(fs.path.join(javaSourcePath, 'io', 'flutter', 'plugins')); - registryDirectory.createSync(recursive: true); - final File registryFile = registryDirectory.childFile('GeneratedPluginRegistrant.java'); - registryFile.writeAsStringSync(pluginRegistry); + final String registryPath = fs.path.join(javaSourcePath, 'io', 'flutter', 'plugins', 'GeneratedPluginRegistrant.java'); + _renderTemplateToFile(_androidPluginRegistryTemplate, context, registryPath); } const String _iosPluginRegistryHeaderTemplate = '''// @@ -192,7 +196,32 @@ const String _iosPluginRegistryImplementationTemplate = '''// @end '''; -void _writeIOSPluginRegistrant(String directory, List plugins) { +const String _iosPluginRegistrantPodspecTemplate = ''' +# +# Generated file, do not edit. +# + +Pod::Spec.new do |s| + s.name = 'FlutterPluginRegistrant' + s.version = '0.0.1' + s.summary = 'Registers plugins with your flutter app' + s.description = <<-DESC +Depends on all your plugins, and provides a function to register them. + DESC + s.homepage = 'https://flutter.io' + s.license = { :type => 'BSD' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.ios.deployment_target = '7.0' + s.source_files = "Classes", "Classes/**/*.{h,m}" + s.source = { :path => '.' } + s.public_header_files = './Classes/**/*.h' + {{#plugins}} + s.dependency '{{name}}' + {{/plugins}} +end +'''; + +void _writeIOSPluginRegistrant(String directory, FlutterManifest manifest, List plugins) { final List> iosPlugins = plugins .where((Plugin p) => p.pluginClass != null) .map((Plugin p) => { @@ -205,16 +234,41 @@ void _writeIOSPluginRegistrant(String directory, List plugins) { 'plugins': iosPlugins, }; - final String pluginRegistryHeader = - new mustache.Template(_iosPluginRegistryHeaderTemplate).renderString(context); - final String pluginRegistryImplementation = - new mustache.Template(_iosPluginRegistryImplementationTemplate).renderString(context); - final Directory registryDirectory = fs.directory(fs.path.join(directory, 'ios', 'Runner')); - registryDirectory.createSync(recursive: true); - final File registryHeaderFile = registryDirectory.childFile('GeneratedPluginRegistrant.h'); - registryHeaderFile.writeAsStringSync(pluginRegistryHeader); - final File registryImplementationFile = registryDirectory.childFile('GeneratedPluginRegistrant.m'); - registryImplementationFile.writeAsStringSync(pluginRegistryImplementation); + if (manifest.isModule) { + // In a module create the GeneratedPluginRegistrant as a pod to be included + // from a hosting app. + final String registryDirectory = fs.path.join(directory, 'FlutterPluginRegistrant'); + final String registryClassesDirectory = fs.path.join(registryDirectory, 'Classes'); + _renderTemplateToFile( + _iosPluginRegistrantPodspecTemplate, + context, + fs.path.join(registryDirectory, 'FlutterPluginRegistrant.podspec'), + ); + _renderTemplateToFile( + _iosPluginRegistryHeaderTemplate, + context, + fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.h'), + ); + _renderTemplateToFile( + _iosPluginRegistryImplementationTemplate, + context, + fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.m'), + ); + } else { + // For a non-module create the GeneratedPluginRegistrant as source files + // directly in the ios project. + final String runnerDirectory = fs.path.join(directory, 'Runner'); + _renderTemplateToFile( + _iosPluginRegistryHeaderTemplate, + context, + fs.path.join(runnerDirectory, 'GeneratedPluginRegistrant.h'), + ); + _renderTemplateToFile( + _iosPluginRegistryImplementationTemplate, + context, + fs.path.join(runnerDirectory, 'GeneratedPluginRegistrant.m'), + ); + } } class InjectPluginsResult{ @@ -229,22 +283,23 @@ class InjectPluginsResult{ } /// Injects plugins found in `pubspec.yaml` into the platform-specific projects. -void injectPlugins({String directory}) { - directory ??= fs.currentDirectory.path; - final List plugins = _findPlugins(directory); - final bool changed = _writeFlutterPluginsList(directory, plugins); - if (fs.isDirectorySync(fs.path.join(directory, '.android', 'Flutter'))) { - _writeAndroidPluginRegistrant(fs.path.join(directory, '.android', 'Flutter'), plugins); - } else if (fs.isDirectorySync(fs.path.join(directory, 'android', 'app'))) { - _writeAndroidPluginRegistrant(fs.path.join(directory, 'android', 'app'), plugins); +void injectPlugins({@required String projectPath, @required FlutterManifest manifest}) { + final List plugins = findPlugins(projectPath); + final bool changed = _writeFlutterPluginsList(projectPath, plugins); + if (manifest.isModule) { + _writeAndroidPluginRegistrant(fs.path.join(projectPath, '.android', 'Flutter'), plugins); + } else if (fs.isDirectorySync(fs.path.join(projectPath, 'android', 'app'))) { + _writeAndroidPluginRegistrant(fs.path.join(projectPath, 'android', 'app'), plugins); } - if (fs.isDirectorySync(fs.path.join(directory, 'ios'))) { - _writeIOSPluginRegistrant(directory, plugins); + if (manifest.isModule) { + _writeIOSPluginRegistrant(fs.path.join(projectPath, '.ios'), manifest, plugins); + } else if (fs.isDirectorySync(fs.path.join(projectPath, 'ios'))) { + _writeIOSPluginRegistrant(fs.path.join(projectPath, 'ios'), manifest, plugins); final CocoaPods cocoaPods = new CocoaPods(); if (plugins.isNotEmpty) - cocoaPods.setupPodfile(directory); + cocoaPods.setupPodfile(projectPath, manifest); if (changed) - cocoaPods.invalidatePodInstallOutput(directory); + cocoaPods.invalidatePodInstallOutput(projectPath); } } diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index dae77723867..d3f2e66a914 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -8,6 +8,7 @@ import 'dart:convert'; import 'android/gradle.dart' as gradle; import 'base/file_system.dart'; +import 'bundle.dart' as bundle; import 'cache.dart'; import 'flutter_manifest.dart'; import 'ios/xcodeproj.dart' as xcode; @@ -16,11 +17,20 @@ import 'template.dart'; /// Represents the contents of a Flutter project at the specified [directory]. class FlutterProject { + FlutterProject(this.directory); + FlutterProject.fromPath(String projectPath) : directory = fs.directory(projectPath); /// The location of this project. final Directory directory; + Future get manifest { + return _manifest ??= FlutterManifest.createFromPath( + directory.childFile(bundle.defaultManifestPath).path, + ); + } + Future _manifest; + /// Asynchronously returns the organization names found in this project as /// part of iOS product bundle identifier, Android application ID, or /// Gradle group ID. @@ -54,11 +64,17 @@ class FlutterProject { /// The generated AndroidModule sub project of this module project. AndroidModuleProject get androidModule => new AndroidModuleProject(directory.childDirectory('.android')); + /// The generated IosModule sub project of this module project. + IosModuleProject get iosModule => new IosModuleProject(directory.childDirectory('.ios')); + /// Returns true if this project has an example application - bool get hasExampleApp => directory.childDirectory('example').childFile('pubspec.yaml').existsSync(); + bool get hasExampleApp => _exampleDirectory.childFile('pubspec.yaml').existsSync(); /// The example sub project of this (package or plugin) project. - FlutterProject get example => new FlutterProject(directory.childDirectory('example')); + FlutterProject get example => new FlutterProject(_exampleDirectory); + + /// The directory that will contain the example if an example exists. + Directory get _exampleDirectory => directory.childDirectory('example'); /// Generates project files necessary to make Gradle builds work on Android /// and CocoaPods+Xcode work on iOS, for app and module projects only. @@ -68,12 +84,13 @@ class FlutterProject { if (!directory.existsSync() || hasExampleApp) { return 0; } - final FlutterManifest manifest = await FlutterManifest.createFromPath(directory.childFile('pubspec.yaml').path); + final FlutterManifest manifest = await this.manifest; if (manifest.isModule) { await androidModule.ensureReadyForPlatformSpecificTooling(manifest); + await iosModule.ensureReadyForPlatformSpecificTooling(manifest); } - injectPlugins(directory: directory.path); - await xcode.generateXcodeProperties(directory.path); + xcode.generateXcodeProperties(projectPath: directory.path, manifest: manifest); + injectPlugins(projectPath: directory.path, manifest: manifest); } } @@ -90,6 +107,25 @@ class IosProject { } } +/// Represents the contents of the .ios/ folder of a Flutter module +/// project. +class IosModuleProject { + IosModuleProject(this.directory); + + final Directory directory; + + Future ensureReadyForPlatformSpecificTooling(FlutterManifest manifest) async { + if (_shouldRegenerate()) { + final Template template = new Template.fromName(fs.path.join('module', 'ios')); + template.render(directory, {}, printStatusWhenWriting: false); + } + } + + bool _shouldRegenerate() { + return Cache.instance.fileOlderThanToolsStamp(directory.childFile('podhelper.rb')); + } +} + /// Represents the contents of the android/ folder of a Flutter project. class AndroidProject { static final RegExp _applicationIdPattern = new RegExp('^\\s*applicationId\\s+[\'\"](.*)[\'\"]\\s*\$'); @@ -129,14 +165,7 @@ class AndroidModuleProject { } bool _shouldRegenerate() { - final File flutterToolsStamp = Cache.instance.getStampFileFor('flutter_tools'); - final File buildDotGradleFile = directory.childFile('build.gradle'); - if (!buildDotGradleFile.existsSync()) - return true; - return flutterToolsStamp.existsSync() && - flutterToolsStamp - .lastModifiedSync() - .isAfter(buildDotGradleFile.lastModifiedSync()); + return Cache.instance.fileOlderThanToolsStamp(directory.childFile('build.gradle')); } } diff --git a/packages/flutter_tools/templates/module/ios/AppFrameworkInfo.plist.copy.tmpl b/packages/flutter_tools/templates/module/ios/AppFrameworkInfo.plist.copy.tmpl new file mode 100644 index 00000000000..9367d483e44 --- /dev/null +++ b/packages/flutter_tools/templates/module/ios/AppFrameworkInfo.plist.copy.tmpl @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/packages/flutter_tools/templates/module/ios/README.md.copy.tmpl b/packages/flutter_tools/templates/module/ios/README.md.copy.tmpl new file mode 100644 index 00000000000..e8a3521c65a --- /dev/null +++ b/packages/flutter_tools/templates/module/ios/README.md.copy.tmpl @@ -0,0 +1,2 @@ +This directory contains support code for embedding the Flutter project in an iOS app. +It should not be edited or checked in. diff --git a/packages/flutter_tools/templates/module/ios/podhelper.rb.copy.tmpl b/packages/flutter_tools/templates/module/ios/podhelper.rb.copy.tmpl new file mode 100644 index 00000000000..b3b9f832524 --- /dev/null +++ b/packages/flutter_tools/templates/module/ios/podhelper.rb.copy.tmpl @@ -0,0 +1,77 @@ +# This file should be used from the target section of the host-app's Podfile like this: +# ``` +# target 'host' do +# flutter_application_path = /"(.*)\/.ios\/Generated.xcconfig"/.match(File.read("./Flutter/FlutterConfig.xcconfig"))[1] +# eval(File.read(File.join(flutter_application_path, '.ios', 'podhelper.rb'))) +# end +# ``` + +def parse_KV_file(file, separator='=') + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return []; + end + pods_array = [] + 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_array.push({:name => podname, :path => podpath}); + else + puts "Invalid plugin specification: #{line}" + end + } + return pods_array +end + +# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock +# referring to absolute paths on developers' machines. +system('rm -rf .symlinks') +system('mkdir -p .symlinks/plugins') + +def flutter_root(f) + generated_xcode_build_settings = parse_KV_file(File.join(f, File.join('.ios', 'Generated.xcconfig'))) + if generated_xcode_build_settings.empty? + puts "Generated.xcconfig must exist. Make sure `flutter packages get` is executed in ${f}." + exit + end + generated_xcode_build_settings.map { |p| + if p[:name] == 'FLUTTER_ROOT' + return p[:path] + end + } +end + +framework_dir = File.join(File.expand_path(File.dirname(__FILE__)), 'Flutter') +engine_dir = File.join(framework_dir, 'engine') +if !File.exist?(engine_dir) + # Copy the debug engine to have something to link against if the xcode backend script has not run yet. + debug_framework_dir = File.join(flutter_root(flutter_application_path), 'bin', 'cache', 'artifacts', 'engine', 'ios') + FileUtils.mkdir(engine_dir) + FileUtils.cp_r(File.join(debug_framework_dir, 'Flutter.framework'), engine_dir) + FileUtils.cp(File.join(debug_framework_dir, 'Flutter.podspec'), engine_dir) +end + +symlink = File.join('.symlinks', 'flutter') + +File.symlink(framework_dir, symlink) +pod 'Flutter', :path => File.join(symlink, 'engine') + + +plugin_pods = parse_KV_file(File.join(flutter_application_path, '.flutter-plugins')) + +plugin_pods.map { |r| + symlink = File.join('.symlinks', 'plugins', r[:name]) + + File.symlink(r[:path], symlink) + pod r[:name], :path => File.join(symlink, 'ios') +} + +symlink = File.join('.symlinks', 'FlutterApp') +File.symlink(File.absolute_path(flutter_application_path), symlink) + +pod 'FlutterPluginRegistrant', :path => File.join(symlink, '.ios', 'FlutterPluginRegistrant') diff --git a/packages/flutter_tools/test/ios/cocoapods_test.dart b/packages/flutter_tools/test/ios/cocoapods_test.dart index 3bb165bf6ed..b65e75763b5 100644 --- a/packages/flutter_tools/test/ios/cocoapods_test.dart +++ b/packages/flutter_tools/test/ios/cocoapods_test.dart @@ -9,6 +9,8 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/flutter_manifest.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/ios/cocoapods.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:mockito/mockito.dart'; @@ -132,21 +134,25 @@ void main() { podFile = fs.file(fs.path.join('project', 'ios', 'Podfile')); }); - testUsingContext('creates objective-c Podfile when not present', () { - cocoaPodsUnderTest.setupPodfile('project'); + testUsingContext('creates objective-c Podfile when not present', () async { + final FlutterManifest manifest = + await new FlutterProject.fromPath('project').manifest; + cocoaPodsUnderTest.setupPodfile('project', manifest); expect(podFile.readAsStringSync(), 'Objective-C podfile template'); }, overrides: { FileSystem: () => fs, }); - testUsingContext('creates swift Podfile if swift', () { + testUsingContext('creates swift Podfile if swift', () async { when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn({ 'SWIFT_VERSION': '4.0', }); - cocoaPodsUnderTest.setupPodfile('project'); + final FlutterManifest manifest = + await new FlutterProject.fromPath('project').manifest; + cocoaPodsUnderTest.setupPodfile('project', manifest); expect(podFile.readAsStringSync(), 'Swift podfile template'); }, overrides: { @@ -154,20 +160,24 @@ void main() { XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, }); - testUsingContext('does not recreate Podfile when already present', () { + testUsingContext('does not recreate Podfile when already present', () async { podFile..createSync()..writeAsStringSync('Existing Podfile'); - cocoaPodsUnderTest.setupPodfile('project'); + final FlutterManifest manifest = + await new FlutterProject.fromPath('project').manifest; + cocoaPodsUnderTest.setupPodfile('project', manifest); expect(podFile.readAsStringSync(), 'Existing Podfile'); }, overrides: { FileSystem: () => fs, }); - testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () { + testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () async { when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); - cocoaPodsUnderTest.setupPodfile('project'); + final FlutterManifest manifest = + await new FlutterProject.fromPath('project').manifest; + cocoaPodsUnderTest.setupPodfile('project', manifest); expect(podFile.existsSync(), false); }, overrides: { @@ -175,12 +185,14 @@ void main() { XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, }); - testUsingContext('includes Pod config in xcconfig files, if not present', () { + testUsingContext('includes Pod config in xcconfig files, if not present', () async { podFile..createSync()..writeAsStringSync('Existing Podfile'); debugConfigFile..createSync(recursive: true)..writeAsStringSync('Existing debug config'); releaseConfigFile..createSync(recursive: true)..writeAsStringSync('Existing release config'); - cocoaPodsUnderTest.setupPodfile('project'); + final FlutterManifest manifest = + await new FlutterProject.fromPath('project').manifest; + cocoaPodsUnderTest.setupPodfile('project', manifest); final String debugContents = debugConfigFile.readAsStringSync(); expect(debugContents, contains( diff --git a/packages/flutter_tools/test/ios/xcodeproj_test.dart b/packages/flutter_tools/test/ios/xcodeproj_test.dart index 621e9922c6a..3f017837f6f 100644 --- a/packages/flutter_tools/test/ios/xcodeproj_test.dart +++ b/packages/flutter_tools/test/ios/xcodeproj_test.dart @@ -11,6 +11,8 @@ import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/bundle.dart' as bundle; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/flutter_manifest.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; @@ -284,12 +286,16 @@ Information about project "Runner": testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async { when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, any)).thenReturn('engine'); when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm')); + const BuildInfo buildInfo = const BuildInfo(BuildMode.debug, null, previewDart2: true, targetPlatform: TargetPlatform.ios, ); - await updateGeneratedXcodeProperties( + final FlutterManifest manifest = + await new FlutterProject.fromPath('path/to/project').manifest; + updateGeneratedXcodeProperties( projectPath: 'path/to/project', + manifest: manifest, buildInfo: buildInfo, previewDart2: true, ); @@ -309,8 +315,11 @@ Information about project "Runner": trackWidgetCreation: true, targetPlatform: TargetPlatform.ios, ); - await updateGeneratedXcodeProperties( + final FlutterManifest manifest = + await new FlutterProject.fromPath('path/to/project').manifest; + updateGeneratedXcodeProperties( projectPath: 'path/to/project', + manifest: manifest, buildInfo: buildInfo, previewDart2: true, ); @@ -329,8 +338,11 @@ Information about project "Runner": previewDart2: true, targetPlatform: TargetPlatform.ios, ); - await updateGeneratedXcodeProperties( + final FlutterManifest manifest = + await new FlutterProject.fromPath('path/to/project').manifest; + updateGeneratedXcodeProperties( projectPath: 'path/to/project', + manifest: manifest, buildInfo: buildInfo, previewDart2: true, ); @@ -349,8 +361,12 @@ Information about project "Runner": previewDart2: true, targetPlatform: TargetPlatform.ios, ); - await updateGeneratedXcodeProperties( + + final FlutterManifest manifest = + await new FlutterProject.fromPath('path/to/project').manifest; + updateGeneratedXcodeProperties( projectPath: 'path/to/project', + manifest: manifest, buildInfo: buildInfo, previewDart2: true, ); @@ -394,15 +410,18 @@ Information about project "Runner": } Future checkBuildVersion({ - String manifest, + String manifestString, BuildInfo buildInfo, String expectedBuildName, String expectedBuildNumber, }) async { - final String projectPath = await createMinimalProject(manifest); + final String projectPath = await createMinimalProject(manifestString); - await updateGeneratedXcodeProperties( + final FlutterManifest manifest = + await new FlutterProject.fromPath(projectPath).manifest; + updateGeneratedXcodeProperties( projectPath: projectPath, + manifest: manifest, buildInfo: buildInfo, targetOverride: bundle.defaultMainPath, previewDart2: false, @@ -427,7 +446,7 @@ flutter: const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null); await checkBuildVersion( - manifest: manifest, + manifestString: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.0', expectedBuildNumber: '1', @@ -445,7 +464,7 @@ flutter: '''; const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null); await checkBuildVersion( - manifest: manifest, + manifestString: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.0', expectedBuildNumber: null, @@ -463,7 +482,7 @@ flutter: '''; const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2'); await checkBuildVersion( - manifest: manifest, + manifestString: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.2', expectedBuildNumber: '1', @@ -481,7 +500,7 @@ flutter: '''; const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildNumber: 3); await checkBuildVersion( - manifest: manifest, + manifestString: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.0', expectedBuildNumber: '3', @@ -499,7 +518,7 @@ flutter: '''; const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3); await checkBuildVersion( - manifest: manifest, + manifestString: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.2', expectedBuildNumber: '3', @@ -517,7 +536,7 @@ flutter: '''; const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3); await checkBuildVersion( - manifest: manifest, + manifestString: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.2', expectedBuildNumber: '3', @@ -534,7 +553,7 @@ flutter: '''; const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3); await checkBuildVersion( - manifest: manifest, + manifestString: manifest, buildInfo: buildInfo, expectedBuildName: '1.0.2', expectedBuildNumber: '3', diff --git a/packages/flutter_tools/test/project_test.dart b/packages/flutter_tools/test/project_test.dart index 89551874774..ee5f20ead6b 100644 --- a/packages/flutter_tools/test/project_test.dart +++ b/packages/flutter_tools/test/project_test.dart @@ -15,7 +15,14 @@ void main() { group('Project', () { testInMemory('knows location', () { final Directory directory = fs.directory('myproject'); - expect(new FlutterProject(directory).directory, directory); + expect( + new FlutterProject(directory).directory.absolute.path, + directory.absolute.path, + ); + expect( + new FlutterProject.fromPath(directory.path).directory.absolute.path, + directory.absolute.path, + ); }); group('ensure ready for platform-specific tooling', () { testInMemory('does nothing, if project is not created', () async { @@ -94,7 +101,7 @@ void main() { }); } -FlutterProject someProject() => new FlutterProject(fs.directory('some_project')); +FlutterProject someProject() => new FlutterProject.fromPath('some_project'); FlutterProject aProjectWithIos() { final Directory directory = fs.directory('ios_project');