blob: 66560ef873f64f76a9fb74b797de2d6538281c70 [file] [log] [blame]
// 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 '../base/common.dart';
import '../base/file_system.dart';
import '../convert.dart';
import '../plugins.dart';
import '../project.dart';
import 'visual_studio_project.dart';
// Constants corresponding to specific reference types in a solution file.
// These values are defined by the .sln format.
const String _kSolutionTypeGuidFolder = '2150E333-8FDC-42A3-9474-1A3956D46DE8';
const String _kSolutionTypeGuidVcxproj = '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942';
// The GUID for the folder above, managed by this class. This is an arbitrary
// value that was randomly generated, but it should not be changed since that
// would cause issues for existing Flutter projects.
const String _kFlutterPluginSolutionFolderGuid = '5C2E738A-1DD3-445A-AAC8-EEB9648DD07C';
// The FlutterBuild project GUID. This is an arbitrary
// value that was randomly generated, but it should not be changed since that
// would cause issues for existing Flutter projects.
const String _kFlutterBuildProjectGuid = '6419BF13-6ECD-4CD2-9E85-E566A1F03F8F';
/// Extracts and stores the plugin name and vcxproj GUID for [plugin].
class _PluginProjectInfo {
_PluginProjectInfo(Plugin plugin, {
@required FileSystem fileSystem,
}) {
name = plugin.name;
final File projectFile = fileSystem.directory(plugin.path).childDirectory('windows').childFile('plugin.vcxproj');
try {
guid = VisualStudioProject(projectFile, fileSystem: fileSystem).guid;
} on FileSystemException {
throwToolExit('Unable to find a plugin.vcxproj for plugin "$name"');
}
if (guid == null) {
throwToolExit('Unable to find a plugin.vcxproj ID for plugin "$name"');
}
}
// The name of the plugin, which is also the name of the symlink folder.
String name;
// The GUID of the plugin's project.
String guid;
}
// TODO(stuartmorgan): Consider replacing this class with a real parser. See
// https://github.com/flutter/flutter/issues/51430.
class VisualStudioSolutionUtils {
const VisualStudioSolutionUtils({
@required WindowsProject project,
@required FileSystem fileSystem,
}) : _project = project,
_fileSystem = fileSystem;
final WindowsProject _project;
final FileSystem _fileSystem;
/// Updates the solution file for [project] to have the project references and
/// dependencies to include [plugins], removing any previous plugins from the
/// solution.
Future<void> updatePlugins(List<Plugin> plugins) async {
final String solutionContent = await _project.solutionFile.readAsString();
// Map of GUID to name for the current plugin list.
final Map<String, String> currentPluginInfo = _getWindowsPluginNamesByGuid(plugins);
// Find any plugins referenced in the project that are no longer used, and
// any that are new.
//
// While the simplest approach to updating the solution would be to remove all
// entries associated with plugins, and then add all the current plugins in
// one block, Visual Studio has its own (unknown, likely data-structure-hash
// based) order that it will use each time it writes out the file due to any
// solution-level changes made in the UI. To avoid thrashing, and resulting
// confusion (e.g., in review diffs), this update attempts to instead preserve
// the ordering that is already there, so that once Visual Studio has
// reordered the plugins, that order will be stable.
final Set<String> existingPlugins = _findPreviousPluginGuids(solutionContent);
final Set<String> currentPlugins = currentPluginInfo.keys.toSet();
final Set<String> removedPlugins = existingPlugins.difference(currentPlugins);
final Set<String> addedPlugins = currentPlugins.difference(existingPlugins);
final RegExp projectStartPattern = RegExp(r'^Project\("{' + _kSolutionTypeGuidVcxproj + r'}"\)\s*=\s*".*",\s*"(.*)",\s*"{([A-Fa-f0-9\-]*)}"\s*$');
final RegExp pluginsFolderProjectStartPattern = RegExp(r'^Project\("{' + _kSolutionTypeGuidFolder + r'}"\)\s*=.*"{' + _kFlutterPluginSolutionFolderGuid + r'}"\s*$');
final RegExp projectEndPattern = RegExp(r'^EndProject\s*$');
final RegExp globalStartPattern = RegExp(r'^Global\s*$');
final RegExp globalEndPattern = RegExp(r'^EndGlobal\s*$');
final RegExp projectDependenciesStartPattern = RegExp(r'^\s*ProjectSection\(ProjectDependencies\)\s*=\s*postProject\s*$');
final RegExp globalSectionProjectConfigurationStartPattern = RegExp(r'^\s*GlobalSection\(ProjectConfigurationPlatforms\)\s*=\s*postSolution\s*$');
final RegExp globalSectionNestedProjectsStartPattern = RegExp(r'^\s*GlobalSection\(NestedProjects\)\s*=\s*preSolution\s*$');
final StringBuffer newSolutionContent = StringBuffer();
// readAsString drops the BOM; re-add it.
newSolutionContent.writeCharCode(unicodeBomCharacterRune);
final Iterator<String> lineIterator = solutionContent.split('\n').iterator;
bool foundFlutterPluginsFolder = false;
bool foundNestedProjectsSection = false;
bool foundRunnerProject = false;
while (lineIterator.moveNext()) {
final Match projectStartMatch = projectStartPattern.firstMatch(lineIterator.current);
if (projectStartMatch != null) {
final String guid = projectStartMatch.group(2);
if (currentPlugins.contains(guid)) {
// Write an up-to-date version at this location (in case, e.g., the name
// has changed).
_writePluginProjectEntry(guid, currentPluginInfo[guid], newSolutionContent);
// Drop the old copy.
_skipUntil(lineIterator, projectEndPattern);
continue;
} else if (removedPlugins.contains(guid)) {
// Drop the stale plugin project.
_skipUntil(lineIterator, projectEndPattern);
continue;
} else if (projectStartMatch.group(1) == _project.vcprojFile.basename) {
foundRunnerProject = true;
// Update the Runner project's dependencies on the plugins.
// Skip to the dependencies section, or if there isn't one the end of
// the project.
while (!projectDependenciesStartPattern.hasMatch(lineIterator.current) &&
!projectEndPattern.hasMatch(lineIterator.current)) {
newSolutionContent.writeln(lineIterator.current);
lineIterator.moveNext();
}
// Add/update the dependencies section.
if (projectDependenciesStartPattern.hasMatch(lineIterator.current)) {
newSolutionContent.writeln(lineIterator.current);
_processSectionPluginReferences(removedPlugins, addedPlugins, lineIterator, _writeProjectDependency, newSolutionContent);
} else {
_writeDependenciesSection(currentPlugins, newSolutionContent);
}
}
}
if (pluginsFolderProjectStartPattern.hasMatch(lineIterator.current)) {
foundFlutterPluginsFolder = true;
}
if (globalStartPattern.hasMatch(lineIterator.current)) {
// The Global section is the end of the project list. Add any new plugins
// here, since the location VS will use is unknown. They will likely be
// reordered the next time VS writes the file.
for (final String guid in addedPlugins) {
_writePluginProjectEntry(guid, currentPluginInfo[guid], newSolutionContent);
}
// Also add the plugins folder if there wasn't already one.
if (!foundFlutterPluginsFolder) {
_writePluginFolderProjectEntry(newSolutionContent);
}
}
// Update the ProjectConfiguration section once it is reached.
if (globalSectionProjectConfigurationStartPattern.hasMatch(lineIterator.current)) {
newSolutionContent.writeln(lineIterator.current);
_processSectionPluginReferences(removedPlugins, addedPlugins, lineIterator, _writePluginConfigurationEntries, newSolutionContent);
}
// Update the NestedProjects section once it is reached.
if (globalSectionNestedProjectsStartPattern.hasMatch(lineIterator.current)) {
newSolutionContent.writeln(lineIterator.current);
_processSectionPluginReferences(removedPlugins, addedPlugins, lineIterator, _writePluginNestingEntry, newSolutionContent);
foundNestedProjectsSection = true;
}
// If there wasn't a NestedProjects global section, add one at the end.
if (!foundNestedProjectsSection && globalEndPattern.hasMatch(lineIterator.current)) {
newSolutionContent.writeln('\tGlobalSection(NestedProjects) = preSolution\r');
for (final String guid in currentPlugins) {
_writePluginNestingEntry(guid, newSolutionContent);
}
newSolutionContent.writeln('\tEndGlobalSection\r');
}
// Re-output anything that hasn't been explicitly skipped above.
newSolutionContent.writeln(lineIterator.current);
}
if (!foundRunnerProject) {
throwToolExit(
'Could not add plugins to Windows project:\n'
'Unable to find a "${_project.vcprojFile.basename}" project in ${_project.solutionFile.path}');
}
await _project.solutionFile.writeAsString(newSolutionContent.toString().trimRight());
}
/// Advances [iterator] it reaches an element that matches [pattern].
///
/// Note that the current element at the time of calling is *not* checked.
void _skipUntil(Iterator<String> iterator, RegExp pattern) {
while (iterator.moveNext()) {
if (pattern.hasMatch(iterator.current)) {
return;
}
}
}
/// Writes the main project entry for the plugin with the given [guid] and
/// [name].
void _writePluginProjectEntry(String guid, String name, StringBuffer output) {
output.write('''
Project("{$_kSolutionTypeGuidVcxproj}") = "$name", "Flutter\\ephemeral\\.plugin_symlinks\\$name\\windows\\plugin.vcxproj", "{$guid}"\r
\tProjectSection(ProjectDependencies) = postProject\r
\t\t{$_kFlutterBuildProjectGuid} = {$_kFlutterBuildProjectGuid}\r
\tEndProjectSection\r
EndProject\r
''');
}
/// Writes the main project entry for the Flutter Plugins solution folder.
void _writePluginFolderProjectEntry(StringBuffer output) {
const String folderName = 'Flutter Plugins';
output.write('''
Project("{$_kSolutionTypeGuidFolder}") = "$folderName", "$folderName", "{$_kFlutterPluginSolutionFolderGuid}"\r
EndProject\r
''');
}
/// Writes a project dependencies section, depending on all the GUIDs in
/// [dependencies].
void _writeDependenciesSection(Iterable<String> dependencies, StringBuffer output) {
output.writeln('ProjectSection(ProjectDependencies) = postProject\r');
for (final String guid in dependencies) {
_writeProjectDependency(guid, output);
}
output.writeln('EndProjectSection\r');
}
/// Returns the GUIDs of all the Flutter plugin projects in the given solution.
Set<String> _findPreviousPluginGuids(String solutionContent) {
// Find the plugin folder's known GUID in ProjectDependencies lines.
// Each line in that section has the form:
// {project GUID} = {solution folder GUID}
final RegExp pluginFolderChildrenPattern = RegExp(
r'^\s*{([A-Fa-f0-9\-]*)}\s*=\s*{' + _kFlutterPluginSolutionFolderGuid + r'}\s*$',
multiLine: true,
);
return pluginFolderChildrenPattern
.allMatches(solutionContent)
.map((Match match) => match.group(1)).toSet();
}
/// Returns a mapping of plugin project GUID to name for all the Windows plugins
/// in [plugins].
Map<String, String> _getWindowsPluginNamesByGuid(List<Plugin> plugins) {
final Map<String, String> currentPluginInfo = <String, String>{};
for (final Plugin plugin in plugins) {
if (plugin.platforms.containsKey(_project.pluginConfigKey)) {
final _PluginProjectInfo info = _PluginProjectInfo(plugin, fileSystem: _fileSystem);
if (currentPluginInfo.containsKey(info.guid)) {
throwToolExit('The plugins "${currentPluginInfo[info.guid]}" and "${info.name}" '
'have the same ProjectGuid, which prevents them from being used together.\n\n'
'Please contact the plugin authors to resolve this, and/or remove one of the '
'plugins from your project.');
}
currentPluginInfo[info.guid] = info.name;
}
}
return currentPluginInfo;
}
/// Walks a GlobalSection or ProjectSection, removing entries related to removed
/// plugins and adding entries for new plugins at the end using
/// [newEntryWriter], which takes the guid of the plugin to write entries for.
///
/// The caller is responsible for printing the section start line, which should
/// be [lineIterator.current] when this is called, and the section end line,
/// which will be [lineIterator.current] on return.
void _processSectionPluginReferences(
Set<String> removedPlugins,
Set<String> addedPlugins,
Iterator<String> lineIterator,
Function(String, StringBuffer) newEntryWriter,
StringBuffer output,
) {
// Extracts the guid of the project that a line refers to. Currently all
// sections this function is used for start with "{project guid}", even though
// the rest of the line varies by section, so the pattern can currently be
// shared rather than parameterized.
final RegExp entryPattern = RegExp(r'^\s*{([A-Fa-f0-9\-]*)}');
final RegExp sectionEndPattern = RegExp(r'^\s*End\w*Section\s*$');
while (lineIterator.moveNext()) {
if (sectionEndPattern.hasMatch(lineIterator.current)) {
// The end of the section; add entries for new plugins, then exit.
for (final String guid in addedPlugins) {
newEntryWriter(guid, output);
}
return;
}
// Otherwise it's the sectino body. Drop any lines associated with old
// plugins, but pass everything else through as output.
final Match entryMatch = entryPattern.firstMatch(lineIterator.current);
if (entryMatch != null && removedPlugins.contains(entryMatch.group(1))) {
continue;
}
output.writeln(lineIterator.current);
}
}
/// Writes all the configuration entries for the plugin project with the given
/// [guid].
///
/// Should be called within the context of writing
/// GlobalSection(ProjectConfigurationPlatforms).
void _writePluginConfigurationEntries(String guid, StringBuffer output) {
final List<String> configurations = <String>['Debug', 'Profile', 'Release'];
final List<String> entryTypes = <String>['ActiveCfg', 'Build.0'];
for (final String configuration in configurations) {
for (final String entryType in entryTypes) {
output.writeln('\t\t{$guid}.$configuration|x64.$entryType = $configuration|x64\r');
}
}
}
/// Writes the entries to nest the plugin projects with the given [guid] under
/// the Flutter Plugins solution folder.
///
/// Should be called within the context of writing
/// GlobalSection(NestedProjects).
void _writePluginNestingEntry(String guid, StringBuffer output) {
output.writeln('\t\t{$guid} = {$_kFlutterPluginSolutionFolderGuid}\r');
}
/// Writes the entrie to make a project depend on another project with the
/// given [guid].
///
/// Should be called within the context of writing
/// ProjectSection(ProjectDependencies).
void _writeProjectDependency(String guid, StringBuffer output) {
output.writeln('\t\t{$guid} = {$guid}\r');
}
}