Generate a Property Sheet for Windows plugins (#50740)
Generates a Property Sheet for Windows builds containing link and include path
information for any included plugins. This allows automating part of the process
of integrating plugins into the build that is currently manual.
To support this change, refactored msbuild_utils into a PropertySheet class so that
it can be used to make different property sheets.
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index cde8cdd..fcf9516 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -19,6 +19,7 @@
import 'macos/cocoapods.dart';
import 'platform_plugins.dart';
import 'project.dart';
+import 'windows/property_sheet.dart';
void _renderTemplateToFile(String template, dynamic context, String filePath) {
final String renderedTemplate =
@@ -852,12 +853,13 @@
);
}
-Future<void> _writeWindowsPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
+Future<void> _writeWindowsPluginFiles(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> windowsPlugins = _extractPlatformMaps(plugins, WindowsPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{
'plugins': windowsPlugins,
};
await _writeCppPluginRegistrant(project.windows.managedDirectory, context);
+ await _writeWindowsPluginProperties(project.windows, windowsPlugins);
}
Future<void> _writeCppPluginRegistrant(Directory destination, Map<String, dynamic> templateContext) async {
@@ -874,6 +876,20 @@
);
}
+Future<void> _writeWindowsPluginProperties(WindowsProject project, List<Map<String, dynamic>> windowsPlugins) async {
+ final List<String> pluginLibraryFilenames = windowsPlugins.map(
+ (Map<String, dynamic> plugin) => '${plugin['name']}_plugin.lib').toList();
+ // Use paths relative to the VS project directory.
+ final String projectDir = project.vcprojFile.parent.path;
+ final String symlinkDirPath = project.pluginSymlinkDirectory.path.substring(projectDir.length + 1);
+ final List<String> pluginIncludePaths = windowsPlugins.map((Map<String, dynamic> plugin) =>
+ globals.fs.path.join(symlinkDirPath, plugin['name'] as String, 'windows')).toList();
+ project.generatedPluginPropertySheetFile.writeAsStringSync(PropertySheet(
+ includePaths: pluginIncludePaths,
+ libraryDependencies: pluginLibraryFilenames,
+ ).toString());
+}
+
Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> webPlugins = _extractPlatformMaps(plugins, WebPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{
@@ -1013,7 +1029,7 @@
await _writeMacOSPluginRegistrant(project, plugins);
}
if (featureFlags.isWindowsEnabled && project.windows.existsSync()) {
- await _writeWindowsPluginRegistrant(project, plugins);
+ await _writeWindowsPluginFiles(project, plugins);
}
for (final XcodeBasedProject subproject in <XcodeBasedProject>[project.ios, project.macos]) {
if (!project.isModule && (!checkProjects || subproject.existsSync())) {
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 6570e91..4ce1cd3 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -952,6 +952,9 @@
/// the build.
File get generatedPropertySheetFile => ephemeralDirectory.childFile('Generated.props');
+ /// Contains configuration to add plugins to the build.
+ File get generatedPluginPropertySheetFile => managedDirectory.childFile('GeneratedPlugins.props');
+
// The MSBuild project file.
File get vcprojFile => _editableDirectory.childFile('Runner.vcxproj');
diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart
index 58d2f5f..ca90058 100644
--- a/packages/flutter_tools/lib/src/windows/build_windows.dart
+++ b/packages/flutter_tools/lib/src/windows/build_windows.dart
@@ -4,6 +4,7 @@
import '../artifacts.dart';
import '../base/common.dart';
+import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../build_info.dart';
@@ -12,7 +13,7 @@
import '../plugins.dart';
import '../project.dart';
import '../reporting/reporting.dart';
-import 'msbuild_utils.dart';
+import 'property_sheet.dart';
import 'visual_studio.dart';
/// Builds the Windows project using msbuild.
@@ -24,22 +25,8 @@
'to learn about adding Windows support to a project.');
}
- final Map<String, String> environment = <String, String>{
- 'FLUTTER_ROOT': Cache.flutterRoot,
- 'FLUTTER_EPHEMERAL_DIR': windowsProject.ephemeralDirectory.path,
- 'PROJECT_DIR': windowsProject.project.directory.path,
- 'TRACK_WIDGET_CREATION': (buildInfo?.trackWidgetCreation == true).toString(),
- };
- if (target != null) {
- environment['FLUTTER_TARGET'] = target;
- }
- if (globals.artifacts is LocalEngineArtifacts) {
- final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts;
- final String engineOutPath = localEngineArtifacts.engineOutPath;
- environment['FLUTTER_ENGINE'] = globals.fs.path.dirname(globals.fs.path.dirname(engineOutPath));
- environment['LOCAL_ENGINE'] = globals.fs.path.basename(engineOutPath);
- }
- writePropertySheet(windowsProject.generatedPropertySheetFile, environment);
+ // Ensure that necessary emphemeral files are generated and up to date.
+ _writeGeneratedFlutterProperties(windowsProject, buildInfo, target);
createPluginSymlinks(windowsProject.project);
final String vcvarsScript = visualStudio.vcvarsPath;
@@ -91,3 +78,26 @@
}
flutterUsage.sendTiming('build', 'vs_build', Duration(milliseconds: sw.elapsedMilliseconds));
}
+
+/// Writes the generatedPropertySheetFile with the configuration for the given build.
+void _writeGeneratedFlutterProperties(WindowsProject windowsProject, BuildInfo buildInfo, String target) {
+ final Map<String, String> environment = <String, String>{
+ 'FLUTTER_ROOT': Cache.flutterRoot,
+ 'FLUTTER_EPHEMERAL_DIR': windowsProject.ephemeralDirectory.path,
+ 'PROJECT_DIR': windowsProject.project.directory.path,
+ 'TRACK_WIDGET_CREATION': (buildInfo?.trackWidgetCreation == true).toString(),
+ };
+ if (target != null) {
+ environment['FLUTTER_TARGET'] = target;
+ }
+ if (globals.artifacts is LocalEngineArtifacts) {
+ final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts;
+ final String engineOutPath = localEngineArtifacts.engineOutPath;
+ environment['FLUTTER_ENGINE'] = globals.fs.path.dirname(globals.fs.path.dirname(engineOutPath));
+ environment['LOCAL_ENGINE'] = globals.fs.path.basename(engineOutPath);
+ }
+
+ final File propsFile = windowsProject.generatedPropertySheetFile;
+ propsFile.createSync(recursive: true);
+ propsFile.writeAsStringSync(PropertySheet(environmentVariables: environment).toString());
+}
diff --git a/packages/flutter_tools/lib/src/windows/msbuild_utils.dart b/packages/flutter_tools/lib/src/windows/msbuild_utils.dart
deleted file mode 100644
index c06f0f1..0000000
--- a/packages/flutter_tools/lib/src/windows/msbuild_utils.dart
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright 2014 The Flutter 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 'package:xml/xml.dart' as xml;
-
-import '../base/file_system.dart';
-
-/// Writes a property sheet (.props) file to expose all of the key/value
-/// pairs in [variables] as environment variables.
-void writePropertySheet(File propertySheetFile, Map<String, String> variables) {
- final xml.XmlBuilder builder = xml.XmlBuilder();
- builder.processing('xml', 'version="1.0" encoding="utf-8"');
- builder.element('Project', nest: () {
- builder.attribute('ToolsVersion', '4.0');
- builder.attribute(
- 'xmlns', 'http://schemas.microsoft.com/developer/msbuild/2003');
- builder.element('ImportGroup', nest: () {
- builder.attribute('Label', 'PropertySheets');
- });
- _addUserMacros(builder, variables);
- builder.element('PropertyGroup');
- builder.element('ItemDefinitionGroup');
- _addItemGroup(builder, variables);
- });
-
- propertySheetFile.createSync(recursive: true);
- propertySheetFile.writeAsStringSync(
- builder.build().toXmlString(pretty: true, indent: ' '));
-}
-
-/// Adds the UserMacros PropertyGroup that defines [variables] to [builder].
-void _addUserMacros(xml.XmlBuilder builder, Map<String, String> variables) {
- builder.element('PropertyGroup', nest: () {
- builder.attribute('Label', 'UserMacros');
- for (final MapEntry<String, String> variable in variables.entries) {
- builder.element(variable.key, nest: () {
- builder.text(variable.value);
- });
- }
- });
-}
-
-/// Adds the ItemGroup to expose the given [variables] as environment variables
-/// to [builder].
-void _addItemGroup(xml.XmlBuilder builder, Map<String, String> variables) {
- builder.element('ItemGroup', nest: () {
- for (final String name in variables.keys) {
- builder.element('BuildMacro', nest: () {
- builder.attribute('Include', name);
- builder.element('Value', nest: () {
- builder.text('\$($name)');
- });
- builder.element('EnvironmentVariable', nest: () {
- builder.text('true');
- });
- });
- }
- });
-}
diff --git a/packages/flutter_tools/lib/src/windows/property_sheet.dart b/packages/flutter_tools/lib/src/windows/property_sheet.dart
new file mode 100644
index 0000000..e088485
--- /dev/null
+++ b/packages/flutter_tools/lib/src/windows/property_sheet.dart
@@ -0,0 +1,121 @@
+// Copyright 2014 The Flutter 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 'package:xml/xml.dart' as xml;
+
+/// A utility class for building property sheet (.props) files for use
+/// with MSBuild/Visual Studio projects.
+class PropertySheet {
+ /// Creates a PropertySheet with the given properties.
+ const PropertySheet({
+ this.environmentVariables,
+ this.includePaths,
+ this.libraryDependencies,
+ });
+
+ /// Variables to make available both as build macros and as environment
+ /// variables for script steps.
+ final Map<String, String> environmentVariables;
+
+ /// Directories to search for headers.
+ final List<String> includePaths;
+
+ /// Libraries to link against.
+ final List<String> libraryDependencies;
+
+ @override
+ String toString() {
+ // See https://docs.microsoft.com/en-us/cpp/build/reference/vcxproj-file-structure#property-sheet-layout
+
+ final xml.XmlBuilder builder = xml.XmlBuilder();
+ builder.processing('xml', 'version="1.0" encoding="utf-8"');
+ builder.element('Project', nest: () {
+ builder.attribute('ToolsVersion', '4.0');
+ builder.attribute(
+ 'xmlns', 'http://schemas.microsoft.com/developer/msbuild/2003');
+
+ builder.element('ImportGroup', nest: () {
+ builder.attribute('Label', 'PropertySheets');
+ });
+ builder.element('PropertyGroup', nest: () {
+ builder.attribute('Label', 'UserMacros');
+
+ _addEnviromentVariableUserMacros(builder);
+ });
+ builder.element('PropertyGroup');
+ builder.element('ItemDefinitionGroup', nest: () {
+ _addIncludePaths(builder);
+ _addLibraryDependencies(builder);
+ });
+ builder.element('ItemGroup', nest: () {
+ _addEnvironmentVariableBuildMacros(builder);
+ });
+ });
+
+ return builder.build().toXmlString(pretty: true, indent: ' ');
+ }
+
+ /// Adds directories to the header search path.
+ ///
+ /// Must be called within the context of the ItemDefinitionGroup.
+ void _addIncludePaths(xml.XmlBuilder builder) {
+ if (includePaths == null || includePaths.isEmpty) {
+ return;
+ }
+ builder.element('ClCompile', nest: () {
+ builder.element('AdditionalIncludeDirectories', nest: () {
+ builder.text('${includePaths.join(';')};%(AdditionalIncludeDirectories)');
+ });
+ });
+ }
+
+ /// Adds libraries to the link step.
+ ///
+ /// Must be called within the context of the ItemDefinitionGroup.
+ void _addLibraryDependencies(xml.XmlBuilder builder) {
+ if (libraryDependencies == null || libraryDependencies.isEmpty) {
+ return;
+ }
+ builder.element('Link', nest: () {
+ builder.element('AdditionalDependencies', nest: () {
+ builder.text('${libraryDependencies.join(';')};%(AdditionalDependencies)');
+ });
+ });
+ }
+
+ /// Writes key/value pairs for any environment variables as user macros.
+ ///
+ /// Must be called within the context of the UserMacros PropertyGroup.
+ void _addEnviromentVariableUserMacros(xml.XmlBuilder builder) {
+ if (environmentVariables == null) {
+ return;
+ }
+ for (final MapEntry<String, String> variable in environmentVariables.entries) {
+ builder.element(variable.key, nest: () {
+ builder.text(variable.value);
+ });
+ }
+ }
+
+ /// Writes the BuildMacros to expose environment variable UserMacros to the
+ /// environment.
+ ///
+ /// Must be called within the context of the ItemGroup.
+ void _addEnvironmentVariableBuildMacros(xml.XmlBuilder builder) {
+ if (environmentVariables == null) {
+ return;
+ }
+ for (final String name in environmentVariables.keys) {
+ builder.element('BuildMacro', nest: () {
+ builder.attribute('Include', name);
+ builder.element('Value', nest: () {
+ builder.text('\$($name)');
+ });
+ builder.element('EnvironmentVariable', nest: () {
+ builder.text('true');
+ });
+ });
+ }
+ }
+}
diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart
index f3b669e..63f25b7 100644
--- a/packages/flutter_tools/test/general.shard/plugins_test.dart
+++ b/packages/flutter_tools/test/general.shard/plugins_test.dart
@@ -73,7 +73,11 @@
windowsProject = MockWindowsProject();
when(flutterProject.windows).thenReturn(windowsProject);
when(windowsProject.pluginConfigKey).thenReturn('windows');
- when(windowsProject.pluginSymlinkDirectory).thenReturn(flutterProject.directory.childDirectory('windows').childDirectory('symlinks'));
+ final Directory windowsManagedDirectory = flutterProject.directory.childDirectory('windows').childDirectory('flutter');
+ when(windowsProject.managedDirectory).thenReturn(windowsManagedDirectory);
+ when(windowsProject.vcprojFile).thenReturn(windowsManagedDirectory.parent.childFile('Runner.vcxproj'));
+ when(windowsProject.pluginSymlinkDirectory).thenReturn(windowsManagedDirectory.childDirectory('ephemeral').childDirectory('.plugin_symlinks'));
+ when(windowsProject.generatedPluginPropertySheetFile).thenReturn(windowsManagedDirectory.childFile('GeneratedPlugins.props'));
when(windowsProject.existsSync()).thenReturn(false);
linuxProject = MockLinuxProject();
when(flutterProject.linux).thenReturn(linuxProject);
@@ -106,9 +110,9 @@
macos:
pluginClass: FLESomePlugin
windows:
- pluginClass: FLESomePlugin
+ pluginClass: SomePlugin
linux:
- pluginClass: FLESomePlugin
+ pluginClass: SomePlugin
web:
pluginClass: SomePlugin
fileName: lib/SomeFile.dart
@@ -827,6 +831,46 @@
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
+
+ testUsingContext('Injecting creates generated Windows registrant', () async {
+ when(windowsProject.existsSync()).thenReturn(true);
+ when(featureFlags.isWindowsEnabled).thenReturn(true);
+ when(flutterProject.isModule).thenReturn(false);
+ configureDummyPackageAsPlugin();
+
+ await injectPlugins(flutterProject, checkProjects: true);
+
+ final File registrantHeader = windowsProject.managedDirectory.childFile('generated_plugin_registrant.h');
+ final File registrantImpl = windowsProject.managedDirectory.childFile('generated_plugin_registrant.cc');
+
+ expect(registrantHeader.existsSync(), isTrue);
+ expect(registrantImpl.existsSync(), isTrue);
+ expect(registrantImpl.readAsStringSync(), contains('SomePluginRegisterWithRegistrar'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => fs,
+ ProcessManager: () => FakeProcessManager.any(),
+ FeatureFlags: () => featureFlags,
+ });
+
+ testUsingContext('Injecting creates generated Windows plugin properties', () async {
+ when(windowsProject.existsSync()).thenReturn(true);
+ when(featureFlags.isWindowsEnabled).thenReturn(true);
+ when(flutterProject.isModule).thenReturn(false);
+ configureDummyPackageAsPlugin();
+
+ await injectPlugins(flutterProject, checkProjects: true);
+
+ final File properties = windowsProject.generatedPluginPropertySheetFile;
+ final String includePath = fs.path.join('flutter', 'ephemeral', '.plugin_symlinks', 'apackage', 'windows');
+
+ expect(properties.existsSync(), isTrue);
+ expect(properties.readAsStringSync(), contains('apackage_plugin.lib'));
+ expect(properties.readAsStringSync(), contains('>$includePath;'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => fs,
+ ProcessManager: () => FakeProcessManager.any(),
+ FeatureFlags: () => featureFlags,
+ });
});
group('createPluginSymlinks', () {
diff --git a/packages/flutter_tools/test/general.shard/windows/property_sheet_test.dart b/packages/flutter_tools/test/general.shard/windows/property_sheet_test.dart
new file mode 100644
index 0000000..8b9fec2
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/windows/property_sheet_test.dart
@@ -0,0 +1,49 @@
+// Copyright 2014 The Flutter 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 'package:flutter_tools/src/windows/property_sheet.dart';
+
+import '../../src/common.dart';
+
+void main() {
+ group('Property Sheet', () {
+ test('Base file matches expected format', () async {
+ const String baseFile = '''
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ImportGroup Label="PropertySheets"/>
+ <PropertyGroup Label="UserMacros"/>
+ <PropertyGroup/>
+ <ItemDefinitionGroup/>
+ <ItemGroup/>
+</Project>''';
+ const PropertySheet sheet = PropertySheet();
+ expect(sheet.toString(), baseFile);
+ });
+
+ test('Environment variable generate the correct elements', () async {
+ const Map<String, String> environment = <String, String>{'FOO': 'Bar'};
+ const PropertySheet sheet = PropertySheet(environmentVariables: environment);
+ final String propsContent = sheet.toString();
+ expect(propsContent, contains('<FOO>Bar</FOO>'));
+ expect(propsContent, contains('''
+ <BuildMacro Include="FOO">
+ <Value>\$(FOO)</Value>
+ <EnvironmentVariable>true</EnvironmentVariable>
+ </BuildMacro>'''));
+ });
+
+ test('Include paths generate the correct elements', () async {
+ const PropertySheet sheet = PropertySheet(includePaths: <String>['foo/bar', 'baz']);
+ final String propsContent = sheet.toString();
+ expect(propsContent, contains('<AdditionalIncludeDirectories>foo/bar;baz;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>'));
+ });
+
+ test('Library dependencies generate the correct elements', () async {
+ const PropertySheet sheet = PropertySheet(libraryDependencies: <String>['foo.lib', 'bar.lib']);
+ final String propsContent = sheet.toString();
+ expect(propsContent, contains('<AdditionalDependencies>foo.lib;bar.lib;%(AdditionalDependencies)</AdditionalDependencies>'));
+ });
+ });
+}