|
1 | 1 | // Licensed to the .NET Foundation under one or more agreements.
|
2 | 2 | // The .NET Foundation licenses this file to you under the MIT license.
|
3 | 3 |
|
| 4 | +using System; |
4 | 5 | using System.CommandLine;
|
| 6 | +using Microsoft.Build.Construction; |
| 7 | +using Microsoft.Build.Exceptions; |
| 8 | +using Microsoft.Build.Execution; |
5 | 9 | using Microsoft.DotNet.Cli;
|
6 |
| -using Microsoft.DotNet.Cli.Sln.Internal; |
7 | 10 | using Microsoft.DotNet.Cli.Utils;
|
8 | 11 | using Microsoft.DotNet.Tools.Common;
|
| 12 | +using Microsoft.VisualStudio.SolutionPersistence; |
| 13 | +using Microsoft.VisualStudio.SolutionPersistence.Model; |
| 14 | +using Microsoft.VisualStudio.SolutionPersistence.Serializer.SlnV12; |
9 | 15 |
|
10 | 16 | namespace Microsoft.DotNet.Tools.Sln.Add
|
11 | 17 | {
|
12 | 18 | internal class AddProjectToSolutionCommand : CommandBase
|
13 | 19 | {
|
| 20 | + private static string[] _defaultPlatforms = new[] { "Any CPU", "x64", "x86" }; |
| 21 | + private static string[] _defaultBuildTypes = new[] { "Debug", "Release" }; |
14 | 22 | private readonly string _fileOrDirectory;
|
15 | 23 | private readonly bool _inRoot;
|
16 |
| - private readonly IList<string> _relativeRootSolutionFolders; |
17 |
| - private readonly IReadOnlyCollection<string> _arguments; |
| 24 | + private readonly IReadOnlyCollection<string> _projects; |
| 25 | + private readonly string? _solutionFolderPath; |
| 26 | + |
| 27 | + private static string GetSolutionFolderPathWithForwardSlashes(string path) |
| 28 | + { |
| 29 | + // SolutionModel::AddFolder expects paths to have leading, trailing and inner forward slashes |
| 30 | + // https://github.com/microsoft/vs-solutionpersistence/blob/87ee8ea069662d55c336a9bd68fe4851d0384fa5/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs#L171C1-L172C1 |
| 31 | + return "/" + string.Join("/", PathUtility.GetPathWithDirectorySeparator(path).Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)) + "/"; |
| 32 | + } |
18 | 33 |
|
19 | 34 | public AddProjectToSolutionCommand(ParseResult parseResult) : base(parseResult)
|
20 | 35 | {
|
21 | 36 | _fileOrDirectory = parseResult.GetValue(SlnCommandParser.SlnArgument);
|
22 |
| - |
23 |
| - _arguments = parseResult.GetValue(SlnAddParser.ProjectPathArgument)?.ToArray() ?? (IReadOnlyCollection<string>)Array.Empty<string>(); |
24 |
| - |
| 37 | + _projects = (IReadOnlyCollection<string>)(parseResult.GetValue(SlnAddParser.ProjectPathArgument) ?? []); |
25 | 38 | _inRoot = parseResult.GetValue(SlnAddParser.InRootOption);
|
26 |
| - string relativeRoot = parseResult.GetValue(SlnAddParser.SolutionFolderOption); |
27 |
| - |
28 |
| - SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _arguments, SlnArgumentValidator.CommandType.Add, _inRoot, relativeRoot); |
29 |
| - |
30 |
| - bool hasRelativeRoot = !string.IsNullOrEmpty(relativeRoot); |
31 |
| - |
32 |
| - if (hasRelativeRoot) |
33 |
| - { |
34 |
| - relativeRoot = PathUtility.GetPathWithDirectorySeparator(relativeRoot); |
35 |
| - _relativeRootSolutionFolders = relativeRoot.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); |
36 |
| - } |
37 |
| - else |
38 |
| - { |
39 |
| - _relativeRootSolutionFolders = null; |
40 |
| - } |
| 39 | + _solutionFolderPath = parseResult.GetValue(SlnAddParser.SolutionFolderOption); |
| 40 | + SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SlnArgumentValidator.CommandType.Add, _inRoot, _solutionFolderPath); |
41 | 41 | }
|
42 | 42 |
|
43 | 43 | public override int Execute()
|
44 | 44 | {
|
45 |
| - SlnFile slnFile = SlnFileFactory.CreateFromFileOrDirectory(_fileOrDirectory); |
46 |
| - |
47 |
| - var arguments = (_parseResult.GetValue<IEnumerable<string>>(SlnAddParser.ProjectPathArgument) ?? Array.Empty<string>()).ToList().AsReadOnly(); |
48 |
| - if (arguments.Count == 0) |
| 45 | + if (_projects.Count == 0) |
49 | 46 | {
|
50 | 47 | throw new GracefulException(CommonLocalizableStrings.SpecifyAtLeastOneProjectToAdd);
|
51 | 48 | }
|
| 49 | + string solutionFileFullPath = SlnCommandParser.GetSlnFileFullPath(_fileOrDirectory); |
52 | 50 |
|
53 |
| - PathUtility.EnsureAllPathsExist(arguments, CommonLocalizableStrings.CouldNotFindProjectOrDirectory, true); |
54 |
| - |
55 |
| - var fullProjectPaths = _arguments.Select(p => |
56 |
| - { |
57 |
| - var fullPath = Path.GetFullPath(p); |
58 |
| - return Directory.Exists(fullPath) ? |
59 |
| - MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : |
60 |
| - fullPath; |
61 |
| - }).ToList(); |
62 |
| - |
63 |
| - var preAddProjectCount = slnFile.Projects.Count; |
64 |
| - |
65 |
| - foreach (var fullProjectPath in fullProjectPaths) |
| 51 | + try |
66 | 52 | {
|
67 |
| - // Identify the intended solution folders |
68 |
| - var solutionFolders = DetermineSolutionFolder(slnFile, fullProjectPath); |
69 |
| - |
70 |
| - slnFile.AddProject(fullProjectPath, solutionFolders); |
| 53 | + PathUtility.EnsureAllPathsExist(_projects, CommonLocalizableStrings.CouldNotFindProjectOrDirectory, true); |
| 54 | + IEnumerable<string> fullProjectPaths = _projects.Select(project => |
| 55 | + { |
| 56 | + var fullPath = Path.GetFullPath(project); |
| 57 | + return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : fullPath; |
| 58 | + }); |
| 59 | + AddProjectsToSolutionAsync(solutionFileFullPath, fullProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); |
| 60 | + return 0; |
71 | 61 | }
|
72 |
| - |
73 |
| - if (slnFile.Projects.Count > preAddProjectCount) |
| 62 | + catch (Exception ex) when (ex is not GracefulException) |
74 | 63 | {
|
75 |
| - slnFile.Write(); |
| 64 | + { |
| 65 | + if (ex is SolutionException || ex.InnerException is SolutionException) |
| 66 | + { |
| 67 | + throw new GracefulException(CommonLocalizableStrings.InvalidSolutionFormatString, solutionFileFullPath, ex.Message); |
| 68 | + } |
| 69 | + throw new GracefulException(ex.Message, ex); |
| 70 | + } |
76 | 71 | }
|
77 |
| - |
78 |
| - return 0; |
79 | 72 | }
|
80 | 73 |
|
81 |
| - private static IList<string> GetSolutionFoldersFromProjectPath(string projectFilePath) |
| 74 | + private async Task AddProjectsToSolutionAsync(string solutionFileFullPath, IEnumerable<string> projectPaths, CancellationToken cancellationToken) |
82 | 75 | {
|
83 |
| - var solutionFolders = new List<string>(); |
84 |
| - |
85 |
| - if (!IsPathInTreeRootedAtSolutionDirectory(projectFilePath)) |
86 |
| - return solutionFolders; |
87 |
| - |
88 |
| - var currentDirString = $".{Path.DirectorySeparatorChar}"; |
89 |
| - if (projectFilePath.StartsWith(currentDirString)) |
| 76 | + ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath); |
| 77 | + SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken); |
| 78 | + // set UTF8 BOM encoding for .sln |
| 79 | + if (serializer is ISolutionSerializer<SlnV12SerializerSettings> v12Serializer) |
90 | 80 | {
|
91 |
| - projectFilePath = projectFilePath.Substring(currentDirString.Length); |
| 81 | + solution.SerializerExtension = v12Serializer.CreateModelExtension(new() |
| 82 | + { |
| 83 | + Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true) |
| 84 | + }); |
| 85 | + } |
| 86 | + // Set default configurations and platforms for sln file |
| 87 | + foreach (var platform in _defaultPlatforms) |
| 88 | + { |
| 89 | + solution.AddPlatform(platform); |
| 90 | + } |
| 91 | + foreach (var buildType in _defaultBuildTypes) |
| 92 | + { |
| 93 | + solution.AddBuildType(buildType); |
92 | 94 | }
|
93 | 95 |
|
94 |
| - var projectDirectoryPath = TrimProject(projectFilePath); |
95 |
| - if (string.IsNullOrEmpty(projectDirectoryPath)) |
96 |
| - return solutionFolders; |
97 |
| - |
98 |
| - var solutionFoldersPath = TrimProjectDirectory(projectDirectoryPath); |
99 |
| - if (string.IsNullOrEmpty(solutionFoldersPath)) |
100 |
| - return solutionFolders; |
101 |
| - |
102 |
| - solutionFolders.AddRange(solutionFoldersPath.Split(Path.DirectorySeparatorChar)); |
| 96 | + SolutionFolderModel? solutionFolder = (!_inRoot && !string.IsNullOrEmpty(_solutionFolderPath)) |
| 97 | + ? solution.AddFolder(GetSolutionFolderPathWithForwardSlashes(_solutionFolderPath)) |
| 98 | + : null; |
103 | 99 |
|
104 |
| - return solutionFolders; |
| 100 | + foreach (var projectPath in projectPaths) |
| 101 | + { |
| 102 | + string relativePath = Path.GetRelativePath(Path.GetDirectoryName(solutionFileFullPath), projectPath); |
| 103 | + // Add fallback solution folder |
| 104 | + string relativeSolutionFolder = Path.GetDirectoryName(relativePath); |
| 105 | + if (!_inRoot && solutionFolder is null && !string.IsNullOrEmpty(relativeSolutionFolder)) |
| 106 | + { |
| 107 | + if (relativeSolutionFolder.Split(Path.DirectorySeparatorChar).LastOrDefault() == Path.GetFileNameWithoutExtension(relativePath)) |
| 108 | + { |
| 109 | + relativeSolutionFolder = Path.Combine(relativeSolutionFolder.Split(Path.DirectorySeparatorChar).SkipLast(1).ToArray()); |
| 110 | + } |
| 111 | + if (!string.IsNullOrEmpty(relativeSolutionFolder)) |
| 112 | + { |
| 113 | + solutionFolder = solution.AddFolder(GetSolutionFolderPathWithForwardSlashes(relativeSolutionFolder)); |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + try |
| 118 | + { |
| 119 | + AddProject(solution, relativePath, projectPath, solutionFolder); |
| 120 | + } |
| 121 | + catch (InvalidProjectFileException ex) |
| 122 | + { |
| 123 | + Reporter.Error.WriteLine(string.Format(CommonLocalizableStrings.InvalidProjectWithExceptionMessage, projectPath, ex.Message)); |
| 124 | + } |
| 125 | + catch (SolutionArgumentException ex) when (solution.FindProject(relativePath) != null || ex.Type == SolutionErrorType.DuplicateProjectName) |
| 126 | + { |
| 127 | + Reporter.Output.WriteLine(CommonLocalizableStrings.SolutionAlreadyContainsProject, solutionFileFullPath, relativePath); |
| 128 | + } |
| 129 | + } |
| 130 | + await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken); |
105 | 131 | }
|
106 | 132 |
|
107 |
| - private IList<string> DetermineSolutionFolder(SlnFile slnFile, string fullProjectPath) |
| 133 | + private void AddProject(SolutionModel solution, string solutionRelativeProjectPath, string fullPath, SolutionFolderModel? solutionFolder) |
108 | 134 | {
|
109 |
| - if (_inRoot) |
| 135 | + // Open project instance to see if it is a valid project |
| 136 | + ProjectRootElement projectRootElement = ProjectRootElement.Open(fullPath); |
| 137 | + SolutionProjectModel project; |
| 138 | + try |
110 | 139 | {
|
111 |
| - // The user requested all projects go to the root folder |
112 |
| - return null; |
| 140 | + project = solution.AddProject(solutionRelativeProjectPath, null, solutionFolder); |
113 | 141 | }
|
114 |
| - |
115 |
| - if (_relativeRootSolutionFolders != null) |
| 142 | + catch (SolutionArgumentException ex) when (ex.ParamName == "projectTypeName") |
116 | 143 | {
|
117 |
| - // The user has specified an explicit root |
118 |
| - return _relativeRootSolutionFolders; |
| 144 | + // If guid is not identified by vs-solutionpersistence, check in project element itself |
| 145 | + var guid = projectRootElement.GetProjectTypeGuid(); |
| 146 | + if (string.IsNullOrEmpty(guid)) |
| 147 | + { |
| 148 | + Reporter.Error.WriteLine(CommonLocalizableStrings.UnsupportedProjectType, fullPath); |
| 149 | + return; |
| 150 | + } |
| 151 | + project = solution.AddProject(solutionRelativeProjectPath, guid, solutionFolder); |
| 152 | + } |
| 153 | + // Add settings based on existing project instance |
| 154 | + ProjectInstance projectInstance = new ProjectInstance(projectRootElement); |
| 155 | + string projectInstanceId = projectInstance.GetProjectId(); |
| 156 | + if (!string.IsNullOrEmpty(projectInstanceId)) |
| 157 | + { |
| 158 | + project.Id = new Guid(projectInstanceId); |
119 | 159 | }
|
120 | 160 |
|
121 |
| - // We determine the root for each individual project |
122 |
| - var relativeProjectPath = Path.GetRelativePath( |
123 |
| - PathUtility.EnsureTrailingSlash(slnFile.BaseDirectory), |
124 |
| - fullProjectPath); |
125 |
| - |
126 |
| - return GetSolutionFoldersFromProjectPath(relativeProjectPath); |
127 |
| - } |
128 |
| - |
129 |
| - private static bool IsPathInTreeRootedAtSolutionDirectory(string path) |
130 |
| - { |
131 |
| - return !path.StartsWith(".."); |
132 |
| - } |
| 161 | + var projectInstanceBuildTypes = projectInstance.GetConfigurations(); |
| 162 | + var projectInstancePlatforms = projectInstance.GetPlatforms(); |
133 | 163 |
|
134 |
| - private static string TrimProject(string path) |
135 |
| - { |
136 |
| - return Path.GetDirectoryName(path); |
137 |
| - } |
| 164 | + foreach (var solutionPlatform in solution.Platforms) |
| 165 | + { |
| 166 | + var projectPlatform = projectInstancePlatforms.FirstOrDefault( |
| 167 | + platform => platform.Replace(" ", string.Empty) == solutionPlatform.Replace(" ", string.Empty), projectInstancePlatforms.FirstOrDefault()); |
| 168 | + project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.Platform, "*", solutionPlatform, projectPlatform)); |
| 169 | + } |
138 | 170 |
|
139 |
| - private static string TrimProjectDirectory(string path) |
140 |
| - { |
141 |
| - return Path.GetDirectoryName(path); |
| 171 | + foreach (var solutionBuildType in solution.BuildTypes) |
| 172 | + { |
| 173 | + var projectBuildType = projectInstanceBuildTypes.FirstOrDefault( |
| 174 | + buildType => buildType.Replace(" ", string.Empty) == solutionBuildType.Replace(" ", string.Empty), projectInstanceBuildTypes.FirstOrDefault()); |
| 175 | + project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.BuildType, solutionBuildType, "*", projectBuildType)); |
| 176 | + } |
| 177 | + Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectAddedToTheSolution, solutionRelativeProjectPath); |
142 | 178 | }
|
143 | 179 | }
|
144 | 180 | }
|
0 commit comments