Skip to content

Commit e284644

Browse files
committed
sln-add: Support for slnx (dotnet#44570)
1 parent 25fd857 commit e284644

File tree

40 files changed

+1214
-787
lines changed

40 files changed

+1214
-787
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,180 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.CommandLine;
6+
using Microsoft.Build.Construction;
7+
using Microsoft.Build.Exceptions;
8+
using Microsoft.Build.Execution;
59
using Microsoft.DotNet.Cli;
6-
using Microsoft.DotNet.Cli.Sln.Internal;
710
using Microsoft.DotNet.Cli.Utils;
811
using Microsoft.DotNet.Tools.Common;
12+
using Microsoft.VisualStudio.SolutionPersistence;
13+
using Microsoft.VisualStudio.SolutionPersistence.Model;
14+
using Microsoft.VisualStudio.SolutionPersistence.Serializer.SlnV12;
915

1016
namespace Microsoft.DotNet.Tools.Sln.Add
1117
{
1218
internal class AddProjectToSolutionCommand : CommandBase
1319
{
20+
private static string[] _defaultPlatforms = new[] { "Any CPU", "x64", "x86" };
21+
private static string[] _defaultBuildTypes = new[] { "Debug", "Release" };
1422
private readonly string _fileOrDirectory;
1523
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+
}
1833

1934
public AddProjectToSolutionCommand(ParseResult parseResult) : base(parseResult)
2035
{
2136
_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) ?? []);
2538
_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);
4141
}
4242

4343
public override int Execute()
4444
{
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)
4946
{
5047
throw new GracefulException(CommonLocalizableStrings.SpecifyAtLeastOneProjectToAdd);
5148
}
49+
string solutionFileFullPath = SlnCommandParser.GetSlnFileFullPath(_fileOrDirectory);
5250

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
6652
{
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;
7161
}
72-
73-
if (slnFile.Projects.Count > preAddProjectCount)
62+
catch (Exception ex) when (ex is not GracefulException)
7463
{
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+
}
7671
}
77-
78-
return 0;
7972
}
8073

81-
private static IList<string> GetSolutionFoldersFromProjectPath(string projectFilePath)
74+
private async Task AddProjectsToSolutionAsync(string solutionFileFullPath, IEnumerable<string> projectPaths, CancellationToken cancellationToken)
8275
{
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)
9080
{
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);
9294
}
9395

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;
10399

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);
105131
}
106132

