Unverified Commit 20004357 authored by Mikkel Nygaard Ravn's avatar Mikkel Nygaard Ravn Committed by GitHub

Move plugin injection to just after pub get (#14743)

parent 558c753f
// 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'));
}
// 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'));
}
// 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'));
}
// 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);
}
}
...@@ -237,6 +237,13 @@ tasks: ...@@ -237,6 +237,13 @@ tasks:
stage: devicelab stage: devicelab
required_agent_capabilities: ["linux/android"] 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: flutter_gallery_instrumentation_test:
description: > description: >
Same as flutter_gallery__transition_perf but uses Android instrumentation Same as flutter_gallery__transition_perf but uses Android instrumentation
...@@ -253,6 +260,13 @@ tasks: ...@@ -253,6 +260,13 @@ tasks:
stage: devicelab_ios stage: devicelab_ios
required_agent_capabilities: ["mac/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: external_ui_integration_test_ios:
description: > description: >
Checks that external UIs work on iOS. Checks that external UIs work on iOS.
...@@ -347,6 +361,13 @@ tasks: ...@@ -347,6 +361,13 @@ tasks:
stage: devicelab_win stage: devicelab_win
required_agent_capabilities: ["windows/android"] 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: hot_mode_dev_cycle_win__benchmark:
description: > description: >
Measures the performance of Dart VM hot patching feature on Windows. Measures the performance of Dart VM hot patching feature on Windows.
......
...@@ -16,7 +16,6 @@ import '../base/utils.dart'; ...@@ -16,7 +16,6 @@ import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import '../globals.dart'; import '../globals.dart';
import '../plugins.dart';
import 'android_sdk.dart'; import 'android_sdk.dart';
import 'android_studio.dart'; import 'android_studio.dart';
...@@ -94,7 +93,6 @@ Future<GradleProject> _gradleProject() async { ...@@ -94,7 +93,6 @@ Future<GradleProject> _gradleProject() async {
Future<GradleProject> _readGradleProject() async { Future<GradleProject> _readGradleProject() async {
final String gradle = await _ensureGradle(); final String gradle = await _ensureGradle();
updateLocalProperties(); updateLocalProperties();
injectPlugins();
try { try {
final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true); final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true);
final RunResult runResult = await runCheckedAsync( final RunResult runResult = await runCheckedAsync(
......
...@@ -181,11 +181,11 @@ abstract class IOSApp extends ApplicationPackage { ...@@ -181,11 +181,11 @@ abstract class IOSApp extends ApplicationPackage {
if (id == null) if (id == null)
return null; return null;
final String projectPath = fs.path.join('ios', 'Runner.xcodeproj'); 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); id = substituteXcodeVariables(id, buildSettings);
return new BuildableIOSApp( return new BuildableIOSApp(
appDirectory: fs.path.join('ios'), appDirectory: 'ios',
projectBundleId: id, projectBundleId: id,
buildSettings: buildSettings, buildSettings: buildSettings,
); );
......
...@@ -14,14 +14,10 @@ import '../base/common.dart'; ...@@ -14,14 +14,10 @@ import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import '../dart/pub.dart'; import '../dart/pub.dart';
import '../doctor.dart'; import '../doctor.dart';
import '../flx.dart' as flx;
import '../globals.dart'; import '../globals.dart';
import '../ios/xcodeproj.dart';
import '../plugins.dart';
import '../project.dart'; import '../project.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../template.dart'; import '../template.dart';
...@@ -232,17 +228,9 @@ class CreateCommand extends FlutterCommand { ...@@ -232,17 +228,9 @@ class CreateCommand extends FlutterCommand {
printStatus('Wrote $generatedCount files.'); printStatus('Wrote $generatedCount files.');
printStatus(''); printStatus('');
updateXcodeGeneratedProperties(
projectPath: appPath,
buildInfo: BuildInfo.debug,
target: flx.defaultMainPath,
hasPlugins: generatePlugin,
previewDart2: false,
);
if (argResults['pub']) { if (argResults['pub']) {
await pubGet(context: PubContext.create, directory: appPath, offline: argResults['offline']); await pubGet(context: PubContext.create, directory: appPath, offline: argResults['offline']);
injectPlugins(directory: appPath); new FlutterProject(fs.directory(appPath)).ensureReadyForPlatformSpecificTooling();
} }
if (android_sdk.androidSdk != null) if (android_sdk.androidSdk != null)
......
...@@ -24,7 +24,8 @@ class InjectPluginsCommand extends FlutterCommand { ...@@ -24,7 +24,8 @@ class InjectPluginsCommand extends FlutterCommand {
@override @override
Future<Null> runCommand() async { Future<Null> runCommand() async {
final bool result = injectPlugins().hasPlugin; injectPlugins();
final bool result = hasPlugins();
if (result) { if (result) {
printStatus('GeneratedPluginRegistrants successfully written.'); printStatus('GeneratedPluginRegistrants successfully written.');
} else { } else {
......
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
import 'dart:async'; import 'dart:async';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../dart/pub.dart'; import '../dart/pub.dart';
import '../project.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
class PackagesCommand extends FlutterCommand { class PackagesCommand extends FlutterCommand {
...@@ -75,6 +77,7 @@ class PackagesGetCommand extends FlutterCommand { ...@@ -75,6 +77,7 @@ class PackagesGetCommand extends FlutterCommand {
offline: argResults['offline'], offline: argResults['offline'],
checkLastModified: false, checkLastModified: false,
); );
new FlutterProject(fs.directory(target)).ensureReadyForPlatformSpecificTooling();
} }
} }
......
...@@ -16,6 +16,7 @@ import '../base/process_manager.dart'; ...@@ -16,6 +16,7 @@ import '../base/process_manager.dart';
import '../base/version.dart'; import '../base/version.dart';
import '../cache.dart'; import '../cache.dart';
import '../globals.dart'; import '../globals.dart';
import 'xcodeproj.dart';
const String noCocoaPodsConsequence = ''' 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. 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 @@ class CocoaPods { ...@@ -60,13 +61,13 @@ class CocoaPods {
// For backward compatibility with previously created Podfile only. // For backward compatibility with previously created Podfile only.
@required String iosEngineDir, @required String iosEngineDir,
bool isSwift: false, bool isSwift: false,
bool pluginOrFlutterPodChanged: true, bool flutterPodChanged: true,
}) async { }) async {
if (!(await appIosDir.childFile('Podfile').exists())) {
throwToolExit('Podfile missing');
}
if (await _checkPodCondition()) { if (await _checkPodCondition()) {
if (!fs.file(fs.path.join(appIosDir.path, 'Podfile')).existsSync()) { if (_shouldRunPodInstall(appIosDir.path, flutterPodChanged))
await _createPodfile(appIosDir, isSwift);
} // TODO(xster): Add more logic for handling merge conflicts.
if (_shouldRunPodInstall(appIosDir.path, pluginOrFlutterPodChanged))
await _runPodInstall(appIosDir, iosEngineDir); await _runPodInstall(appIosDir, iosEngineDir);
} }
} }
...@@ -99,39 +100,69 @@ class CocoaPods { ...@@ -99,39 +100,69 @@ class CocoaPods {
return true; return true;
} }
Future<Null> _createPodfile(Directory bundle, bool isSwift) async { /// Ensures the `ios` sub-project of the Flutter project at [directory]
final File podfileTemplate = fs.file(fs.path.join( /// contains a suitable `Podfile` and that its `Flutter/Xxx.xcconfig` files
Cache.flutterRoot, /// include pods configuration.
'packages', void setupPodfile(String directory) {
'flutter_tools', if (!xcodeProjectInterpreter.canInterpretXcodeProjects) {
'templates', // Don't do anything for iOS when host platform doesn't support it.
'cocoapods', return;
isSwift ? 'Podfile-swift' : 'Podfile-objc', }
)); final String podfilePath = fs.path.join(directory, 'ios', 'Podfile');
podfileTemplate.copySync(fs.path.join(bundle.path, '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. // Check if you need to run pod install.
// The pod install will run if any of below is true. // The pod install will run if any of below is true.
// 1. Any plugins changed (add/update/delete) // 1. The flutter.framework has changed (debug/release/profile)
// 2. The flutter.framework has changed (debug/release/profile) // 2. The podfile.lock doesn't exist
// 3. The podfile.lock doesn't exists // 3. The Pods/Manifest.lock doesn't exist (It is deleted when plugins change)
// 4. The Pods/manifest.lock doesn't exists // 4. The podfile.lock doesn't match Pods/Manifest.lock.
// 5. The podfile.lock doesn't match Pods/manifest.lock. bool _shouldRunPodInstall(String appDir, bool flutterPodChanged) {
bool _shouldRunPodInstall(String appDir, bool pluginOrFlutterPodChanged) { if (flutterPodChanged)
if (pluginOrFlutterPodChanged)
return true; 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 podfileLockFile = fs.file(fs.path.join(appDir, 'Podfile.lock'));
final File manifestLockFile = final File manifestLockFile = fs.file(fs.path.join(appDir, 'Pods', 'Manifest.lock'));
fs.file(fs.path.join(appDir, 'Pods', 'Manifest.lock')); return !podfileLockFile.existsSync()
if (!podfileLockFile.existsSync()
|| !manifestLockFile.existsSync() || !manifestLockFile.existsSync()
|| podfileLockFile.readAsStringSync() != || podfileLockFile.readAsStringSync() != manifestLockFile.readAsStringSync();
manifestLockFile.readAsStringSync()) {
return true;
}
return false;
} }
Future<Null> _runPodInstall(Directory bundle, String engineDirectory) async { Future<Null> _runPodInstall(Directory bundle, String engineDirectory) async {
......
...@@ -219,7 +219,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -219,7 +219,7 @@ Future<XcodeBuildResult> buildXcodeProject({
return new XcodeBuildResult(success: false); 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')) { if (!projectInfo.targets.contains('Runner')) {
printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.'); printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.');
printError('Open Xcode to fix the problem:'); printError('Open Xcode to fix the problem:');
...@@ -256,26 +256,22 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -256,26 +256,22 @@ Future<XcodeBuildResult> buildXcodeProject({
// copied over to a location that is suitable for Xcodebuild to find them. // copied over to a location that is suitable for Xcodebuild to find them.
final Directory appDirectory = fs.directory(app.appDirectory); final Directory appDirectory = fs.directory(app.appDirectory);
await _addServicesToBundle(appDirectory); await _addServicesToBundle(appDirectory);
final InjectPluginsResult injectPluginsResult = injectPlugins();
final bool hasFlutterPlugins = injectPluginsResult.hasPlugin;
final String previousGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory); final String previousGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory);
updateXcodeGeneratedProperties( updateGeneratedXcodeProperties(
projectPath: fs.currentDirectory.path, projectPath: fs.currentDirectory.path,
buildInfo: buildInfo, buildInfo: buildInfo,
target: target, target: target,
hasPlugins: hasFlutterPlugins,
previewDart2: buildInfo.previewDart2, previewDart2: buildInfo.previewDart2,
); );
if (hasFlutterPlugins) { if (hasPlugins()) {
final String currentGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory); final String currentGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory);
await cocoaPods.processPods( await cocoaPods.processPods(
appIosDir: appDirectory, appIosDir: appDirectory,
iosEngineDir: flutterFrameworkDir(buildInfo.mode), iosEngineDir: flutterFrameworkDir(buildInfo.mode),
isSwift: app.isSwift, isSwift: app.isSwift,
pluginOrFlutterPodChanged: injectPluginsResult.hasChanged flutterPodChanged: (previousGeneratedXcconfig != currentGeneratedXcconfig),
|| previousGeneratedXcconfig != currentGeneratedXcconfig,
); );
} }
...@@ -465,7 +461,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -465,7 +461,7 @@ Future<XcodeBuildResult> buildXcodeProject({
String readGeneratedXcconfig(String appPath) { String readGeneratedXcconfig(String appPath) {
final String generatedXcconfigPath = 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); final File generatedXcconfigFile = fs.file(generatedXcconfigPath);
if (!generatedXcconfigFile.existsSync()) if (!generatedXcconfigFile.existsSync())
return null; return null;
......
...@@ -5,11 +5,13 @@ ...@@ -5,11 +5,13 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/context.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import '../flx.dart' as flx;
import '../globals.dart'; import '../globals.dart';
final RegExp _settingExpr = new RegExp(r'(\w+)\s*=\s*(.*)$'); final RegExp _settingExpr = new RegExp(r'(\w+)\s*=\s*(.*)$');
...@@ -19,11 +21,28 @@ String flutterFrameworkDir(BuildMode mode) { ...@@ -19,11 +21,28 @@ String flutterFrameworkDir(BuildMode mode) {
return fs.path.normalize(fs.path.dirname(artifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, mode))); 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 String projectPath,
@required BuildInfo buildInfo, @required BuildInfo buildInfo,
@required String target, @required String target,
@required bool hasPlugins,
@required bool previewDart2, @required bool previewDart2,
}) { }) {
final StringBuffer localsBuffer = new StringBuffer(); final StringBuffer localsBuffer = new StringBuffer();
...@@ -58,21 +77,42 @@ void updateXcodeGeneratedProperties({ ...@@ -58,21 +77,42 @@ void updateXcodeGeneratedProperties({
localsBuffer.writeln('PREVIEW_DART_2=true'); localsBuffer.writeln('PREVIEW_DART_2=true');
} }
// Add dependency to CocoaPods' generated project only if plugins are used. final File localsFile = fs.file(_generatedXcodePropertiesPath(projectPath));
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'));
localsFile.createSync(recursive: true); localsFile.createSync(recursive: true);
localsFile.writeAsStringSync(localsBuffer.toString()); localsFile.writeAsStringSync(localsBuffer.toString());
} }
Map<String, String> getXcodeBuildSettings(String xcodeProjPath, String target) { XcodeProjectInterpreter get xcodeProjectInterpreter => context.putIfAbsent(
final String absProjPath = fs.path.absolute(xcodeProjPath); XcodeProjectInterpreter,
final String out = runCheckedSync(<String>[ () => const XcodeProjectInterpreter(),
'/usr/bin/xcodebuild', '-project', absProjPath, '-target', target, '-showBuildSettings' );
]);
return parseXcodeBuildSettings(out); /// 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) { Map<String, String> parseXcodeBuildSettings(String showBuildSettingsOutput) {
...@@ -101,13 +141,6 @@ String substituteXcodeVariables(String str, Map<String, String> xcodeBuildSettin ...@@ -101,13 +141,6 @@ String substituteXcodeVariables(String str, Map<String, String> xcodeBuildSettin
class XcodeProjectInfo { class XcodeProjectInfo {
XcodeProjectInfo(this.targets, this.buildConfigurations, this.schemes); 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) { factory XcodeProjectInfo.fromXcodeBuildOutput(String output) {
final List<String> targets = <String>[]; final List<String> targets = <String>[];
final List<String> buildConfigurations = <String>[]; final List<String> buildConfigurations = <String>[];
......
...@@ -9,6 +9,7 @@ import 'package:yaml/yaml.dart'; ...@@ -9,6 +9,7 @@ import 'package:yaml/yaml.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'dart/package_map.dart'; import 'dart/package_map.dart';
import 'globals.dart'; import 'globals.dart';
import 'ios/cocoapods.dart';
class Plugin { class Plugin {
final String name; final String name;
...@@ -80,21 +81,25 @@ List<Plugin> _findPlugins(String directory) { ...@@ -80,21 +81,25 @@ List<Plugin> _findPlugins(String directory) {
/// Returns true if .flutter-plugins has changed, otherwise returns false. /// Returns true if .flutter-plugins has changed, otherwise returns false.
bool _writeFlutterPluginsList(String directory, List<Plugin> plugins) { bool _writeFlutterPluginsList(String directory, List<Plugin> plugins) {
final File pluginsProperties = fs.file(fs.path.join(directory, '.flutter-plugins')); final File pluginsFile = fs.file(fs.path.join(directory, '.flutter-plugins'));
final String previousFlutterPlugins = final String oldContents = _readFlutterPluginsList(directory);
pluginsProperties.existsSync() ? pluginsProperties.readAsStringSync() : null;
final String pluginManifest = final String pluginManifest =
plugins.map((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n'); plugins.map((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n');
if (pluginManifest.isNotEmpty) { if (pluginManifest.isNotEmpty) {
pluginsProperties.writeAsStringSync('$pluginManifest\n'); pluginsFile.writeAsStringSync('$pluginManifest\n', flush: true);
} else { } else {
if (pluginsProperties.existsSync()) { if (pluginsFile.existsSync())
pluginsProperties.deleteSync(); pluginsFile.deleteSync();
}
} }
final String currentFlutterPlugins = final String newContents = _readFlutterPluginsList(directory);
pluginsProperties.existsSync() ? pluginsProperties.readAsStringSync() : null; return oldContents != newContents;
return currentFlutterPlugins != previousFlutterPlugins; }
/// 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; const String _androidPluginRegistryTemplate = '''package io.flutter.plugins;
...@@ -128,7 +133,7 @@ public final class GeneratedPluginRegistrant { ...@@ -128,7 +133,7 @@ public final class GeneratedPluginRegistrant {
} }
'''; ''';
void _writeAndroidPluginRegistry(String directory, List<Plugin> plugins) { void _writeAndroidPluginRegistrant(String directory, List<Plugin> plugins) {
final List<Map<String, dynamic>> androidPlugins = plugins final List<Map<String, dynamic>> androidPlugins = plugins
.where((Plugin p) => p.androidPackage != null && p.pluginClass != null) .where((Plugin p) => p.androidPackage != null && p.pluginClass != null)
.map((Plugin p) => <String, dynamic>{ .map((Plugin p) => <String, dynamic>{
...@@ -187,7 +192,7 @@ const String _iosPluginRegistryImplementationTemplate = '''// ...@@ -187,7 +192,7 @@ const String _iosPluginRegistryImplementationTemplate = '''//
@end @end
'''; ''';
void _writeIOSPluginRegistry(String directory, List<Plugin> plugins) { void _writeIOSPluginRegistrant(String directory, List<Plugin> plugins) {
final List<Map<String, dynamic>> iosPlugins = plugins final List<Map<String, dynamic>> iosPlugins = plugins
.where((Plugin p) => p.pluginClass != null) .where((Plugin p) => p.pluginClass != null)
.map((Plugin p) => <String, dynamic>{ .map((Plugin p) => <String, dynamic>{
...@@ -210,7 +215,6 @@ void _writeIOSPluginRegistry(String directory, List<Plugin> plugins) { ...@@ -210,7 +215,6 @@ void _writeIOSPluginRegistry(String directory, List<Plugin> plugins) {
registryHeaderFile.writeAsStringSync(pluginRegistryHeader); registryHeaderFile.writeAsStringSync(pluginRegistryHeader);
final File registryImplementationFile = registryDirectory.childFile('GeneratedPluginRegistrant.m'); final File registryImplementationFile = registryDirectory.childFile('GeneratedPluginRegistrant.m');
registryImplementationFile.writeAsStringSync(pluginRegistryImplementation); registryImplementationFile.writeAsStringSync(pluginRegistryImplementation);
} }
class InjectPluginsResult{ class InjectPluginsResult{
...@@ -224,17 +228,30 @@ class InjectPluginsResult{ ...@@ -224,17 +228,30 @@ class InjectPluginsResult{
final bool hasChanged; final bool hasChanged;
} }
/// Finds Flutter plugins in the pubspec.yaml, creates platform injection /// Injects plugins found in `pubspec.yaml` into the platform-specific projects.
/// registries classes and add them to the build dependencies. void injectPlugins({String directory}) {
///
/// Returns whether any Flutter plugins are added and whether they changed.
InjectPluginsResult injectPlugins({String directory}) {
directory ??= fs.currentDirectory.path; 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 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'))) if (fs.isDirectorySync(fs.path.join(directory, 'android')))
_writeAndroidPluginRegistry(directory, plugins); _writeAndroidPluginRegistrant(directory, plugins);
if (fs.isDirectorySync(fs.path.join(directory, 'ios'))) if (fs.isDirectorySync(fs.path.join(directory, 'ios'))) {
_writeIOSPluginRegistry(directory, plugins); _writeIOSPluginRegistrant(directory, plugins);
return new InjectPluginsResult(hasPlugin: plugins.isNotEmpty, hasChanged: hasPluginsChanged); 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;
} }
...@@ -4,7 +4,11 @@ ...@@ -4,7 +4,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'ios/xcodeproj.dart';
import 'plugins.dart';
/// Represents the contents of a Flutter project at the specified [directory]. /// Represents the contents of a Flutter project at the specified [directory].
class FlutterProject { class FlutterProject {
...@@ -43,8 +47,25 @@ class FlutterProject { ...@@ -43,8 +47,25 @@ class FlutterProject {
/// The Android sub project of this project. /// The Android sub project of this project.
AndroidProject get android => new AndroidProject(directory.childDirectory('android')); 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. /// The example sub project of this (plugin) project.
FlutterProject get example => new FlutterProject(directory.childDirectory('example')); 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. /// Represents the contents of the ios/ folder of a Flutter project.
......
...@@ -21,6 +21,7 @@ import '../device.dart'; ...@@ -21,6 +21,7 @@ import '../device.dart';
import '../doctor.dart'; import '../doctor.dart';
import '../flx.dart' as flx; import '../flx.dart' as flx;
import '../globals.dart'; import '../globals.dart';
import '../project.dart';
import '../usage.dart'; import '../usage.dart';
import 'flutter_command_runner.dart'; import 'flutter_command_runner.dart';
...@@ -272,8 +273,10 @@ abstract class FlutterCommand extends Command<Null> { ...@@ -272,8 +273,10 @@ abstract class FlutterCommand extends Command<Null> {
if (shouldUpdateCache) if (shouldUpdateCache)
await cache.updateAll(); await cache.updateAll();
if (shouldRunPub) if (shouldRunPub) {
await pubGet(context: PubContext.getVerifyContext(name)); await pubGet(context: PubContext.getVerifyContext(name));
new FlutterProject(fs.currentDirectory).ensureReadyForPlatformSpecificTooling();
}
setupApplicationPackages(); setupApplicationPackages();
......
...@@ -29,9 +29,19 @@ void main() { ...@@ -29,9 +29,19 @@ void main() {
temp.deleteSync(recursive: true); 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 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 PackagesCommand command = new PackagesCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command); final CommandRunner<Null> runner = createTestCommandRunner(command);
...@@ -41,31 +51,148 @@ void main() { ...@@ -41,31 +51,148 @@ void main() {
commandArgs.add(projectPath); commandArgs.add(projectPath);
await runner.run(commandArgs); await runner.run(commandArgs);
return projectPath;
} }
void expectExists(String projectPath, String relPath) { 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',
);
}
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();
}
} }
// Verify that we create a project that is well-formed. testUsingContext('get fetches packages', () async {
testUsingContext('get', () async { final String projectPath = await createProject(temp);
final String projectPath = await runCommand('get');
expectExists(projectPath, 'lib/main.dart'); removeGeneratedFiles(projectPath);
expectExists(projectPath, '.packages');
await runCommandIn(projectPath, 'get');
expectDependenciesResolved(projectPath);
expectZeroPluginsInjected(projectPath);
}, timeout: allowForRemotePubInvocation); }, timeout: allowForRemotePubInvocation);
testUsingContext('get --offline', () async { testUsingContext('get --offline fetches packages', () async {
final String projectPath = await runCommand('get', args: <String>['--offline']); final String projectPath = await createProject(temp);
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');
testUsingContext('upgrade', () async { expectDependenciesResolved(projectPath);
final String projectPath = await runCommand('upgrade'); expectPluginInjected(projectPath);
expectExists(projectPath, 'lib/main.dart');
expectExists(projectPath, '.packages');
}, timeout: allowForRemotePubInvocation); }, timeout: allowForRemotePubInvocation);
}); });
......
...@@ -17,6 +17,29 @@ void main() { ...@@ -17,6 +17,29 @@ void main() {
final Directory directory = fs.directory('myproject'); final Directory directory = fs.directory('myproject');
expect(new FlutterProject(directory).directory, directory); 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', () { group('organization names set', () {
testInMemory('is empty, if project not created', () async { testInMemory('is empty, if project not created', () async {
final FlutterProject project = someProject(); final FlutterProject project = someProject();
...@@ -71,8 +94,23 @@ void main() { ...@@ -71,8 +94,23 @@ void main() {
}); });
} }
FlutterProject someProject() => FlutterProject someProject() => new FlutterProject(fs.directory('some_project'));
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()) { void testInMemory(String description, Future<Null> testMethod()) {
testUsingContext( testUsingContext(
...@@ -84,6 +122,13 @@ void testInMemory(String description, Future<Null> testMethod()) { ...@@ -84,6 +122,13 @@ void testInMemory(String description, Future<Null> testMethod()) {
); );
} }
void addPubPackages(Directory directory) {
directory.childFile('pubspec.yaml')
..createSync(recursive: true);
directory.childFile('.packages')
..createSync(recursive: true);
}
void addIosWithBundleId(Directory directory, String id) { void addIosWithBundleId(Directory directory, String id) {
directory directory
.childDirectory('ios') .childDirectory('ios')
......
...@@ -19,6 +19,7 @@ import 'package:flutter_tools/src/device.dart'; ...@@ -19,6 +19,7 @@ import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/simulators.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/run_hot.dart';
import 'package:flutter_tools/src/usage.dart'; import 'package:flutter_tools/src/usage.dart';
import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/version.dart';
...@@ -50,6 +51,7 @@ void _defaultInitializeContext(AppContext testContext) { ...@@ -50,6 +51,7 @@ void _defaultInitializeContext(AppContext testContext) {
..putIfAbsent(OperatingSystemUtils, () => new MockOperatingSystemUtils()) ..putIfAbsent(OperatingSystemUtils, () => new MockOperatingSystemUtils())
..putIfAbsent(PortScanner, () => new MockPortScanner()) ..putIfAbsent(PortScanner, () => new MockPortScanner())
..putIfAbsent(Xcode, () => new Xcode()) ..putIfAbsent(Xcode, () => new Xcode())
..putIfAbsent(XcodeProjectInterpreter, () => new MockXcodeProjectInterpreter())
..putIfAbsent(IOSSimulatorUtils, () { ..putIfAbsent(IOSSimulatorUtils, () {
final MockIOSSimulatorUtils mock = new MockIOSSimulatorUtils(); final MockIOSSimulatorUtils mock = new MockIOSSimulatorUtils();
when(mock.getAttachedDevices()).thenReturn(<IOSSimulator>[]); when(mock.getAttachedDevices()).thenReturn(<IOSSimulator>[]);
...@@ -262,6 +264,25 @@ class MockUsage implements Usage { ...@@ -262,6 +264,25 @@ class MockUsage implements Usage {
void printWelcome() { } 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 MockFlutterVersion extends Mock implements FlutterVersion {}
class MockClock extends Mock implements Clock {} class MockClock extends Mock implements Clock {}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment