Unverified Commit f4177a6d authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

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.
parent e2554a92
......@@ -19,6 +19,7 @@ import 'globals.dart' as globals;
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> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> pl
);
}
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> _writeCppPluginRegistrant(Directory destination, Map<String, dynami
);
}
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 @@ Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false})
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())) {
......
......@@ -952,6 +952,9 @@ class WindowsProject extends FlutterProjectPlatform {
/// 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');
......
......@@ -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 '../globals.dart' as globals;
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 @@ Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {S
'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 @@ Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {S
}
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());
}
// 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');
});
});
}
});
}
// 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');
});
});
}
}
}
......@@ -73,7 +73,11 @@ void main() {
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 @@ void main() {
macos:
pluginClass: FLESomePlugin
windows:
pluginClass: FLESomePlugin
pluginClass: SomePlugin
linux:
pluginClass: FLESomePlugin
pluginClass: SomePlugin
web:
pluginClass: SomePlugin
fileName: lib/SomeFile.dart
......@@ -827,6 +831,46 @@ web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri.toStr
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', () {
......
// 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>'));
});
});
}
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