107-
private IList<string> DetermineSolutionFolder(SlnFile slnFile, string fullProjectPath)
133+
private void AddProject(SolutionModel solution, string solutionRelativeProjectPath, string fullPath, SolutionFolderModel? solutionFolder)
108134
{
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
110139
{
111-
// The user requested all projects go to the root folder
112-
return null;
140+
project = solution.AddProject(solutionRelativeProjectPath, null, solutionFolder);
113141
}
114-
115-
if (_relativeRootSolutionFolders != null)
142+
catch (SolutionArgumentException ex) when (ex.ParamName == "projectTypeName")
116143
{
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);
119159
}
120160

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();
133163

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+
}
138170

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);
142178
}
143179
}
144180
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<invalid>
2+
</invalid>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is a test of an invalid solution.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<invalid>
2+
</invalid>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Solution>
2+
<Configurations>
3+
<Platform Name="Any CPU" />
4+
<Platform Name="x64" />
5+
<Platform Name="x86" />
6+
</Configurations>
7+
</Solution>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio 15
3+
VisualStudioVersion = 15.0.26006.2
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "App\App.csproj", "{7072A694-548F-4CAE-A58F-12D257D5F486}"
6+
EndProject
7+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib", "Lib\Lib.csproj", "__LIB_PROJECT_GUID__"
8+
EndProject
9+
Global
10+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
11+
Debug|Any CPU = Debug|Any CPU
12+
Debug|x64 = Debug|x64
13+
Debug|x86 = Debug|x86
14+
Release|Any CPU = Release|Any CPU
15+
Release|x64 = Release|x64
16+
Release|x86 = Release|x86
17+
EndGlobalSection
18+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
19+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU
21+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64
22+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64
23+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86
24+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86
25+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU
26+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU
27+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64
28+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64
29+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86
30+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86
31+
__LIB_PROJECT_GUID__.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32+
__LIB_PROJECT_GUID__.Debug|Any CPU.Build.0 = Debug|Any CPU
33+
__LIB_PROJECT_GUID__.Debug|x64.ActiveCfg = Debug|Any CPU
34+
__LIB_PROJECT_GUID__.Debug|x64.Build.0 = Debug|Any CPU
35+
__LIB_PROJECT_GUID__.Debug|x86.ActiveCfg = Debug|Any CPU
36+
__LIB_PROJECT_GUID__.Debug|x86.Build.0 = Debug|Any CPU
37+
__LIB_PROJECT_GUID__.Release|Any CPU.ActiveCfg = Release|Any CPU
38+
__LIB_PROJECT_GUID__.Release|Any CPU.Build.0 = Release|Any CPU
39+
__LIB_PROJECT_GUID__.Release|x64.ActiveCfg = Release|Any CPU
40+
__LIB_PROJECT_GUID__.Release|x64.Build.0 = Release|Any CPU
41+
__LIB_PROJECT_GUID__.Release|x86.ActiveCfg = Release|Any CPU
42+
__LIB_PROJECT_GUID__.Release|x86.Build.0 = Release|Any CPU
43+
EndGlobalSection
44+
GlobalSection(SolutionProperties) = preSolution
45+
HideSolutionNode = FALSE
46+
EndGlobalSection
47+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Solution>
2+
<Configurations>
3+
<Platform Name="Any CPU" />
4+
<Platform Name="x64" />
5+
<Platform Name="x86" />
6+
</Configurations>
7+
<Project Path="App/App.csproj">
8+
<Platform Solution="*|x64" Project="x64" />
9+
<Platform Solution="*|x86" Project="x86" />
10+
</Project>
11+
<Project Path="Lib/Lib.csproj" Id="__LIB_PROJECT_GUID__"/>
12+
</Solution>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio 15
3+
VisualStudioVersion = 15.0.26006.2
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib", "Lib\Lib.csproj", "__LIB_PROJECT_GUID__"
6+
EndProject
7+
Global
8+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9+
Debug|Any CPU = Debug|Any CPU
10+
Debug|x64 = Debug|x64
11+
Debug|x86 = Debug|x86
12+
Release|Any CPU = Release|Any CPU
13+
Release|x64 = Release|x64
14+
Release|x86 = Release|x86
15+
EndGlobalSection
16+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
17+
__LIB_PROJECT_GUID__.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18+
__LIB_PROJECT_GUID__.Debug|Any CPU.Build.0 = Debug|Any CPU
19+
__LIB_PROJECT_GUID__.Debug|x64.ActiveCfg = Debug|Any CPU
20+
__LIB_PROJECT_GUID__.Debug|x64.Build.0 = Debug|Any CPU
21+
__LIB_PROJECT_GUID__.Debug|x86.ActiveCfg = Debug|Any CPU
22+
__LIB_PROJECT_GUID__.Debug|x86.Build.0 = Debug|Any CPU
23+
__LIB_PROJECT_GUID__.Release|Any CPU.ActiveCfg = Release|Any CPU
24+
__LIB_PROJECT_GUID__.Release|Any CPU.Build.0 = Release|Any CPU
25+
__LIB_PROJECT_GUID__.Release|x64.ActiveCfg = Release|Any CPU
26+
__LIB_PROJECT_GUID__.Release|x64.Build.0 = Release|Any CPU
27+
__LIB_PROJECT_GUID__.Release|x86.ActiveCfg = Release|Any CPU
28+
__LIB_PROJECT_GUID__.Release|x86.Build.0 = Release|Any CPU
29+
EndGlobalSection
30+
GlobalSection(SolutionProperties) = preSolution
31+
HideSolutionNode = FALSE
32+
EndGlobalSection
33+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Solution>
2+
<Configurations>
3+
<Platform Name="Any CPU" />
4+
<Platform Name="x64" />
5+
<Platform Name="x86" />
6+
</Configurations>
7+
<Project Path="Lib/Lib.csproj" Id="__LIB_PROJECT_GUID__" />
8+
</Solution>

0 commit comments

Comments
 (0)