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:
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 @@ tasks:
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 @@ tasks:
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.
......
......@@ -16,7 +16,6 @@ import '../base/utils.dart';
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> _gradleProject() async {
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(
......
......@@ -181,11 +181,11 @@ abstract class IOSApp extends ApplicationPackage {
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,
);
......
......@@ -14,14 +14,10 @@ import '../base/common.dart';
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 @@ class CreateCommand extends FlutterCommand {
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)
......
......@@ -24,7 +24,8 @@ class InjectPluginsCommand extends FlutterCommand {
@override
Future<Null> runCommand() async {
final bool result = injectPlugins().hasPlugin;
injectPlugins();
final bool result = hasPlugins();
if (result) {
printStatus('GeneratedPluginRegistrants successfully written.');
} else {
......
......@@ -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 @@ class PackagesGetCommand extends FlutterCommand {
offline: argResults['offline'],
checkLastModified: false,
);
new FlutterProject(fs.directory(target)).ensureReadyForPlatformSpecificTooling();
}
}
......
......@@ -16,6 +16,7 @@ import '../base/process_manager.dart';
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 @@ class CocoaPods {
// 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 @@ class CocoaPods {
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 {
......
......@@ -219,7 +219,7 @@ Future<XcodeBuildResult> buildXcodeProject({
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 @@ Future<XcodeBuildResult> buildXcodeProject({
// 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 @@ Future<XcodeBuildResult> buildXcodeProject({
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;
......
......@@ -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 @@ String flutterFrameworkDir(BuildMode 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 BuildInfo buildInfo,
@required String target,
@required bool hasPlugins,
@required bool previewDart2,
}) {
final StringBuffer localsBuffer = new StringBuffer();
......@@ -58,21 +77,42 @@ void updateXcodeGeneratedProperties({
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 @@ String substituteXcodeVariables(String str, Map<String, String> xcodeBuildSettin
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>[];
......
......@@ -9,6 +9,7 @@ import 'package:yaml/yaml.dart';
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 @@ List<Plugin> _findPlugins(String directory) {
/// 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 @@ 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
.where((Plugin p) => p.androidPackage != null && p.pluginClass != null)
.map((Plugin p) => <String, dynamic>{
......@@ -187,7 +192,7 @@ const String _iosPluginRegistryImplementationTemplate = '''//
@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 @@ void _writeIOSPluginRegistry(String directory, List<Plugin> plugins) {
registryHeaderFile.writeAsStringSync(pluginRegistryHeader);
final File registryImplementationFile = registryDirectory.childFile('GeneratedPluginRegistrant.m');
registryImplementationFile.writeAsStringSync(pluginRegistryImplementation);
}
class InjectPluginsResult{
......@@ -224,17 +228,30 @@ class InjectPluginsResult{
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;
}
......@@ -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 @@ class FlutterProject {
/// 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.
......
......@@ -21,6 +21,7 @@ import '../device.dart';
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 @@ abstract class FlutterCommand extends Command<Null> {
if (shouldUpdateCache)
await cache.updateAll();
if (shouldRunPub)
if (shouldRunPub) {
await pubGet(context: PubContext.getVerifyContext(name));
new FlutterProject(fs.currentDirectory).ensureReadyForPlatformSpecificTooling();
}
setupApplicationPackages();
......
......@@ -29,9 +29,19 @@ void main() {
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 @@ void main() {
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',
);
}
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', () async {
final String projectPath = await runCommand('get');
expectExists(projectPath, 'lib/main.dart');
expectExists(projectPath, '.packages');
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);
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 {
final String projectPath = await runCommand('upgrade');
expectExists(projectPath, 'lib/main.dart');
expectExists(projectPath, '.packages');
expectDependenciesResolved(projectPath);
expectPluginInjected(projectPath);
}, timeout: allowForRemotePubInvocation);
});
......
......@@ -17,6 +17,29 @@ void main() {
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 @@ void main() {
});
}
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 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) {
directory
.childDirectory('ios')
......
......@@ -19,6 +19,7 @@ import 'package:flutter_tools/src/device.dart';
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 @@ void _defaultInitializeContext(AppContext testContext) {
..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 @@ class MockUsage implements Usage {
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 {}
......
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