Move plugin injection to just after pub get (#14743)
diff --git a/dev/devicelab/bin/tasks/plugin_test.dart b/dev/devicelab/bin/tasks/plugin_test.dart new file mode 100644 index 0000000..3217961 --- /dev/null +++ b/dev/devicelab/bin/tasks/plugin_test.dart
@@ -0,0 +1,12 @@ +// Copyright (c) 2018 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 'package:flutter_devicelab/tasks/plugin_tests.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; + +Future<Null> main() async { + await task(new PluginTest('apk')); +}
diff --git a/dev/devicelab/bin/tasks/plugin_test_ios.dart b/dev/devicelab/bin/tasks/plugin_test_ios.dart new file mode 100644 index 0000000..492e7d6 --- /dev/null +++ b/dev/devicelab/bin/tasks/plugin_test_ios.dart
@@ -0,0 +1,12 @@ +// Copyright (c) 2018 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 'package:flutter_devicelab/tasks/plugin_tests.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; + +Future<Null> main() async { + await task(new PluginTest('ios')); +}
diff --git a/dev/devicelab/bin/tasks/plugin_test_win.dart b/dev/devicelab/bin/tasks/plugin_test_win.dart new file mode 100644 index 0000000..3217961 --- /dev/null +++ b/dev/devicelab/bin/tasks/plugin_test_win.dart
@@ -0,0 +1,12 @@ +// Copyright (c) 2018 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 'package:flutter_devicelab/tasks/plugin_tests.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; + +Future<Null> main() async { + await task(new PluginTest('apk')); +}
diff --git a/dev/devicelab/lib/tasks/plugin_tests.dart b/dev/devicelab/lib/tasks/plugin_tests.dart new file mode 100644 index 0000000..d7baac5 --- /dev/null +++ b/dev/devicelab/lib/tasks/plugin_tests.dart
@@ -0,0 +1,77 @@ +// Copyright (c) 2018 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 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/ios.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; + +/// Defines task that creates new Flutter project, adds a plugin, and then +/// builds the specified [buildTarget]. +class PluginTest { + final String buildTarget; + + PluginTest(this.buildTarget); + + Future<TaskResult> call() async { + section('Create Flutter project'); + final Directory tmp = await Directory.systemTemp.createTemp('plugin'); + final FlutterProject project = await FlutterProject.create(tmp); + if (buildTarget == 'ios') { + await prepareProvisioningCertificates(project.rootPath); + } + try { + section('Add plugin'); + await project.addPlugin('path_provider'); + + section('Build'); + await project.build(buildTarget); + + return new TaskResult.success(null); + } catch (e) { + return new TaskResult.failure(e.toString()); + } finally { + await project.delete(); + } + } +} + +class FlutterProject { + FlutterProject(this.parent, this.name); + + final Directory parent; + final String name; + + static Future<FlutterProject> create(Directory directory) async { + await inDirectory(directory, () async { + await flutter('create', options: <String>['--org', 'io.flutter.devicelab', 'plugintest']); + }); + return new FlutterProject(directory, 'plugintest'); + } + + String get rootPath => path.join(parent.path, name); + + Future<Null> addPlugin(String plugin) async { + final File pubspec = new File(path.join(rootPath, 'pubspec.yaml')); + String content = await pubspec.readAsString(); + content = content.replaceFirst( + '\ndependencies:\n', + '\ndependencies:\n $plugin:\n', + ); + await pubspec.writeAsString(content, flush: true); + } + + Future<Null> build(String target) async { + await inDirectory(new Directory(rootPath), () async { + await flutter('build', options: <String>[target]); + }); + } + + Future<Null> delete() async { + await parent.delete(recursive: true); + } +}
diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml index 63b3d1f..27a0bfc 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml
@@ -237,6 +237,13 @@ stage: devicelab required_agent_capabilities: ["linux/android"] + plugin_test: + description: > + Checks that the project template works and supports plugins. + stage: devicelab + required_agent_capabilities: ["linux/android"] + flaky: true + flutter_gallery_instrumentation_test: description: > Same as flutter_gallery__transition_perf but uses Android instrumentation @@ -253,6 +260,13 @@ stage: devicelab_ios required_agent_capabilities: ["mac/ios"] + plugin_test_ios: + description: > + Checks that the project template works and supports plugins on iOS. + stage: devicelab_ios + required_agent_capabilities: ["mac/ios"] + flaky: true + external_ui_integration_test_ios: description: > Checks that external UIs work on iOS. @@ -347,6 +361,13 @@ stage: devicelab_win required_agent_capabilities: ["windows/android"] + plugin_test_win: + description: > + Checks that the project template works and supports plugins on Windows. + stage: devicelab_win + required_agent_capabilities: ["windows/android"] + flaky: true + hot_mode_dev_cycle_win__benchmark: description: > Measures the performance of Dart VM hot patching feature on Windows.
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index e0d5149..75af21b 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -16,7 +16,6 @@ import '../build_info.dart'; import '../cache.dart'; import '../globals.dart'; -import '../plugins.dart'; import 'android_sdk.dart'; import 'android_studio.dart'; @@ -94,7 +93,6 @@ Future<GradleProject> _readGradleProject() async { final String gradle = await _ensureGradle(); updateLocalProperties(); - injectPlugins(); try { final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true); final RunResult runResult = await runCheckedAsync(
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart index f98dd46a..ea4e9dc3 100644 --- a/packages/flutter_tools/lib/src/application_package.dart +++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -181,11 +181,11 @@ if (id == null) return null; final String projectPath = fs.path.join('ios', 'Runner.xcodeproj'); - final Map<String, String> buildSettings = getXcodeBuildSettings(projectPath, 'Runner'); + final Map<String, String> buildSettings = xcodeProjectInterpreter.getBuildSettings(projectPath, 'Runner'); id = substituteXcodeVariables(id, buildSettings); return new BuildableIOSApp( - appDirectory: fs.path.join('ios'), + appDirectory: 'ios', projectBundleId: id, buildSettings: buildSettings, );
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 643a452..5956021 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -14,14 +14,10 @@ import '../base/file_system.dart'; import '../base/os.dart'; import '../base/utils.dart'; -import '../build_info.dart'; import '../cache.dart'; import '../dart/pub.dart'; import '../doctor.dart'; -import '../flx.dart' as flx; import '../globals.dart'; -import '../ios/xcodeproj.dart'; -import '../plugins.dart'; import '../project.dart'; import '../runner/flutter_command.dart'; import '../template.dart'; @@ -232,17 +228,9 @@ printStatus('Wrote $generatedCount files.'); printStatus(''); - updateXcodeGeneratedProperties( - projectPath: appPath, - buildInfo: BuildInfo.debug, - target: flx.defaultMainPath, - hasPlugins: generatePlugin, - previewDart2: false, - ); - if (argResults['pub']) { await pubGet(context: PubContext.create, directory: appPath, offline: argResults['offline']); - injectPlugins(directory: appPath); + new FlutterProject(fs.directory(appPath)).ensureReadyForPlatformSpecificTooling(); } if (android_sdk.androidSdk != null)
diff --git a/packages/flutter_tools/lib/src/commands/inject_plugins.dart b/packages/flutter_tools/lib/src/commands/inject_plugins.dart index f1df432..6886937 100644 --- a/packages/flutter_tools/lib/src/commands/inject_plugins.dart +++ b/packages/flutter_tools/lib/src/commands/inject_plugins.dart
@@ -24,7 +24,8 @@ @override Future<Null> runCommand() async { - final bool result = injectPlugins().hasPlugin; + injectPlugins(); + final bool result = hasPlugins(); if (result) { printStatus('GeneratedPluginRegistrants successfully written.'); } else {
diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart index 44aafbc..3600289 100644 --- a/packages/flutter_tools/lib/src/commands/packages.dart +++ b/packages/flutter_tools/lib/src/commands/packages.dart
@@ -5,8 +5,10 @@ import 'dart:async'; import '../base/common.dart'; +import '../base/file_system.dart'; import '../base/os.dart'; import '../dart/pub.dart'; +import '../project.dart'; import '../runner/flutter_command.dart'; class PackagesCommand extends FlutterCommand { @@ -75,6 +77,7 @@ offline: argResults['offline'], checkLastModified: false, ); + new FlutterProject(fs.directory(target)).ensureReadyForPlatformSpecificTooling(); } }
diff --git a/packages/flutter_tools/lib/src/ios/cocoapods.dart b/packages/flutter_tools/lib/src/ios/cocoapods.dart index da63cdc..1d558d4 100644 --- a/packages/flutter_tools/lib/src/ios/cocoapods.dart +++ b/packages/flutter_tools/lib/src/ios/cocoapods.dart
@@ -16,6 +16,7 @@ import '../base/version.dart'; import '../cache.dart'; import '../globals.dart'; +import 'xcodeproj.dart'; const String noCocoaPodsConsequence = ''' CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side. @@ -60,13 +61,13 @@ // For backward compatibility with previously created Podfile only. @required String iosEngineDir, bool isSwift: false, - bool pluginOrFlutterPodChanged: true, + bool flutterPodChanged: true, }) async { + if (!(await appIosDir.childFile('Podfile').exists())) { + throwToolExit('Podfile missing'); + } if (await _checkPodCondition()) { - if (!fs.file(fs.path.join(appIosDir.path, 'Podfile')).existsSync()) { - await _createPodfile(appIosDir, isSwift); - } // TODO(xster): Add more logic for handling merge conflicts. - if (_shouldRunPodInstall(appIosDir.path, pluginOrFlutterPodChanged)) + if (_shouldRunPodInstall(appIosDir.path, flutterPodChanged)) await _runPodInstall(appIosDir, iosEngineDir); } } @@ -99,39 +100,69 @@ return true; } - Future<Null> _createPodfile(Directory bundle, bool isSwift) async { - final File podfileTemplate = fs.file(fs.path.join( - Cache.flutterRoot, - 'packages', - 'flutter_tools', - 'templates', - 'cocoapods', - isSwift ? 'Podfile-swift' : 'Podfile-objc', - )); - podfileTemplate.copySync(fs.path.join(bundle.path, 'Podfile')); + /// Ensures the `ios` sub-project of the Flutter project at [directory] + /// contains a suitable `Podfile` and that its `Flutter/Xxx.xcconfig` files + /// include pods configuration. + void setupPodfile(String directory) { + if (!xcodeProjectInterpreter.canInterpretXcodeProjects) { + // Don't do anything for iOS when host platform doesn't support it. + return; + } + final String podfilePath = fs.path.join(directory, 'ios', 'Podfile'); + if (!fs.file(podfilePath).existsSync()) { + final bool isSwift = xcodeProjectInterpreter.getBuildSettings( + fs.path.join(directory, 'ios', 'Runner.xcodeproj'), + 'Runner', + ).containsKey('SWIFT_VERSION'); + final File podfileTemplate = fs.file(fs.path.join( + Cache.flutterRoot, + 'packages', + 'flutter_tools', + 'templates', + 'cocoapods', + isSwift ? 'Podfile-swift' : 'Podfile-objc', + )); + podfileTemplate.copySync(podfilePath); + } + _addPodsDependencyToFlutterXcconfig(directory, 'Debug'); + _addPodsDependencyToFlutterXcconfig(directory, 'Release'); + } + + void _addPodsDependencyToFlutterXcconfig(String directory, String mode) { + final File file = fs.file(fs.path.join(directory, 'ios', 'Flutter', '$mode.xcconfig')); + if (file.existsSync()) { + final String content = file.readAsStringSync(); + final String include = '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode + .toLowerCase()}.xcconfig"'; + if (!content.contains(include)) + file.writeAsStringSync('$include\n$content', flush: true); + } + } + + /// Ensures that pod install is deemed needed on next check. + void invalidatePodInstallOutput(String directory) { + final File manifest = fs.file( + fs.path.join(directory, 'ios', 'Pods', 'Manifest.lock'), + ); + if (manifest.existsSync()) + manifest.deleteSync(); } // Check if you need to run pod install. // The pod install will run if any of below is true. - // 1. Any plugins changed (add/update/delete) - // 2. The flutter.framework has changed (debug/release/profile) - // 3. The podfile.lock doesn't exists - // 4. The Pods/manifest.lock doesn't exists - // 5. The podfile.lock doesn't match Pods/manifest.lock. - bool _shouldRunPodInstall(String appDir, bool pluginOrFlutterPodChanged) { - if (pluginOrFlutterPodChanged) + // 1. The flutter.framework has changed (debug/release/profile) + // 2. The podfile.lock doesn't exist + // 3. The Pods/Manifest.lock doesn't exist (It is deleted when plugins change) + // 4. The podfile.lock doesn't match Pods/Manifest.lock. + bool _shouldRunPodInstall(String appDir, bool flutterPodChanged) { + if (flutterPodChanged) return true; - // Check if podfile.lock and Pods/Manifest.lock exists and matches. + // Check if podfile.lock and Pods/Manifest.lock exist and match. final File podfileLockFile = fs.file(fs.path.join(appDir, 'Podfile.lock')); - final File manifestLockFile = - fs.file(fs.path.join(appDir, 'Pods', 'Manifest.lock')); - if (!podfileLockFile.existsSync() + final File manifestLockFile = fs.file(fs.path.join(appDir, 'Pods', 'Manifest.lock')); + return !podfileLockFile.existsSync() || !manifestLockFile.existsSync() - || podfileLockFile.readAsStringSync() != - manifestLockFile.readAsStringSync()) { - return true; - } - return false; + || podfileLockFile.readAsStringSync() != manifestLockFile.readAsStringSync(); } Future<Null> _runPodInstall(Directory bundle, String engineDirectory) async {
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 32d37b1..b2a5f6f 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -219,7 +219,7 @@ return new XcodeBuildResult(success: false); } - final XcodeProjectInfo projectInfo = new XcodeProjectInfo.fromProjectSync(app.appDirectory); + final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.appDirectory); if (!projectInfo.targets.contains('Runner')) { printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.'); printError('Open Xcode to fix the problem:'); @@ -256,26 +256,22 @@ // copied over to a location that is suitable for Xcodebuild to find them. final Directory appDirectory = fs.directory(app.appDirectory); await _addServicesToBundle(appDirectory); - final InjectPluginsResult injectPluginsResult = injectPlugins(); - final bool hasFlutterPlugins = injectPluginsResult.hasPlugin; final String previousGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory); - updateXcodeGeneratedProperties( + updateGeneratedXcodeProperties( projectPath: fs.currentDirectory.path, buildInfo: buildInfo, target: target, - hasPlugins: hasFlutterPlugins, previewDart2: buildInfo.previewDart2, ); - if (hasFlutterPlugins) { + if (hasPlugins()) { final String currentGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory); await cocoaPods.processPods( - appIosDir: appDirectory, - iosEngineDir: flutterFrameworkDir(buildInfo.mode), - isSwift: app.isSwift, - pluginOrFlutterPodChanged: injectPluginsResult.hasChanged - || previousGeneratedXcconfig != currentGeneratedXcconfig, + appIosDir: appDirectory, + iosEngineDir: flutterFrameworkDir(buildInfo.mode), + isSwift: app.isSwift, + flutterPodChanged: (previousGeneratedXcconfig != currentGeneratedXcconfig), ); } @@ -465,7 +461,7 @@ String readGeneratedXcconfig(String appPath) { final String generatedXcconfigPath = - fs.path.join(fs.currentDirectory.path, appPath, 'Flutter','Generated.xcconfig'); + fs.path.join(fs.currentDirectory.path, appPath, 'Flutter', 'Generated.xcconfig'); final File generatedXcconfigFile = fs.file(generatedXcconfigPath); if (!generatedXcconfigFile.existsSync()) return null;
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 1bc8e79..9a50d30 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -5,11 +5,13 @@ import 'package:meta/meta.dart'; import '../artifacts.dart'; +import '../base/context.dart'; import '../base/file_system.dart'; import '../base/process.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; +import '../flx.dart' as flx; import '../globals.dart'; final RegExp _settingExpr = new RegExp(r'(\w+)\s*=\s*(.*)$'); @@ -19,11 +21,28 @@ return fs.path.normalize(fs.path.dirname(artifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, mode))); } -void updateXcodeGeneratedProperties({ +String _generatedXcodePropertiesPath(String projectPath) { + return fs.path.join(projectPath, 'ios', 'Flutter', 'Generated.xcconfig'); +} + +/// Writes default Xcode properties files in the Flutter project at +/// [projectPath], if such files do not already exist. +void generateXcodeProperties(String projectPath) { + if (fs.file(_generatedXcodePropertiesPath(projectPath)).existsSync()) + return; + updateGeneratedXcodeProperties( + projectPath: projectPath, + buildInfo: BuildInfo.debug, + target: flx.defaultMainPath, + previewDart2: false, + ); +} + +/// Writes or rewrites Xcode property files with the specified information. +void updateGeneratedXcodeProperties({ @required String projectPath, @required BuildInfo buildInfo, @required String target, - @required bool hasPlugins, @required bool previewDart2, }) { final StringBuffer localsBuffer = new StringBuffer(); @@ -58,21 +77,42 @@ localsBuffer.writeln('PREVIEW_DART_2=true'); } - // Add dependency to CocoaPods' generated project only if plugins are used. - if (hasPlugins) - localsBuffer.writeln('#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"'); - - final File localsFile = fs.file(fs.path.join(projectPath, 'ios', 'Flutter', 'Generated.xcconfig')); + final File localsFile = fs.file(_generatedXcodePropertiesPath(projectPath)); localsFile.createSync(recursive: true); localsFile.writeAsStringSync(localsBuffer.toString()); } -Map<String, String> getXcodeBuildSettings(String xcodeProjPath, String target) { - final String absProjPath = fs.path.absolute(xcodeProjPath); - final String out = runCheckedSync(<String>[ - '/usr/bin/xcodebuild', '-project', absProjPath, '-target', target, '-showBuildSettings' - ]); - return parseXcodeBuildSettings(out); +XcodeProjectInterpreter get xcodeProjectInterpreter => context.putIfAbsent( + XcodeProjectInterpreter, + () => const XcodeProjectInterpreter(), +); + +/// Interpreter of Xcode projects settings. +class XcodeProjectInterpreter { + static const String _executable = '/usr/bin/xcodebuild'; + + const XcodeProjectInterpreter(); + + bool get canInterpretXcodeProjects => fs.isFileSync(_executable); + + Map<String, String> getBuildSettings(String projectPath, String target) { + final String out = runCheckedSync(<String>[ + _executable, + '-project', + fs.path.absolute(projectPath), + '-target', + target, + '-showBuildSettings' + ], workingDirectory: projectPath); + return parseXcodeBuildSettings(out); + } + + XcodeProjectInfo getInfo(String projectPath) { + final String out = runCheckedSync(<String>[ + _executable, '-list', + ], workingDirectory: projectPath); + return new XcodeProjectInfo.fromXcodeBuildOutput(out); + } } Map<String, String> parseXcodeBuildSettings(String showBuildSettingsOutput) { @@ -101,13 +141,6 @@ class XcodeProjectInfo { XcodeProjectInfo(this.targets, this.buildConfigurations, this.schemes); - factory XcodeProjectInfo.fromProjectSync(String projectPath) { - final String out = runCheckedSync(<String>[ - '/usr/bin/xcodebuild', '-list', - ], workingDirectory: projectPath); - return new XcodeProjectInfo.fromXcodeBuildOutput(out); - } - factory XcodeProjectInfo.fromXcodeBuildOutput(String output) { final List<String> targets = <String>[]; final List<String> buildConfigurations = <String>[];
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index 0d03f04..697cfdc 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -9,6 +9,7 @@ import 'base/file_system.dart'; import 'dart/package_map.dart'; import 'globals.dart'; +import 'ios/cocoapods.dart'; class Plugin { final String name; @@ -80,21 +81,25 @@ /// Returns true if .flutter-plugins has changed, otherwise returns false. bool _writeFlutterPluginsList(String directory, List<Plugin> plugins) { - final File pluginsProperties = fs.file(fs.path.join(directory, '.flutter-plugins')); - final String previousFlutterPlugins = - pluginsProperties.existsSync() ? pluginsProperties.readAsStringSync() : null; + final File pluginsFile = fs.file(fs.path.join(directory, '.flutter-plugins')); + final String oldContents = _readFlutterPluginsList(directory); final String pluginManifest = plugins.map((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n'); if (pluginManifest.isNotEmpty) { - pluginsProperties.writeAsStringSync('$pluginManifest\n'); + pluginsFile.writeAsStringSync('$pluginManifest\n', flush: true); } else { - if (pluginsProperties.existsSync()) { - pluginsProperties.deleteSync(); - } + if (pluginsFile.existsSync()) + pluginsFile.deleteSync(); } - final String currentFlutterPlugins = - pluginsProperties.existsSync() ? pluginsProperties.readAsStringSync() : null; - return currentFlutterPlugins != previousFlutterPlugins; + final String newContents = _readFlutterPluginsList(directory); + return oldContents != newContents; +} + +/// Returns the contents of the `.flutter-plugins` file in [directory], or +/// null if that file does not exist. +String _readFlutterPluginsList(String directory) { + final File pluginsFile = fs.file(fs.path.join(directory, '.flutter-plugins')); + return pluginsFile.existsSync() ? pluginsFile.readAsStringSync() : null; } const String _androidPluginRegistryTemplate = '''package io.flutter.plugins; @@ -128,7 +133,7 @@ } '''; -void _writeAndroidPluginRegistry(String directory, List<Plugin> plugins) { +void _writeAndroidPluginRegistrant(String directory, List<Plugin> plugins) { final List<Map<String, dynamic>> androidPlugins = plugins .where((Plugin p) => p.androidPackage != null && p.pluginClass != null) .map((Plugin p) => <String, dynamic>{ @@ -187,7 +192,7 @@ @end '''; -void _writeIOSPluginRegistry(String directory, List<Plugin> plugins) { +void _writeIOSPluginRegistrant(String directory, List<Plugin> plugins) { final List<Map<String, dynamic>> iosPlugins = plugins .where((Plugin p) => p.pluginClass != null) .map((Plugin p) => <String, dynamic>{ @@ -210,7 +215,6 @@ registryHeaderFile.writeAsStringSync(pluginRegistryHeader); final File registryImplementationFile = registryDirectory.childFile('GeneratedPluginRegistrant.m'); registryImplementationFile.writeAsStringSync(pluginRegistryImplementation); - } class InjectPluginsResult{ @@ -224,17 +228,30 @@ final bool hasChanged; } -/// Finds Flutter plugins in the pubspec.yaml, creates platform injection -/// registries classes and add them to the build dependencies. -/// -/// Returns whether any Flutter plugins are added and whether they changed. -InjectPluginsResult injectPlugins({String directory}) { +/// Injects plugins found in `pubspec.yaml` into the platform-specific projects. +void injectPlugins({String directory}) { directory ??= fs.currentDirectory.path; + if (fs.file(fs.path.join(directory, 'example', 'pubspec.yaml')).existsSync()) { + // Switch to example app if in plugin project template. + directory = fs.path.join(directory, 'example'); + } final List<Plugin> plugins = _findPlugins(directory); - final bool hasPluginsChanged = _writeFlutterPluginsList(directory, plugins); + final bool changed = _writeFlutterPluginsList(directory, plugins); if (fs.isDirectorySync(fs.path.join(directory, 'android'))) - _writeAndroidPluginRegistry(directory, plugins); - if (fs.isDirectorySync(fs.path.join(directory, 'ios'))) - _writeIOSPluginRegistry(directory, plugins); - return new InjectPluginsResult(hasPlugin: plugins.isNotEmpty, hasChanged: hasPluginsChanged); + _writeAndroidPluginRegistrant(directory, plugins); + if (fs.isDirectorySync(fs.path.join(directory, 'ios'))) { + _writeIOSPluginRegistrant(directory, plugins); + final CocoaPods cocoaPods = const CocoaPods(); + if (plugins.isNotEmpty) + cocoaPods.setupPodfile(directory); + if (changed) + cocoaPods.invalidatePodInstallOutput(directory); + } +} + +/// Returns whether the Flutter project at the specified [directory] +/// has any plugin dependencies. +bool hasPlugins({String directory}) { + directory ??= fs.currentDirectory.path; + return _readFlutterPluginsList(directory) != null; }
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 1fd1b4f..7800f2a 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart
@@ -4,7 +4,11 @@ import 'dart:async'; import 'dart:convert'; + import 'base/file_system.dart'; +import 'ios/xcodeproj.dart'; +import 'plugins.dart'; + /// Represents the contents of a Flutter project at the specified [directory]. class FlutterProject { @@ -43,8 +47,25 @@ /// The Android sub project of this project. AndroidProject get android => new AndroidProject(directory.childDirectory('android')); + /// Returns true if this project is a plugin project. + bool get isPluginProject => directory.childDirectory('example').childFile('pubspec.yaml').existsSync(); + /// The example sub project of this (plugin) project. FlutterProject get example => new FlutterProject(directory.childDirectory('example')); + + /// Generates project files necessary to make Gradle builds work on Android + /// and CocoaPods+Xcode work on iOS. + void ensureReadyForPlatformSpecificTooling() { + if (!directory.existsSync()) { + return; + } + if (isPluginProject) { + example.ensureReadyForPlatformSpecificTooling(); + } else { + injectPlugins(directory: directory.path); + generateXcodeProperties(directory.path); + } + } } /// Represents the contents of the ios/ folder of a Flutter project.
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 9d08b3e..ca48a1a 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -21,6 +21,7 @@ import '../doctor.dart'; import '../flx.dart' as flx; import '../globals.dart'; +import '../project.dart'; import '../usage.dart'; import 'flutter_command_runner.dart'; @@ -272,8 +273,10 @@ if (shouldUpdateCache) await cache.updateAll(); - if (shouldRunPub) + if (shouldRunPub) { await pubGet(context: PubContext.getVerifyContext(name)); + new FlutterProject(fs.currentDirectory).ensureReadyForPlatformSpecificTooling(); + } setupApplicationPackages();
diff --git a/packages/flutter_tools/test/commands/packages_test.dart b/packages/flutter_tools/test/commands/packages_test.dart index 86bbc28..34f6fb8 100644 --- a/packages/flutter_tools/test/commands/packages_test.dart +++ b/packages/flutter_tools/test/commands/packages_test.dart
@@ -29,9 +29,19 @@ temp.deleteSync(recursive: true); }); - Future<String> runCommand(String verb, { List<String> args }) async { + Future<String> createProjectWithPlugin(String plugin) async { final String projectPath = await createProject(temp); + final File pubspec = fs.file(fs.path.join(projectPath, 'pubspec.yaml')); + String content = await pubspec.readAsString(); + content = content.replaceFirst( + '\ndependencies:\n', + '\ndependencies:\n $plugin:\n', + ); + await pubspec.writeAsString(content, flush: true); + return projectPath; + } + Future<Null> runCommandIn(String projectPath, String verb, { List<String> args }) async { final PackagesCommand command = new PackagesCommand(); final CommandRunner<Null> runner = createTestCommandRunner(command); @@ -41,31 +51,148 @@ commandArgs.add(projectPath); await runner.run(commandArgs); - - return projectPath; } void expectExists(String projectPath, String relPath) { - expect(fs.isFileSync(fs.path.join(projectPath, relPath)), true); + expect( + fs.isFileSync(fs.path.join(projectPath, relPath)), + true, + reason: '$projectPath/$relPath should exist, but does not', + ); } - // Verify that we create a project that is well-formed. - testUsingContext('get', () async { - final String projectPath = await runCommand('get'); - expectExists(projectPath, 'lib/main.dart'); - expectExists(projectPath, '.packages'); + void expectContains(String projectPath, String relPath, String substring) { + expectExists(projectPath, relPath); + expect( + fs.file(fs.path.join(projectPath, relPath)).readAsStringSync(), + contains(substring), + reason: '$projectPath/$relPath has unexpected content' + ); + } + + void expectNotExists(String projectPath, String relPath) { + expect( + fs.isFileSync(fs.path.join(projectPath, relPath)), + false, + reason: '$projectPath/$relPath should not exist, but does', + ); + } + + void expectNotContains(String projectPath, String relPath, String substring) { + expectExists(projectPath, relPath); + expect( + fs.file(fs.path.join(projectPath, relPath)).readAsStringSync(), + isNot(contains(substring)), + reason: '$projectPath/$relPath has unexpected content', + ); + } + + const List<String> pubOutput = const <String>[ + '.packages', + 'pubspec.lock', + ]; + + const List<String> pluginRegistrants = const <String>[ + 'ios/Runner/GeneratedPluginRegistrant.h', + 'ios/Runner/GeneratedPluginRegistrant.m', + 'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', + ]; + + const List<String> pluginWitnesses = const <String>[ + '.flutter-plugins', + 'ios/Podfile', + ]; + + const Map<String, String> pluginContentWitnesses = const <String, String>{ + 'ios/Flutter/Debug.xcconfig': '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"', + 'ios/Flutter/Release.xcconfig': '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"', + }; + + void expectDependenciesResolved(String projectPath) { + for (String output in pubOutput) { + expectExists(projectPath, output); + } + } + + void expectZeroPluginsInjected(String projectPath) { + for (final String registrant in pluginRegistrants) { + expectExists(projectPath, registrant); + } + for (final String witness in pluginWitnesses) { + expectNotExists(projectPath, witness); + } + pluginContentWitnesses.forEach((String witness, String content) { + expectNotContains(projectPath, witness, content); + }); + } + + void expectPluginInjected(String projectPath) { + for (final String registrant in pluginRegistrants) { + expectExists(projectPath, registrant); + } + for (final String witness in pluginWitnesses) { + expectExists(projectPath, witness); + } + pluginContentWitnesses.forEach((String witness, String content) { + expectContains(projectPath, witness, content); + }); + } + + void removeGeneratedFiles(String projectPath) { + final Iterable<String> allFiles = <List<String>>[ + pubOutput, + pluginRegistrants, + pluginWitnesses, + ].expand((List<String> list) => list); + for (String path in allFiles) { + final File file = fs.file(fs.path.join(projectPath, path)); + if (file.existsSync()) + file.deleteSync(); + } + } + + testUsingContext('get fetches packages', () async { + final String projectPath = await createProject(temp); + + removeGeneratedFiles(projectPath); + + await runCommandIn(projectPath, 'get'); + + expectDependenciesResolved(projectPath); + expectZeroPluginsInjected(projectPath); }, timeout: allowForRemotePubInvocation); - testUsingContext('get --offline', () async { - final String projectPath = await runCommand('get', args: <String>['--offline']); - expectExists(projectPath, 'lib/main.dart'); - expectExists(projectPath, '.packages'); - }); + testUsingContext('get --offline fetches packages', () async { + final String projectPath = await createProject(temp); - testUsingContext('upgrade', () async { - final String projectPath = await runCommand('upgrade'); - expectExists(projectPath, 'lib/main.dart'); - expectExists(projectPath, '.packages'); + removeGeneratedFiles(projectPath); + + await runCommandIn(projectPath, 'get', args: <String>['--offline']); + + expectDependenciesResolved(projectPath); + expectZeroPluginsInjected(projectPath); + }, timeout: allowForCreateFlutterProject); + + testUsingContext('upgrade fetches packages', () async { + final String projectPath = await createProject(temp); + + removeGeneratedFiles(projectPath); + + await runCommandIn(projectPath, 'upgrade'); + + expectDependenciesResolved(projectPath); + expectZeroPluginsInjected(projectPath); + }, timeout: allowForRemotePubInvocation); + + testUsingContext('get fetches packages and injects plugin', () async { + final String projectPath = await createProjectWithPlugin('path_provider'); + + removeGeneratedFiles(projectPath); + + await runCommandIn(projectPath, 'get'); + + expectDependenciesResolved(projectPath); + expectPluginInjected(projectPath); }, timeout: allowForRemotePubInvocation); });
diff --git a/packages/flutter_tools/test/ios/cocoapods_test.dart b/packages/flutter_tools/test/ios/cocoapods_test.dart index 531c8ab..77c1e6d 100644 --- a/packages/flutter_tools/test/ios/cocoapods_test.dart +++ b/packages/flutter_tools/test/ios/cocoapods_test.dart
@@ -6,9 +6,11 @@ import 'package:file/file.dart'; 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/ios/cocoapods.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/ios/cocoapods.dart'; +import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:mockito/mockito.dart'; import 'package:process/process.dart'; import 'package:test/test.dart'; @@ -18,6 +20,7 @@ void main() { FileSystem fs; ProcessManager mockProcessManager; + MockXcodeProjectInterpreter mockXcodeProjectInterpreter; Directory projectUnderTest; CocoaPods cocoaPodsUnderTest; @@ -25,7 +28,9 @@ Cache.flutterRoot = 'flutter'; fs = new MemoryFileSystem(); mockProcessManager = new MockProcessManager(); + mockXcodeProjectInterpreter = new MockXcodeProjectInterpreter(); projectUnderTest = fs.directory(fs.path.join('project', 'ios'))..createSync(recursive: true); + fs.file(fs.path.join( Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-objc' )) @@ -45,97 +50,122 @@ )).thenReturn(exitsHappy); }); - testUsingContext( - 'create objective-c Podfile when not present', - () async { - await cocoaPodsUnderTest.processPods( - appIosDir: projectUnderTest, - iosEngineDir: 'engine/path', - ); - expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Objective-C podfile template'); - verify(mockProcessManager.run( - <String>['pod', 'install', '--verbose'], - workingDirectory: 'project/ios', - environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, - )); - }, - overrides: <Type, Generator>{ - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }, - ); + group('Setup Podfile', () { + File podfile; + File debugConfigFile; + File releaseConfigFile; - testUsingContext( - 'create swift Podfile if swift', - () async { - await cocoaPodsUnderTest.processPods( - appIosDir: projectUnderTest, - iosEngineDir: 'engine/path', - isSwift: true, - ); - expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Swift podfile template'); - verify(mockProcessManager.run( - <String>['pod', 'install', '--verbose'], - workingDirectory: 'project/ios', - environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, - )); - }, - overrides: <Type, Generator>{ - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }, - ); + setUp(() { + debugConfigFile = fs.file(fs.path.join('project', 'ios', 'Flutter', 'Debug.xcconfig')); + releaseConfigFile = fs.file(fs.path.join('project', 'ios', 'Flutter', 'Release.xcconfig')); + podfile = fs.file(fs.path.join('project', 'ios', 'Podfile')); + }); - testUsingContext( - 'do not recreate Podfile when present', - () async { - fs.file(fs.path.join('project', 'ios', 'Podfile')) - ..createSync() - ..writeAsString('Existing Podfile'); - await cocoaPodsUnderTest.processPods( - appIosDir: projectUnderTest, - iosEngineDir: 'engine/path', - ); - expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Existing Podfile'); - verify(mockProcessManager.run( - <String>['pod', 'install', '--verbose'], - workingDirectory: 'project/ios', - environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, - )); - }, - overrides: <Type, Generator>{ - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }, - ); + testUsingContext('creates objective-c Podfile when not present', () { + cocoaPodsUnderTest.setupPodfile('project'); - testUsingContext( - 'missing CocoaPods throws', - () async { + expect(podfile.readAsStringSync(), 'Objective-C podfile template'); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + + testUsingContext('creates swift Podfile if swift', () { + when(mockXcodeProjectInterpreter.canInterpretXcodeProjects).thenReturn(true); + when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{ + 'SWIFT_VERSION': '4.0', + }); + + cocoaPodsUnderTest.setupPodfile('project'); + + expect(podfile.readAsStringSync(), 'Swift podfile template'); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, + }); + + testUsingContext('does not recreate Podfile when already present', () { + podfile..createSync()..writeAsStringSync('Existing Podfile'); + + cocoaPodsUnderTest.setupPodfile('project'); + + expect(podfile.readAsStringSync(), 'Existing Podfile'); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + + testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () { + when(mockXcodeProjectInterpreter.canInterpretXcodeProjects).thenReturn(false); + + cocoaPodsUnderTest.setupPodfile('project'); + + expect(podfile.existsSync(), false); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, + }); + + testUsingContext('includes Pod config in xcconfig files, if not present', () { + 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 String debugContents = debugConfigFile.readAsStringSync(); + expect(debugContents, contains( + '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n')); + expect(debugContents, contains('Existing debug config')); + final String releaseContents = releaseConfigFile.readAsStringSync(); + expect(releaseContents, contains( + '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n')); + expect(releaseContents, contains('Existing release config')); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + }); + + group('Process pods', () { + testUsingContext('prints error, if CocoaPods is not installed', () async { + projectUnderTest.childFile('Podfile').createSync(); cocoaPodsUnderTest = const TestCocoaPods(false); + await cocoaPodsUnderTest.processPods( + appIosDir: projectUnderTest, + iosEngineDir: 'engine/path', + ); + verifyNever(mockProcessManager.run( + typed<List<String>>(any), + workingDirectory: any, + environment: typed<Map<String, String>>(any, named: 'environment'), + )); + expect(testLogger.errorText, contains('not installed')); + expect(testLogger.errorText, contains('Skipping pod install')); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('throws, if Podfile is missing.', () async { + cocoaPodsUnderTest = const TestCocoaPods(true); try { await cocoaPodsUnderTest.processPods( appIosDir: projectUnderTest, iosEngineDir: 'engine/path', ); - fail('Expected tool error'); - } catch (ToolExit) { + fail('ToolExit expected'); + } catch(e) { + expect(e, const isInstanceOf<ToolExit>()); verifyNever(mockProcessManager.run( - <String>['pod', 'install', '--verbose'], - workingDirectory: 'project/ios', - environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, + typed<List<String>>(any), + workingDirectory: any, + environment: typed<Map<String, String>>(any, named: 'environment'), )); } - }, - overrides: <Type, Generator>{ + }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => mockProcessManager, - }, - ); + }); - testUsingContext( - 'outdated specs repo should print error', - () async { + testUsingContext('throws, if specs repo is outdated.', () async { fs.file(fs.path.join('project', 'ios', 'Podfile')) ..createSync() ..writeAsString('Existing Podfile'); @@ -143,7 +173,10 @@ when(mockProcessManager.run( <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', - environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, + environment: <String, String>{ + 'FLUTTER_FRAMEWORK_DIR': 'engine/path', + 'COCOAPODS_DISABLE_STATS': 'true', + }, )).thenReturn(new ProcessResult( 1, 1, @@ -167,78 +200,152 @@ await cocoaPodsUnderTest.processPods( appIosDir: projectUnderTest, iosEngineDir: 'engine/path', - ); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Existing Podfile'); - fail('Exception expected'); - } catch (ToolExit) { - expect(testLogger.errorText, contains("CocoaPods's specs repository is too out-of-date to satisfy dependencies")); + ); + fail('ToolExit expected'); + } catch (e) { + expect(e, const isInstanceOf<ToolExit>()); + expect( + testLogger.errorText, + contains("CocoaPods's specs repository is too out-of-date to satisfy dependencies"), + ); } - }, - overrides: <Type, Generator>{ + }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => mockProcessManager, - }, - ); + }); - testUsingContext( - 'Run pod install if plugins or flutter framework have changes.', - () async { - fs.file(fs.path.join('project', 'ios', 'Podfile')) + testUsingContext('run pod install, if Podfile.lock is missing', () async { + projectUnderTest.childFile('Podfile') ..createSync() ..writeAsString('Existing Podfile'); - fs.file(fs.path.join('project', 'ios', 'Podfile.lock')) - ..createSync() - ..writeAsString('Existing lock files.'); - fs.file(fs.path.join('project', 'ios', 'Pods','Manifest.lock')) + projectUnderTest.childFile('Pods/Manifest.lock') ..createSync(recursive: true) - ..writeAsString('Existing lock files.'); + ..writeAsString('Existing lock file.'); await cocoaPodsUnderTest.processPods( - appIosDir: projectUnderTest, - iosEngineDir: 'engine/path', - pluginOrFlutterPodChanged: true + appIosDir: projectUnderTest, + iosEngineDir: 'engine/path', + flutterPodChanged: false, ); verify(mockProcessManager.run( <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, )); - }, - overrides: <Type, Generator>{ + }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => mockProcessManager, - }, - ); + }); - testUsingContext( - 'Skip pod install if plugins and flutter framework remain unchanged.', - () async { - fs.file(fs.path.join('project', 'ios', 'Podfile')) + testUsingContext('runs pod install, if Manifest.lock is missing', () async { + projectUnderTest.childFile('Podfile') ..createSync() ..writeAsString('Existing Podfile'); - fs.file(fs.path.join('project', 'ios', 'Podfile.lock')) + projectUnderTest.childFile('Podfile.lock') ..createSync() - ..writeAsString('Existing lock files.'); - fs.file(fs.path.join('project', 'ios', 'Pods','Manifest.lock')) - ..createSync(recursive: true) - ..writeAsString('Existing lock files.'); + ..writeAsString('Existing lock file.'); await cocoaPodsUnderTest.processPods( - appIosDir: projectUnderTest, - iosEngineDir: 'engine/path', - pluginOrFlutterPodChanged: false + appIosDir: projectUnderTest, + iosEngineDir: 'engine/path', + flutterPodChanged: false, ); - verifyNever(mockProcessManager.run( + verify(mockProcessManager.run( <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', - environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, + environment: <String, String>{ + 'FLUTTER_FRAMEWORK_DIR': 'engine/path', + 'COCOAPODS_DISABLE_STATS': 'true', + }, )); - }, - overrides: <Type, Generator>{ + }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => mockProcessManager, - }, - ); + }); + + testUsingContext('runs pod install, if Manifest.lock different from Podspec.lock', () async { + projectUnderTest.childFile('Podfile') + ..createSync() + ..writeAsString('Existing Podfile'); + projectUnderTest.childFile('Podfile.lock') + ..createSync() + ..writeAsString('Existing lock file.'); + projectUnderTest.childFile('Pods/Manifest.lock') + ..createSync(recursive: true) + ..writeAsString('Different lock file.'); + await cocoaPodsUnderTest.processPods( + appIosDir: projectUnderTest, + iosEngineDir: 'engine/path', + flutterPodChanged: false, + ); + verify(mockProcessManager.run( + <String>['pod', 'install', '--verbose'], + workingDirectory: 'project/ios', + environment: <String, String>{ + 'FLUTTER_FRAMEWORK_DIR': 'engine/path', + 'COCOAPODS_DISABLE_STATS': 'true', + }, + )); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('runs pod install, if flutter framework changed', () async { + projectUnderTest.childFile('Podfile') + ..createSync() + ..writeAsString('Existing Podfile'); + projectUnderTest.childFile('Podfile.lock') + ..createSync() + ..writeAsString('Existing lock file.'); + projectUnderTest.childFile('Pods/Manifest.lock') + ..createSync(recursive: true) + ..writeAsString('Existing lock file.'); + await cocoaPodsUnderTest.processPods( + appIosDir: projectUnderTest, + iosEngineDir: 'engine/path', + flutterPodChanged: true, + ); + verify(mockProcessManager.run( + <String>['pod', 'install', '--verbose'], + workingDirectory: 'project/ios', + environment: <String, String>{ + 'FLUTTER_FRAMEWORK_DIR': 'engine/path', + 'COCOAPODS_DISABLE_STATS': 'true', + }, + )); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('skips pod install, if nothing changed', () async { + projectUnderTest.childFile('Podfile') + ..createSync() + ..writeAsString('Existing Podfile'); + projectUnderTest.childFile('Podfile.lock') + ..createSync() + ..writeAsString('Existing lock file.'); + projectUnderTest.childFile('Pods/Manifest.lock') + ..createSync(recursive: true) + ..writeAsString('Existing lock file.'); + await cocoaPodsUnderTest.processPods( + appIosDir: projectUnderTest, + iosEngineDir: 'engine/path', + flutterPodChanged: false, + ); + verifyNever(mockProcessManager.run( + typed<List<String>>(any), + workingDirectory: any, + environment: typed<Map<String, String>>(any, named: 'environment'), + )); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + ProcessManager: () => mockProcessManager, + }); + }); } class MockProcessManager extends Mock implements ProcessManager {} +class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {} class TestCocoaPods extends CocoaPods { const TestCocoaPods([this._hasCocoaPods = true]);
diff --git a/packages/flutter_tools/test/project_test.dart b/packages/flutter_tools/test/project_test.dart index 2f1a3ff..6ef5bbb 100644 --- a/packages/flutter_tools/test/project_test.dart +++ b/packages/flutter_tools/test/project_test.dart
@@ -17,6 +17,29 @@ final Directory directory = fs.directory('myproject'); expect(new FlutterProject(directory).directory, directory); }); + group('ensure ready for platform-specific tooling', () { + testInMemory('does nothing, if project is not created', () async { + final FlutterProject project = someProject(); + project.ensureReadyForPlatformSpecificTooling(); + expect(project.directory.existsSync(), isFalse); + }); + testInMemory('injects plugins', () async { + final FlutterProject project = aProjectWithIos(); + project.ensureReadyForPlatformSpecificTooling(); + expect(project.ios.directory.childFile('Runner/GeneratedPluginRegistrant.h').existsSync(), isTrue); + }); + testInMemory('generates Xcode configuration', () async { + final FlutterProject project = aProjectWithIos(); + project.ensureReadyForPlatformSpecificTooling(); + expect(project.ios.directory.childFile('Flutter/Generated.xcconfig').existsSync(), isTrue); + }); + testInMemory('generates files in plugin example project', () async { + final FlutterProject project = aPluginProject(); + project.ensureReadyForPlatformSpecificTooling(); + expect(project.example.ios.directory.childFile('Runner/GeneratedPluginRegistrant.h').existsSync(), isTrue); + expect(project.example.ios.directory.childFile('Flutter/Generated.xcconfig').existsSync(), isTrue); + }); + }); group('organization names set', () { testInMemory('is empty, if project not created', () async { final FlutterProject project = someProject(); @@ -71,8 +94,23 @@ }); } -FlutterProject someProject() => - new FlutterProject(fs.directory('some_project')); +FlutterProject someProject() => new FlutterProject(fs.directory('some_project')); + +FlutterProject aProjectWithIos() { + final Directory directory = fs.directory('ios_project'); + directory.childFile('pubspec.yaml').createSync(recursive: true); + directory.childFile('.packages').createSync(recursive: true); + directory.childDirectory('ios').createSync(recursive: true); + return new FlutterProject(directory); +} + +FlutterProject aPluginProject() { + final Directory directory = fs.directory('plugin_project/example'); + directory.childFile('pubspec.yaml').createSync(recursive: true); + directory.childFile('.packages').createSync(recursive: true); + directory.childDirectory('ios').createSync(recursive: true); + return new FlutterProject(directory.parent); +} void testInMemory(String description, Future<Null> testMethod()) { testUsingContext( @@ -84,6 +122,13 @@ ); } +void addPubPackages(Directory directory) { + directory.childFile('pubspec.yaml') + ..createSync(recursive: true); + directory.childFile('.packages') + ..createSync(recursive: true); +} + void addIosWithBundleId(Directory directory, String id) { directory .childDirectory('ios')
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 3c5fe1b..dbe5188 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart
@@ -19,6 +19,7 @@ import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/simulators.dart'; +import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/usage.dart'; import 'package:flutter_tools/src/version.dart'; @@ -50,6 +51,7 @@ ..putIfAbsent(OperatingSystemUtils, () => new MockOperatingSystemUtils()) ..putIfAbsent(PortScanner, () => new MockPortScanner()) ..putIfAbsent(Xcode, () => new Xcode()) + ..putIfAbsent(XcodeProjectInterpreter, () => new MockXcodeProjectInterpreter()) ..putIfAbsent(IOSSimulatorUtils, () { final MockIOSSimulatorUtils mock = new MockIOSSimulatorUtils(); when(mock.getAttachedDevices()).thenReturn(<IOSSimulator>[]); @@ -262,6 +264,25 @@ void printWelcome() { } } +class MockXcodeProjectInterpreter implements XcodeProjectInterpreter { + @override + bool get canInterpretXcodeProjects => true; + + @override + Map<String, String> getBuildSettings(String projectPath, String target) { + return <String, String>{}; + } + + @override + XcodeProjectInfo getInfo(String projectPath) { + return new XcodeProjectInfo( + <String>['Runner'], + <String>['Debug', 'Release'], + <String>['Runner'], + ); + } +} + class MockFlutterVersion extends Mock implements FlutterVersion {} class MockClock extends Mock implements Clock {}