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'; ...@@ -9,6 +9,7 @@ import '../base/process.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../plugins.dart';
import '../project.dart'; import '../project.dart';
import '../reporting/reporting.dart'; import '../reporting/reporting.dart';
...@@ -38,6 +39,7 @@ export PROJECT_DIR=${linuxProject.project.directory.path} ...@@ -38,6 +39,7 @@ export PROJECT_DIR=${linuxProject.project.directory.path}
linuxProject.generatedMakeConfigFile linuxProject.generatedMakeConfigFile
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync(buffer.toString()); ..writeAsStringSync(buffer.toString());
createPluginSymlinks(linuxProject.project);
if (!buildInfo.isDebug) { if (!buildInfo.isDebug) {
const String warning = '🚧 '; const String warning = '🚧 ';
......
...@@ -318,6 +318,12 @@ List<Plugin> findPlugins(FlutterProject project) { ...@@ -318,6 +318,12 @@ List<Plugin> findPlugins(FlutterProject project) {
return plugins; 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]. /// Filters [plugins] to those supported by [platformKey].
List<Map<String, dynamic>> _filterPluginsByPlatform(List<Plugin>plugins, String platformKey) { List<Map<String, dynamic>> _filterPluginsByPlatform(List<Plugin>plugins, String platformKey) {
final Iterable<Plugin> platformPlugins = plugins.where((Plugin p) { final Iterable<Plugin> platformPlugins = plugins.where((Plugin p) {
...@@ -328,9 +334,9 @@ List<Plugin> findPlugins(FlutterProject project) { ...@@ -328,9 +334,9 @@ List<Plugin> findPlugins(FlutterProject project) {
final List<Map<String, dynamic>> list = <Map<String, dynamic>>[]; final List<Map<String, dynamic>> list = <Map<String, dynamic>>[];
for (final Plugin plugin in platformPlugins) { for (final Plugin plugin in platformPlugins) {
list.add(<String, dynamic>{ list.add(<String, dynamic>{
'name': plugin.name, _kFlutterPluginsNameKey: plugin.name,
'path': globals.fsUtils.escapePath(plugin.path), _kFlutterPluginsPathKey: globals.fsUtils.escapePath(plugin.path),
'dependencies': <String>[...plugin.dependencies.where(pluginNames.contains)], _kFlutterPluginsDependenciesKey: <String>[...plugin.dependencies.where(pluginNames.contains)],
}); });
} }
return list; return list;
...@@ -411,7 +417,7 @@ bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) { ...@@ -411,7 +417,7 @@ bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
final Map<String, dynamic> result = <String, dynamic> {}; final Map<String, dynamic> result = <String, dynamic> {};
result['info'] = 'This is a generated file; do not edit or check into version control.'; 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 /// The dependencyGraph object is kept for backwards compatibility, but
/// should be removed once migration is complete. /// should be removed once migration is complete.
/// https://github.com/flutter/flutter/issues/48918 /// https://github.com/flutter/flutter/issues/48918
...@@ -889,6 +895,72 @@ Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plug ...@@ -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 /// Rewrites the `.flutter-plugins` file of [project] based on the plugin
/// dependencies declared in `pubspec.yaml`. /// dependencies declared in `pubspec.yaml`.
/// ///
...@@ -905,6 +977,7 @@ void refreshPluginsList(FlutterProject project, {bool checkProjects = false}) { ...@@ -905,6 +977,7 @@ void refreshPluginsList(FlutterProject project, {bool checkProjects = false}) {
final bool changed = _writeFlutterPluginsList(project, plugins); final bool changed = _writeFlutterPluginsList(project, plugins);
if (changed || legacyChanged) { if (changed || legacyChanged) {
createPluginSymlinks(project, force: true);
if (!checkProjects || project.ios.existsSync()) { if (!checkProjects || project.ios.existsSync()) {
cocoaPods.invalidatePodInstallOutput(project.ios); cocoaPods.invalidatePodInstallOutput(project.ios);
} }
......
...@@ -963,6 +963,9 @@ class WindowsProject extends FlutterProjectPlatform { ...@@ -963,6 +963,9 @@ class WindowsProject extends FlutterProjectPlatform {
/// Ideally this will be replaced in the future with inspection of the project. /// Ideally this will be replaced in the future with inspection of the project.
File get nameFile => ephemeralDirectory.childFile('exe_filename'); File get nameFile => ephemeralDirectory.childFile('exe_filename');
/// The directory to write plugin symlinks.
Directory get pluginSymlinkDirectory => ephemeralDirectory.childDirectory('.plugin_symlinks');
Future<void> ensureReadyForPlatformSpecificTooling() async {} Future<void> ensureReadyForPlatformSpecificTooling() async {}
} }
...@@ -997,6 +1000,9 @@ class LinuxProject extends FlutterProjectPlatform { ...@@ -997,6 +1000,9 @@ class LinuxProject extends FlutterProjectPlatform {
/// the build. /// the build.
File get generatedMakeConfigFile => ephemeralDirectory.childFile('generated_config.mk'); 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 {} Future<void> ensureReadyForPlatformSpecificTooling() async {}
} }
......
...@@ -9,6 +9,7 @@ import '../base/process.dart'; ...@@ -9,6 +9,7 @@ import '../base/process.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../plugins.dart';
import '../project.dart'; import '../project.dart';
import '../reporting/reporting.dart'; import '../reporting/reporting.dart';
import 'msbuild_utils.dart'; import 'msbuild_utils.dart';
...@@ -39,6 +40,7 @@ Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {S ...@@ -39,6 +40,7 @@ Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {S
environment['LOCAL_ENGINE'] = globals.fs.path.basename(engineOutPath); environment['LOCAL_ENGINE'] = globals.fs.path.basename(engineOutPath);
} }
writePropertySheet(windowsProject.generatedPropertySheetFile, environment); writePropertySheet(windowsProject.generatedPropertySheetFile, environment);
createPluginSymlinks(windowsProject.project);
final String vcvarsScript = visualStudio.vcvarsPath; final String vcvarsScript = visualStudio.vcvarsPath;
if (vcvarsScript == null) { if (vcvarsScript == null) {
......
...@@ -73,10 +73,12 @@ void main() { ...@@ -73,10 +73,12 @@ void main() {
windowsProject = MockWindowsProject(); windowsProject = MockWindowsProject();
when(flutterProject.windows).thenReturn(windowsProject); when(flutterProject.windows).thenReturn(windowsProject);
when(windowsProject.pluginConfigKey).thenReturn('windows'); when(windowsProject.pluginConfigKey).thenReturn('windows');
when(windowsProject.pluginSymlinkDirectory).thenReturn(flutterProject.directory.childDirectory('windows').childDirectory('symlinks'));
when(windowsProject.existsSync()).thenReturn(false); when(windowsProject.existsSync()).thenReturn(false);
linuxProject = MockLinuxProject(); linuxProject = MockLinuxProject();
when(flutterProject.linux).thenReturn(linuxProject); when(flutterProject.linux).thenReturn(linuxProject);
when(linuxProject.pluginConfigKey).thenReturn('linux'); when(linuxProject.pluginConfigKey).thenReturn('linux');
when(linuxProject.pluginSymlinkDirectory).thenReturn(flutterProject.directory.childDirectory('linux').childDirectory('symlinks'));
when(linuxProject.existsSync()).thenReturn(false); when(linuxProject.existsSync()).thenReturn(false);
when(mockClock.now()).thenAnswer( when(mockClock.now()).thenAnswer(
...@@ -826,6 +828,138 @@ web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri.toStr ...@@ -826,6 +828,138 @@ web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri.toStr
FeatureFlags: () => featureFlags, 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