Unverified Commit 7bdd4757 authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Create plugin symlinks for Windows and Linux (#50599)

This makes ephemeral symlinks to each plugin, for use by build systems.
This is similar to the logic implemented in the Podfile on iOS and
macOS, but managed internally to the Flutter tool.

Exploration for addressing #32719 and #32720
Related to #41146
parent d3c318ed
......@@ -9,6 +9,7 @@ import '../base/process.dart';
import '../build_info.dart';
import '../cache.dart';
import '../globals.dart' as globals;
import '../plugins.dart';
import '../project.dart';
import '../reporting/reporting.dart';
......@@ -38,6 +39,7 @@ export PROJECT_DIR=${linuxProject.project.directory.path}
linuxProject.generatedMakeConfigFile
..createSync(recursive: true)
..writeAsStringSync(buffer.toString());
createPluginSymlinks(linuxProject.project);
if (!buildInfo.isDebug) {
const String warning = '🚧 ';
......
......@@ -318,6 +318,12 @@ List<Plugin> findPlugins(FlutterProject project) {
return plugins;
}
// Key strings for the .flutter-plugins-dependencies file.
const String _kFlutterPluginsPluginListKey = 'plugins';
const String _kFlutterPluginsNameKey = 'name';
const String _kFlutterPluginsPathKey = 'path';
const String _kFlutterPluginsDependenciesKey = 'dependencies';
/// Filters [plugins] to those supported by [platformKey].
List<Map<String, dynamic>> _filterPluginsByPlatform(List<Plugin>plugins, String platformKey) {
final Iterable<Plugin> platformPlugins = plugins.where((Plugin p) {
......@@ -328,9 +334,9 @@ List<Plugin> findPlugins(FlutterProject project) {
final List<Map<String, dynamic>> list = <Map<String, dynamic>>[];
for (final Plugin plugin in platformPlugins) {
list.add(<String, dynamic>{
'name': plugin.name,
'path': globals.fsUtils.escapePath(plugin.path),
'dependencies': <String>[...plugin.dependencies.where(pluginNames.contains)],
_kFlutterPluginsNameKey: plugin.name,
_kFlutterPluginsPathKey: globals.fsUtils.escapePath(plugin.path),
_kFlutterPluginsDependenciesKey: <String>[...plugin.dependencies.where(pluginNames.contains)],
});
}
return list;
......@@ -411,7 +417,7 @@ bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
final Map<String, dynamic> result = <String, dynamic> {};
result['info'] = 'This is a generated file; do not edit or check into version control.';
result['plugins'] = pluginsMap;
result[_kFlutterPluginsPluginListKey] = pluginsMap;
/// The dependencyGraph object is kept for backwards compatibility, but
/// should be removed once migration is complete.
/// https://github.com/flutter/flutter/issues/48918
......@@ -889,6 +895,72 @@ Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plug
}
}
/// For each platform that uses them, creates symlinks within the platform
/// directory to each plugin used on that platform.
///
/// If |force| is true, the symlinks will be recreated, otherwise they will
/// be created only if missing.
///
/// This uses [project.flutterPluginsDependenciesFile], so it should only be
/// run after refreshPluginList has been run since the last plugin change.
void createPluginSymlinks(FlutterProject project, {bool force = false}) {
Map<String, dynamic> platformPlugins;
final String pluginFileContent = _readFileContent(project.flutterPluginsDependenciesFile);
if (pluginFileContent != null) {
final Map<String, dynamic> pluginInfo = json.decode(pluginFileContent) as Map<String, dynamic>;
platformPlugins = pluginInfo[_kFlutterPluginsPluginListKey] as Map<String, dynamic>;
}
platformPlugins ??= <String, dynamic>{};
if (featureFlags.isWindowsEnabled && project.windows.existsSync()) {
_createPlatformPluginSymlinks(
project.windows.pluginSymlinkDirectory,
platformPlugins[project.windows.pluginConfigKey] as List<dynamic>,
force: force,
);
}
if (featureFlags.isLinuxEnabled && project.linux.existsSync()) {
_createPlatformPluginSymlinks(
project.linux.pluginSymlinkDirectory,
platformPlugins[project.linux.pluginConfigKey] as List<dynamic>,
force: force,
);
}
}
/// Creates [symlinkDirectory] containing symlinks to each plugin listed in [platformPlugins].
///
/// If [force] is true, the directory will be created only if missing.
void _createPlatformPluginSymlinks(Directory symlinkDirectory, List<dynamic> platformPlugins, {bool force = false}) {
if (force && symlinkDirectory.existsSync()) {
// Start fresh to avoid stale links.
symlinkDirectory.deleteSync(recursive: true);
}
symlinkDirectory.createSync(recursive: true);
if (platformPlugins == null) {
return;
}
for (final Map<String, dynamic> pluginInfo in platformPlugins.cast<Map<String, dynamic>>()) {
final String name = pluginInfo[_kFlutterPluginsNameKey] as String;
final String path = pluginInfo[_kFlutterPluginsPathKey] as String;
final Link link = symlinkDirectory.childLink(name);
if (link.existsSync()) {
continue;
}
try {
link.createSync(path);
} on FileSystemException catch (e) {
if (globals.platform.isWindows && (e.osError?.errorCode ?? 0) == 1314) {
throwToolExit(
'Building with plugins requires symlink support. '
'Please enable Developer Mode in your system settings.\n\n$e'
);
}
rethrow;
}
}
}
/// Rewrites the `.flutter-plugins` file of [project] based on the plugin
/// dependencies declared in `pubspec.yaml`.
///
......@@ -905,6 +977,7 @@ void refreshPluginsList(FlutterProject project, {bool checkProjects = false}) {
final bool changed = _writeFlutterPluginsList(project, plugins);
if (changed || legacyChanged) {
createPluginSymlinks(project, force: true);
if (!checkProjects || project.ios.existsSync()) {
cocoaPods.invalidatePodInstallOutput(project.ios);
}
......
......@@ -963,6 +963,9 @@ class WindowsProject extends FlutterProjectPlatform {
/// Ideally this will be replaced in the future with inspection of the project.
File get nameFile => ephemeralDirectory.childFile('exe_filename');
/// The directory to write plugin symlinks.
Directory get pluginSymlinkDirectory => ephemeralDirectory.childDirectory('.plugin_symlinks');
Future<void> ensureReadyForPlatformSpecificTooling() async {}
}
......@@ -997,6 +1000,9 @@ class LinuxProject extends FlutterProjectPlatform {
/// the build.
File get generatedMakeConfigFile => ephemeralDirectory.childFile('generated_config.mk');
/// The directory to write plugin symlinks.
Directory get pluginSymlinkDirectory => ephemeralDirectory.childDirectory('.plugin_symlinks');
Future<void> ensureReadyForPlatformSpecificTooling() async {}
}
......
......@@ -9,6 +9,7 @@ import '../base/process.dart';
import '../build_info.dart';
import '../cache.dart';
import '../globals.dart' as globals;
import '../plugins.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import 'msbuild_utils.dart';
......@@ -39,6 +40,7 @@ Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {S
environment['LOCAL_ENGINE'] = globals.fs.path.basename(engineOutPath);
}
writePropertySheet(windowsProject.generatedPropertySheetFile, environment);
createPluginSymlinks(windowsProject.project);
final String vcvarsScript = visualStudio.vcvarsPath;
if (vcvarsScript == null) {
......
......@@ -73,10 +73,12 @@ void main() {
windowsProject = MockWindowsProject();
when(flutterProject.windows).thenReturn(windowsProject);
when(windowsProject.pluginConfigKey).thenReturn('windows');
when(windowsProject.pluginSymlinkDirectory).thenReturn(flutterProject.directory.childDirectory('windows').childDirectory('symlinks'));
when(windowsProject.existsSync()).thenReturn(false);
linuxProject = MockLinuxProject();
when(flutterProject.linux).thenReturn(linuxProject);
when(linuxProject.pluginConfigKey).thenReturn('linux');
when(linuxProject.pluginSymlinkDirectory).thenReturn(flutterProject.directory.childDirectory('linux').childDirectory('symlinks'));
when(linuxProject.existsSync()).thenReturn(false);
when(mockClock.now()).thenAnswer(
......@@ -826,6 +828,138 @@ web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri.toStr
FeatureFlags: () => featureFlags,
});
});
group('createPluginSymlinks', () {
MockFeatureFlags featureFlags;
setUp(() {
featureFlags = MockFeatureFlags();
when(featureFlags.isLinuxEnabled).thenReturn(true);
when(featureFlags.isWindowsEnabled).thenReturn(true);
});
testUsingContext('Symlinks are created for Linux plugins', () {
when(linuxProject.existsSync()).thenReturn(true);
configureDummyPackageAsPlugin();
// refreshPluginsList should call createPluginSymlinks.
refreshPluginsList(flutterProject);
expect(linuxProject.pluginSymlinkDirectory.childLink('apackage').existsSync(), true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
testUsingContext('Symlinks are created for Windows plugins', () {
when(windowsProject.existsSync()).thenReturn(true);
configureDummyPackageAsPlugin();
// refreshPluginsList should call createPluginSymlinks.
refreshPluginsList(flutterProject);
expect(windowsProject.pluginSymlinkDirectory.childLink('apackage').existsSync(), true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
testUsingContext('Existing symlinks are removed when no longer in use with force', () {
when(linuxProject.existsSync()).thenReturn(true);
when(windowsProject.existsSync()).thenReturn(true);
final List<File> dummyFiles = <File>[
flutterProject.linux.pluginSymlinkDirectory.childFile('dummy'),
flutterProject.windows.pluginSymlinkDirectory.childFile('dummy'),
];
for (final File file in dummyFiles) {
file.createSync(recursive: true);
}
createPluginSymlinks(flutterProject, force: true);
for (final File file in dummyFiles) {
expect(file.existsSync(), false);
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
testUsingContext('Existing symlinks are removed automatically on refresh when no longer in use', () {
when(linuxProject.existsSync()).thenReturn(true);
when(windowsProject.existsSync()).thenReturn(true);
final List<File> dummyFiles = <File>[
flutterProject.linux.pluginSymlinkDirectory.childFile('dummy'),
flutterProject.windows.pluginSymlinkDirectory.childFile('dummy'),
];
for (final File file in dummyFiles) {
file.createSync(recursive: true);
}
// refreshPluginsList should remove existing links and recreate on changes.
configureDummyPackageAsPlugin();
refreshPluginsList(flutterProject);
for (final File file in dummyFiles) {
expect(file.existsSync(), false);
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
testUsingContext('createPluginSymlinks is a no-op without force when up to date', () {
when(linuxProject.existsSync()).thenReturn(true);
when(windowsProject.existsSync()).thenReturn(true);
final List<File> dummyFiles = <File>[
flutterProject.linux.pluginSymlinkDirectory.childFile('dummy'),
flutterProject.windows.pluginSymlinkDirectory.childFile('dummy'),
];
for (final File file in dummyFiles) {
file.createSync(recursive: true);
}
// Without force, this should do nothing to existing files.
createPluginSymlinks(flutterProject);
for (final File file in dummyFiles) {
expect(file.existsSync(), true);
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
testUsingContext('createPluginSymlinks repairs missing links', () {
when(linuxProject.existsSync()).thenReturn(true);
when(windowsProject.existsSync()).thenReturn(true);
configureDummyPackageAsPlugin();
refreshPluginsList(flutterProject);
final List<Link> links = <Link>[
linuxProject.pluginSymlinkDirectory.childLink('apackage'),
windowsProject.pluginSymlinkDirectory.childLink('apackage'),
];
for (final Link link in links) {
link.deleteSync();
}
createPluginSymlinks(flutterProject);
for (final Link link in links) {
expect(link.existsSync(), true);
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
});
});
}
......
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