Add .flutter-plugins-dependencies to the project, which contains the app's plugin dependency graph (#45379)

This commit is contained in:
Emmanuel Garcia 2019-11-22 15:02:20 -08:00 committed by GitHub
parent 161e4dfb04
commit b6e92003c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 245 additions and 38 deletions

1
.gitignore vendored
View file

@ -44,6 +44,7 @@ version
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/

View file

@ -24,6 +24,7 @@
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/

View file

@ -27,6 +27,7 @@
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/

View file

@ -24,6 +24,7 @@
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/

View file

@ -24,6 +24,7 @@
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/

View file

@ -39,3 +39,4 @@ build/
.android/
.ios/
.flutter-plugins
.flutter-plugins-dependencies

View file

@ -24,6 +24,7 @@
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/

View file

@ -6,6 +6,7 @@ import static groovy.io.FileType.FILES
import com.android.builder.model.AndroidProject
import com.android.build.OutputFile
import groovy.json.JsonSlurper
import java.nio.file.Path
import java.nio.file.Paths
import java.util.regex.Matcher
@ -253,6 +254,7 @@ class FlutterPlugin implements Plugin<Project> {
private void configurePlugins() {
if (!buildPluginAsAar()) {
getPluginList().each this.&configurePluginProject
getPluginDependencies().each this.&configurePluginDependencies
return
}
project.repositories {
@ -298,19 +300,15 @@ class FlutterPlugin implements Plugin<Project> {
}
// Adds the plugin project dependency to the app project .
private void configurePluginProject(String name, String _) {
Project pluginProject = project.rootProject.findProject(":$name")
private void configurePluginProject(String pluginName, String _) {
Project pluginProject = project.rootProject.findProject(":$pluginName")
if (pluginProject == null) {
project.logger.error("Plugin project :$name not found. Please update settings.gradle.")
project.logger.error("Plugin project :$pluginName not found. Please update settings.gradle.")
return
}
// Add plugin dependency to the app project.
project.dependencies {
if (project.getConfigurations().findByName("implementation")) {
implementation pluginProject
} else {
compile pluginProject
}
implementation pluginProject
}
Closure addEmbeddingCompileOnlyDependency = { buildType ->
String flutterBuildMode = buildModeFor(buildType)
@ -337,6 +335,36 @@ class FlutterPlugin implements Plugin<Project> {
}
}
// Add the dependencies on other plugin projects to the plugin project.
// A plugin A can depend on plugin B. As a result, this dependency must be surfaced by
// making the Gradle plugin project A depend on the Gradle plugin project B.
private void configurePluginDependencies(Object dependencyObject) {
assert dependencyObject.name instanceof String
Project pluginProject = project.rootProject.findProject(":${dependencyObject.name}")
if (pluginProject == null) {
// Ignore plugins that don't have a project since most likely they don't
// have an android/ directory.
return
}
assert dependencyObject.dependencies instanceof List
dependencyObject.dependencies.each { pluginDependencyName ->
assert pluginDependencyName instanceof String
if (pluginDependencyName.empty) {
return
}
Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName")
if (dependencyProject == null) {
return
}
// Wait for the Android plugin to load and add the dependency to the plugin project.
pluginProject.afterEvaluate {
pluginProject.dependencies {
implementation dependencyProject
}
}
}
}
private Properties getPluginList() {
File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
Properties allPlugins = readPropertiesIfExist(pluginsFile)
@ -353,6 +381,39 @@ class FlutterPlugin implements Plugin<Project> {
return androidPlugins
}
// Gets the plugins dependencies from `.flutter-plugins-dependencies`.
private List getPluginDependencies() {
// Consider a `.flutter-plugins-dependencies` file with the following content:
// {
// "dependencyGraph": [
// {
// "name": "plugin-a",
// "dependencies": ["plugin-b","plugin-c"]
// },
// {
// "name": "plugin-b",
// "dependencies": ["plugin-c"]
// },
// {
// "name": "plugin-c",
// "dependencies": []'
// }
// ]
// }
//
// This means, `plugin-a` depends on `plugin-b` and `plugin-c`.
// `plugin-b` depends on `plugin-c`.
// `plugin-c` doesn't depend on anything.
File pluginsDependencyFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins-dependencies')
if (pluginsDependencyFile.exists()) {
def object = new JsonSlurper().parseText(pluginsDependencyFile.text)
assert object instanceof Map
assert object.dependencyGraph instanceof List
return object.dependencyGraph
}
return []
}
private static String toCammelCase(List<String> parts) {
if (parts.empty) {
return ""

View file

@ -4,12 +4,14 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:mustache/mustache.dart' as mustache;
import 'package:yaml/yaml.dart';
import 'android/gradle.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'convert.dart';
import 'dart/package_map.dart';
import 'features.dart';
import 'globals.dart';
@ -27,10 +29,14 @@ void _renderTemplateToFile(String template, dynamic context, String filePath) {
class Plugin {
Plugin({
this.name,
this.path,
this.platforms,
});
@required this.name,
@required this.path,
@required this.platforms,
@required this.dependencies,
}) : assert(name != null),
assert(path != null),
assert(platforms != null),
assert(dependencies != null);
/// Parses [Plugin] specification from the provided pluginYaml.
///
@ -60,18 +66,28 @@ class Plugin {
/// pluginClass: SamplePlugin
/// windows:
/// pluginClass: SamplePlugin
factory Plugin.fromYaml(String name, String path, YamlMap pluginYaml) {
factory Plugin.fromYaml(
String name,
String path,
YamlMap pluginYaml,
List<String> dependencies,
) {
final List<String> errors = validatePluginYaml(pluginYaml);
if (errors.isNotEmpty) {
throwToolExit('Invalid plugin specification.\n${errors.join('\n')}');
}
if (pluginYaml != null && pluginYaml['platforms'] != null) {
return Plugin._fromMultiPlatformYaml(name, path, pluginYaml);
return Plugin._fromMultiPlatformYaml(name, path, pluginYaml, dependencies);
}
return Plugin._fromLegacyYaml(name, path, pluginYaml);
return Plugin._fromLegacyYaml(name, path, pluginYaml, dependencies);
}
factory Plugin._fromMultiPlatformYaml(String name, String path, dynamic pluginYaml) {
factory Plugin._fromMultiPlatformYaml(
String name,
String path,
dynamic pluginYaml,
List<String> dependencies,
) {
assert (pluginYaml != null && pluginYaml['platforms'] != null,
'Invalid multi-platform plugin specification.');
final YamlMap platformsYaml = pluginYaml['platforms'] as YamlMap;
@ -118,10 +134,16 @@ class Plugin {
name: name,
path: path,
platforms: platforms,
dependencies: dependencies,
);
}
factory Plugin._fromLegacyYaml(String name, String path, dynamic pluginYaml) {
factory Plugin._fromLegacyYaml(
String name,
String path,
dynamic pluginYaml,
List<String> dependencies,
) {
final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
final String pluginClass = pluginYaml['pluginClass'] as String;
if (pluginYaml != null && pluginClass != null) {
@ -147,6 +169,7 @@ class Plugin {
name: name,
path: path,
platforms: platforms,
dependencies: dependencies,
);
}
@ -232,6 +255,9 @@ class Plugin {
final String name;
final String path;
/// The name of the packages this plugin depends on.
final List<String> dependencies;
/// This is a mapping from platform config key to the plugin platform spec.
final Map<String, PluginPlatform> platforms;
}
@ -250,11 +276,13 @@ Plugin _pluginFromPubspec(String name, Uri packageRoot) {
return null;
}
final String packageRootPath = fs.path.fromUri(packageRoot);
final YamlMap dependencies = pubspec['dependencies'];
printTrace('Found plugin $name at $packageRootPath');
return Plugin.fromYaml(
name,
packageRootPath,
flutterConfig['plugin'] as YamlMap,
dependencies == null ? <String>[] : <String>[...dependencies.keys],
);
}
@ -281,29 +309,58 @@ List<Plugin> findPlugins(FlutterProject project) {
return plugins;
}
/// Returns true if .flutter-plugins has changed, otherwise returns false.
/// Writes the .flutter-plugins and .flutter-plugins-dependencies files based on the list of plugins.
/// If there aren't any plugins, then the files aren't written to disk.
///
/// Finally, returns [true] if .flutter-plugins or .flutter-plugins-dependencies have changed,
/// otherwise returns [false].
bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
final List<dynamic> directAppDependencies = <dynamic>[];
final StringBuffer flutterPluginsBuffer = StringBuffer();
final Set<String> pluginNames = <String>{};
for (Plugin plugin in plugins) {
pluginNames.add(plugin.name);
}
for (Plugin plugin in plugins) {
flutterPluginsBuffer.write('${plugin.name}=${escapePath(plugin.path)}\n');
directAppDependencies.add(<String, dynamic>{
'name': plugin.name,
// Extract the plugin dependencies which happen to be plugins.
'dependencies': <String>[...plugin.dependencies.where(pluginNames.contains)],
});
}
final File pluginsFile = project.flutterPluginsFile;
final String oldContents = _readFlutterPluginsList(project);
final String pluginManifest =
plugins.map<String>((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n');
if (pluginManifest.isNotEmpty) {
pluginsFile.writeAsStringSync('$pluginManifest\n', flush: true);
final String oldPluginFileContent = _readFileContent(pluginsFile);
final String pluginFileContent = flutterPluginsBuffer.toString();
if (pluginFileContent.isNotEmpty) {
pluginsFile.writeAsStringSync(pluginFileContent, flush: true);
} else {
if (pluginsFile.existsSync()) {
pluginsFile.deleteSync();
}
}
final String newContents = _readFlutterPluginsList(project);
return oldContents != newContents;
final File dependenciesFile = project.flutterPluginsDependenciesFile;
final String oldDependenciesFileContent = _readFileContent(dependenciesFile);
final String dependenciesFileContent = json.encode(<String, dynamic>{
'dependencyGraph': directAppDependencies,
});
if (pluginFileContent.isNotEmpty) {
dependenciesFile.writeAsStringSync(dependenciesFileContent, flush: true);
} else {
if (dependenciesFile.existsSync()) {
dependenciesFile.deleteSync();
}
}
return oldPluginFileContent != _readFileContent(pluginsFile)
|| oldDependenciesFileContent != _readFileContent(dependenciesFile);
}
/// Returns the contents of the `.flutter-plugins` file in [project], or
/// null if that file does not exist.
String _readFlutterPluginsList(FlutterProject project) {
return project.flutterPluginsFile.existsSync()
? project.flutterPluginsFile.readAsStringSync()
: null;
/// Returns the contents of [File] or [null] if that file does not exist.
String _readFileContent(File file) {
return file.existsSync() ? file.readAsStringSync() : null;
}
const String _androidPluginRegistryTemplateOldEmbedding = '''package io.flutter.plugins;
@ -782,5 +839,5 @@ Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false})
///
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
bool hasPlugins(FlutterProject project) {
return _readFlutterPluginsList(project) != null;
return _readFileContent(project.flutterPluginsFile) != null;
}

View file

@ -146,6 +146,10 @@ class FlutterProject {
/// The `.flutter-plugins` file of this project.
File get flutterPluginsFile => directory.childFile('.flutter-plugins');
/// The `.flutter-plugins-dependencies` file of this project,
/// which contains the dependencies each plugin depends on.
File get flutterPluginsDependenciesFile => directory.childFile('.flutter-plugins-dependencies');
/// The `.dart-tool` directory of this project.
Directory get dartTool => directory.childDirectory('.dart_tool');

View file

@ -24,6 +24,7 @@
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/

View file

@ -39,3 +39,4 @@ build/
.android/
.ios/
.flutter-plugins
.flutter-plugins-dependencies

View file

@ -24,6 +24,7 @@
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/

View file

@ -20,7 +20,7 @@ void main() {
final dynamic pluginYaml = loadYaml(pluginYamlRaw);
final Plugin plugin =
Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml);
Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml, const <String>[]);
final AndroidPlugin androidPlugin =
plugin.platforms[AndroidPlugin.kConfigKey];
@ -53,7 +53,7 @@ void main() {
final dynamic pluginYaml = loadYaml(pluginYamlRaw);
final Plugin plugin =
Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml);
Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml, const <String>[]);
final AndroidPlugin androidPlugin =
plugin.platforms[AndroidPlugin.kConfigKey];
@ -100,7 +100,7 @@ void main() {
final dynamic pluginYaml = loadYaml(pluginYamlRaw);
final Plugin plugin =
Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml);
Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml, const <String>[]);
final AndroidPlugin androidPlugin =
plugin.platforms[AndroidPlugin.kConfigKey];
@ -144,7 +144,7 @@ void main() {
final dynamic pluginYaml = loadYaml(pluginYamlRaw);
final Plugin plugin =
Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml);
Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml, const <String>[]);
expect(plugin.platforms, <String, PluginPlatform> {});
});

View file

@ -9,7 +9,7 @@ import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/plugins.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import '../src/common.dart';
@ -32,7 +32,8 @@ void main() {
// Add basic properties to the Flutter project and subprojects
flutterProject = MockFlutterProject();
when(flutterProject.directory).thenReturn(fs.directory('/'));
when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.plugins'));
when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins'));
when(flutterProject.flutterPluginsDependenciesFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins-dependencies'));
iosProject = MockIosProject();
when(flutterProject.ios).thenReturn(iosProject);
when(iosProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('Runner'));
@ -189,6 +190,37 @@ flutter:
);
}
void createPluginWithDependencies({
@required String name,
@required List<String> dependencies,
}) {
assert(name != null);
assert(dependencies != null);
final Directory pluginDirectory = fs.systemTempDirectory.createTempSync('plugin.');
pluginDirectory
.childFile('pubspec.yaml')
.writeAsStringSync('''
name: $name
flutter:
plugin:
androidPackage: plugin2
pluginClass: UseNewEmbedding
dependencies:
''');
for (String dependency in dependencies) {
pluginDirectory
.childFile('pubspec.yaml')
.writeAsStringSync(' $dependency:\n', mode: FileMode.append);
}
flutterProject.directory
.childFile('.packages')
.writeAsStringSync(
'$name:${pluginDirectory.childDirectory('lib').uri.toString()}\n',
mode: FileMode.append,
);
}
// Creates the files that would indicate that pod install has run for the
// given project.
void simulatePodInstallRun(XcodeBasedProject project) {
@ -199,6 +231,7 @@ flutter:
testUsingContext('Refreshing the plugin list is a no-op when the plugins list stays empty', () {
refreshPluginsList(flutterProject);
expect(flutterProject.flutterPluginsFile.existsSync(), false);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), false);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
@ -210,6 +243,7 @@ flutter:
when(macosProject.existsSync()).thenReturn(false);
refreshPluginsList(flutterProject);
expect(flutterProject.flutterPluginsFile.existsSync(), false);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), false);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
@ -221,6 +255,47 @@ flutter:
when(macosProject.existsSync()).thenReturn(false);
refreshPluginsList(flutterProject);
expect(flutterProject.flutterPluginsFile.existsSync(), true);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Refreshing the plugin list modifies .flutter-plugins and .flutter-plugins-dependencies when there are plugins', () {
createPluginWithDependencies(name: 'plugin-a', dependencies: const <String>['plugin-b', 'plugin-c', 'random-package']);
createPluginWithDependencies(name: 'plugin-b', dependencies: const <String>['plugin-c']);
createPluginWithDependencies(name: 'plugin-c', dependencies: const <String>[]);
when(iosProject.existsSync()).thenReturn(false);
when(macosProject.existsSync()).thenReturn(false);
refreshPluginsList(flutterProject);
expect(flutterProject.flutterPluginsFile.existsSync(), true);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
expect(flutterProject.flutterPluginsFile.readAsStringSync(),
'plugin-a=/.tmp_rand0/plugin.rand0/\n'
'plugin-b=/.tmp_rand0/plugin.rand1/\n'
'plugin-c=/.tmp_rand0/plugin.rand2/\n'
''
);
expect(flutterProject.flutterPluginsDependenciesFile.readAsStringSync(),
'{'
'"dependencyGraph":['
'{'
'"name":"plugin-a",'
'"dependencies":["plugin-b","plugin-c"]'
'},'
'{'
'"name":"plugin-b",'
'"dependencies":["plugin-c"]'
'},'
'{'
'"name":"plugin-c",'
'"dependencies":[]'
'}'
']'
'}'
);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),