| // 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'); |
| } |
| } |