Unverified Commit 22c80777 authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Automatically add plugin projects to Windows .sln (#51246)

Adds utility code for managing list of plugin projects within a solution file, updating them as the plugins change.

This is a prototype of an approach to solution-level portion of Windows plugin tooling; it may not be what the final plugin handling on Windows uses, but it makes things much better in the short term, and gives us a baseline to evaluate other possible solution management systems against.

Part of #32719
parent c71978f6
......@@ -20,6 +20,7 @@ import 'macos/cocoapods.dart';
import 'platform_plugins.dart';
import 'project.dart';
import 'windows/property_sheet.dart';
import 'windows/visual_studio_solution_utils.dart';
void _renderTemplateToFile(String template, dynamic context, String filePath) {
final String renderedTemplate =
......@@ -1038,6 +1039,7 @@ Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false})
}
if (featureFlags.isWindowsEnabled && project.windows.existsSync()) {
await _writeWindowsPluginFiles(project, plugins);
await VisualStudioSolutionUtils(project: project.windows, fileSystem: globals.fs).updatePlugins(plugins);
}
for (final XcodeBasedProject subproject in <XcodeBasedProject>[project.ios, project.macos]) {
if (!project.isModule && (!checkProjects || subproject.existsSync())) {
......
// 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:meta/meta.dart';
import 'package:xml/xml.dart' as xml;
import '../base/file_system.dart';
/// A utility class for interacting with Visual Studio project files (e.g.,
/// .vcxproj).
class VisualStudioProject {
/// Creates a project object from the project file at [file].
VisualStudioProject(this.file, {
@required FileSystem fileSystem,
}): _fileSystem = fileSystem {
try {
content = xml.parse(file.readAsStringSync());
} on xml.XmlParserException {
// Silently continue; formatUnderstood will return false.
}
}
final FileSystem _fileSystem;
/// The file corresponding to this object.
final File file;
/// The content of the project file.
xml.XmlDocument content;
/// Whether or not the the project file was correctly parsed.
///
/// If false, this could indicate that the project file is damaged, or that
/// it's an unsupported project type.
bool get formatUnderstood => content != null;
String _guid;
/// Returns the ProjectGuid for the project, or null if it's not present.
String get guid {
return _guid ??= _findGuid();
}
String _findGuid() {
if (!formatUnderstood) {
return null;
}
try {
final String guidValue = content.findAllElements('ProjectGuid').single.text.trim();
// Remove the enclosing {} from the value.
return guidValue.substring(1, guidValue.length - 1);
} on StateError {
// If there is not exactly one ProjectGuid, return null.
return null;
}
}
String _name;
/// Returns the ProjectName for the project.
///
/// If not explicitly set in the project, uses the basename of the project
/// file.
String get name {
return _name ??= _findName();
}
String _findName() {
if (!formatUnderstood) {
return null;
}
try {
return content.findAllElements('ProjectName').first.text.trim();
} on StateError {
// If there is no name, fall back to filename.
return _fileSystem.path.basenameWithoutExtension(file.path);
}
}
}
......@@ -76,6 +76,7 @@ void main() {
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.solutionFile).thenReturn(windowsManagedDirectory.parent.childFile('Runner.sln'));
when(windowsProject.pluginSymlinkDirectory).thenReturn(windowsManagedDirectory.childDirectory('ephemeral').childDirectory('.plugin_symlinks'));
when(windowsProject.generatedPluginPropertySheetFile).thenReturn(windowsManagedDirectory.childFile('GeneratedPlugins.props'));
when(windowsProject.existsSync()).thenReturn(false);
......@@ -309,6 +310,44 @@ dependencies:
project.podManifestLock.createSync(recursive: true);
}
// Creates a Windows solution file sufficient to allow plugin injection
// to run without failing.
void createDummyWindowsSolutionFile() {
windowsProject.solutionFile.createSync(recursive: true);
// This isn't a valid solution file, but it's just enough to work with the
// plugin injection.
windowsProject.solutionFile.writeAsStringSync('''
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Runner", "Runner.vcxproj", "{3842E94C-E348-463A-ADBE-625A2B69B628}"
ProjectSection(ProjectDependencies) = postProject
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F} = {6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}
EndProjectSection
EndProject
Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution
EndGlobalSection
EndGlobal''');
}
// Creates a Windows project file for dummyPackageDirectory sufficient to
// allow plugin injection to run without failing.
void createDummyPluginWindowsProjectFile() {
final File projectFile = dummyPackageDirectory
.parent
.childDirectory('windows')
.childFile('plugin.vcxproj');
projectFile.createSync(recursive: true);
// This isn't a valid project file, but it's just enough to work with the
// plugin injection.
projectFile.writeAsStringSync('''
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>{5919689F-A5D5-462C-AF50-D405CCEF89B8}</ProjectGuid>'}
<ProjectName>apackage</ProjectName>
</PropertyGroup>
</Project>''');
}
group('refreshPlugins', () {
testUsingContext('Refreshing the plugin list is a no-op when the plugins list stays empty', () {
refreshPluginsList(flutterProject);
......@@ -837,6 +876,8 @@ web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri.toStr
when(featureFlags.isWindowsEnabled).thenReturn(true);
when(flutterProject.isModule).thenReturn(false);
configureDummyPackageAsPlugin();
createDummyWindowsSolutionFile();
createDummyPluginWindowsProjectFile();
await injectPlugins(flutterProject, checkProjects: true);
......@@ -857,6 +898,8 @@ web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri.toStr
when(featureFlags.isWindowsEnabled).thenReturn(true);
when(flutterProject.isModule).thenReturn(false);
configureDummyPackageAsPlugin();
createDummyWindowsSolutionFile();
createDummyPluginWindowsProjectFile();
await injectPlugins(flutterProject, checkProjects: true);
......@@ -871,6 +914,23 @@ web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri.toStr
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
testUsingContext('Injecting updates Windows solution file', () async {
when(windowsProject.existsSync()).thenReturn(true);
when(featureFlags.isWindowsEnabled).thenReturn(true);
when(flutterProject.isModule).thenReturn(false);
configureDummyPackageAsPlugin();
createDummyWindowsSolutionFile();
createDummyPluginWindowsProjectFile();
await injectPlugins(flutterProject, checkProjects: true);
expect(windowsProject.solutionFile.readAsStringSync(), contains(r'apackage\windows\plugin.vcxproj'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
});
group('createPluginSymlinks', () {
......
......@@ -225,6 +225,20 @@ void main() {
testUsingContext('injects plugins for Windows', () async {
final FlutterProject project = await someProject();
project.windows.managedDirectory.createSync(recursive: true);
project.windows.solutionFile.createSync(recursive: true);
// Just enough solution file to allow injection to pass.
// TODO(stuartmorgan): Consider allowing injecting a mock solution util
// class into the test environment instead.
project.windows.solutionFile.writeAsStringSync('''
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Runner", "Runner.vcxproj", "{3842E94C-E348-463A-ADBE-625A2B69B628}"
ProjectSection(ProjectDependencies) = postProject
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F} = {6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}
EndProjectSection
EndProject
Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution
EndGlobalSection
EndGlobal''');
await project.ensureReadyForPlatformSpecificTooling();
expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.h'));
expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.cc'));
......
// 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:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/windows/visual_studio_project.dart';
import '../../src/common.dart';
void main() {
group('Visual Studio Project', () {
String generateProjectContents({String guid, String name}) {
// A bare-bones project.
return '''
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
${guid == null ? '' : '<ProjectGuid>{$guid}</ProjectGuid>'}
${name == null ? '' : '<ProjectName>$name</ProjectName>'}
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="\$(VCTargetsPath)\\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<Import Project="\$(VCTargetsPath)\\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<PropertyGroup />
<ItemDefinitionGroup />
<ItemGroup>
</ItemGroup>
<Import Project="\$(VCTargetsPath)\\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>''';
}
test('Property extraction works on a simple vcxproj', () async {
final FileSystem fileSystem = MemoryFileSystem();
const String guid = '017C4BAC-FEBA-406D-8A2C-3099FFE9D811';
const String name = 'Test';
final File projectFile = fileSystem.file('aproject.vcxproj');
projectFile.writeAsStringSync(generateProjectContents(guid: guid, name: name));
final VisualStudioProject project = VisualStudioProject(projectFile, fileSystem: fileSystem);
expect(project.formatUnderstood, true);
expect(project.guid, guid);
expect(project.name, name);
});
test('Missing GUID returns null', () async {
final FileSystem fileSystem = MemoryFileSystem();
final File projectFile = fileSystem.file('aproject.vcxproj');
projectFile.writeAsStringSync(generateProjectContents());
final VisualStudioProject project = VisualStudioProject(projectFile, fileSystem: fileSystem);
expect(project.formatUnderstood, true);
expect(project.guid, null);
});
test('Missing project name uses filename', () async {
final FileSystem fileSystem = MemoryFileSystem();
final File projectFile = fileSystem.file('aproject.vcxproj');
projectFile.writeAsStringSync(generateProjectContents());
final VisualStudioProject project = VisualStudioProject(projectFile, fileSystem: fileSystem);
expect(project.formatUnderstood, true);
expect(project.name, 'aproject');
});
test('Unknown file contents creates an object, and return false for formatUnderstood', () async {
final FileSystem fileSystem = MemoryFileSystem();
final File projectFile = fileSystem.file('aproject.vcxproj');
projectFile.writeAsStringSync('This is not XML!');
final VisualStudioProject project = VisualStudioProject(projectFile, fileSystem: fileSystem);
expect(project.formatUnderstood, false);
});
test('Missing project file throws on creation', () async {
final FileSystem fileSystem = MemoryFileSystem();
final File projectFile = fileSystem.file('aproject.vcxproj');
expect(() => VisualStudioProject(projectFile, fileSystem: fileSystem), throwsFileSystemException());
});
});
}
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