Skip to content

Commit

Permalink
sln-add: Support for slnx (dotnet#44570)
Browse files Browse the repository at this point in the history
  • Loading branch information
edvilme committed Dec 11, 2024
1 parent 25fd857 commit e284644
Show file tree
Hide file tree
Showing 40 changed files with 1,214 additions and 787 deletions.
222 changes: 129 additions & 93 deletions src/Cli/dotnet/commands/dotnet-sln/add/Program.cs
Original file line number Diff line number Diff line change
@@ -1,144 +1,180 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.CommandLine;
using Microsoft.Build.Construction;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Sln.Internal;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Tools.Common;
using Microsoft.VisualStudio.SolutionPersistence;
using Microsoft.VisualStudio.SolutionPersistence.Model;
using Microsoft.VisualStudio.SolutionPersistence.Serializer.SlnV12;

namespace Microsoft.DotNet.Tools.Sln.Add
{
internal class AddProjectToSolutionCommand : CommandBase
{
private static string[] _defaultPlatforms = new[] { "Any CPU", "x64", "x86" };
private static string[] _defaultBuildTypes = new[] { "Debug", "Release" };
private readonly string _fileOrDirectory;
private readonly bool _inRoot;
private readonly IList<string> _relativeRootSolutionFolders;
private readonly IReadOnlyCollection<string> _arguments;
private readonly IReadOnlyCollection<string> _projects;
private readonly string? _solutionFolderPath;

private static string GetSolutionFolderPathWithForwardSlashes(string path)
{
// SolutionModel::AddFolder expects paths to have leading, trailing and inner forward slashes
// https://github.com/microsoft/vs-solutionpersistence/blob/87ee8ea069662d55c336a9bd68fe4851d0384fa5/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs#L171C1-L172C1
return "/" + string.Join("/", PathUtility.GetPathWithDirectorySeparator(path).Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)) + "/";
}

public AddProjectToSolutionCommand(ParseResult parseResult) : base(parseResult)
{
_fileOrDirectory = parseResult.GetValue(SlnCommandParser.SlnArgument);

_arguments = parseResult.GetValue(SlnAddParser.ProjectPathArgument)?.ToArray() ?? (IReadOnlyCollection<string>)Array.Empty<string>();

_projects = (IReadOnlyCollection<string>)(parseResult.GetValue(SlnAddParser.ProjectPathArgument) ?? []);
_inRoot = parseResult.GetValue(SlnAddParser.InRootOption);
string relativeRoot = parseResult.GetValue(SlnAddParser.SolutionFolderOption);

SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _arguments, SlnArgumentValidator.CommandType.Add, _inRoot, relativeRoot);

bool hasRelativeRoot = !string.IsNullOrEmpty(relativeRoot);

if (hasRelativeRoot)
{
relativeRoot = PathUtility.GetPathWithDirectorySeparator(relativeRoot);
_relativeRootSolutionFolders = relativeRoot.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
}
else
{
_relativeRootSolutionFolders = null;
}
_solutionFolderPath = parseResult.GetValue(SlnAddParser.SolutionFolderOption);
SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SlnArgumentValidator.CommandType.Add, _inRoot, _solutionFolderPath);
}

public override int Execute()
{
SlnFile slnFile = SlnFileFactory.CreateFromFileOrDirectory(_fileOrDirectory);

var arguments = (_parseResult.GetValue<IEnumerable<string>>(SlnAddParser.ProjectPathArgument) ?? Array.Empty<string>()).ToList().AsReadOnly();
if (arguments.Count == 0)
if (_projects.Count == 0)
{
throw new GracefulException(CommonLocalizableStrings.SpecifyAtLeastOneProjectToAdd);
}
string solutionFileFullPath = SlnCommandParser.GetSlnFileFullPath(_fileOrDirectory);

PathUtility.EnsureAllPathsExist(arguments, CommonLocalizableStrings.CouldNotFindProjectOrDirectory, true);

var fullProjectPaths = _arguments.Select(p =>
{
var fullPath = Path.GetFullPath(p);
return Directory.Exists(fullPath) ?
MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName :
fullPath;
}).ToList();

var preAddProjectCount = slnFile.Projects.Count;

foreach (var fullProjectPath in fullProjectPaths)
try
{
// Identify the intended solution folders
var solutionFolders = DetermineSolutionFolder(slnFile, fullProjectPath);

slnFile.AddProject(fullProjectPath, solutionFolders);
PathUtility.EnsureAllPathsExist(_projects, CommonLocalizableStrings.CouldNotFindProjectOrDirectory, true);
IEnumerable<string> fullProjectPaths = _projects.Select(project =>
{
var fullPath = Path.GetFullPath(project);
return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : fullPath;
});
AddProjectsToSolutionAsync(solutionFileFullPath, fullProjectPaths, CancellationToken.None).GetAwaiter().GetResult();
return 0;
}

if (slnFile.Projects.Count > preAddProjectCount)
catch (Exception ex) when (ex is not GracefulException)
{
slnFile.Write();
{
if (ex is SolutionException || ex.InnerException is SolutionException)
{
throw new GracefulException(CommonLocalizableStrings.InvalidSolutionFormatString, solutionFileFullPath, ex.Message);
}
throw new GracefulException(ex.Message, ex);
}
}

return 0;
}

private static IList<string> GetSolutionFoldersFromProjectPath(string projectFilePath)
private async Task AddProjectsToSolutionAsync(string solutionFileFullPath, IEnumerable<string> projectPaths, CancellationToken cancellationToken)
{
var solutionFolders = new List<string>();

if (!IsPathInTreeRootedAtSolutionDirectory(projectFilePath))
return solutionFolders;

var currentDirString = $".{Path.DirectorySeparatorChar}";
if (projectFilePath.StartsWith(currentDirString))
ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath);
SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken);
// set UTF8 BOM encoding for .sln
if (serializer is ISolutionSerializer<SlnV12SerializerSettings> v12Serializer)
{
projectFilePath = projectFilePath.Substring(currentDirString.Length);
solution.SerializerExtension = v12Serializer.CreateModelExtension(new()
{
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)
});
}
// Set default configurations and platforms for sln file
foreach (var platform in _defaultPlatforms)
{
solution.AddPlatform(platform);
}
foreach (var buildType in _defaultBuildTypes)
{
solution.AddBuildType(buildType);
}

var projectDirectoryPath = TrimProject(projectFilePath);
if (string.IsNullOrEmpty(projectDirectoryPath))
return solutionFolders;

var solutionFoldersPath = TrimProjectDirectory(projectDirectoryPath);
if (string.IsNullOrEmpty(solutionFoldersPath))
return solutionFolders;

solutionFolders.AddRange(solutionFoldersPath.Split(Path.DirectorySeparatorChar));
SolutionFolderModel? solutionFolder = (!_inRoot && !string.IsNullOrEmpty(_solutionFolderPath))
? solution.AddFolder(GetSolutionFolderPathWithForwardSlashes(_solutionFolderPath))
: null;

return solutionFolders;
foreach (var projectPath in projectPaths)
{
string relativePath = Path.GetRelativePath(Path.GetDirectoryName(solutionFileFullPath), projectPath);
// Add fallback solution folder
string relativeSolutionFolder = Path.GetDirectoryName(relativePath);
if (!_inRoot && solutionFolder is null && !string.IsNullOrEmpty(relativeSolutionFolder))
{
if (relativeSolutionFolder.Split(Path.DirectorySeparatorChar).LastOrDefault() == Path.GetFileNameWithoutExtension(relativePath))
{
relativeSolutionFolder = Path.Combine(relativeSolutionFolder.Split(Path.DirectorySeparatorChar).SkipLast(1).ToArray());
}
if (!string.IsNullOrEmpty(relativeSolutionFolder))
{
solutionFolder = solution.AddFolder(GetSolutionFolderPathWithForwardSlashes(relativeSolutionFolder));
}
}

try
{
AddProject(solution, relativePath, projectPath, solutionFolder);
}
catch (InvalidProjectFileException ex)
{
Reporter.Error.WriteLine(string.Format(CommonLocalizableStrings.InvalidProjectWithExceptionMessage, projectPath, ex.Message));
}
catch (SolutionArgumentException ex) when (solution.FindProject(relativePath) != null || ex.Type == SolutionErrorType.DuplicateProjectName)
{
Reporter.Output.WriteLine(CommonLocalizableStrings.SolutionAlreadyContainsProject, solutionFileFullPath, relativePath);
}
}
await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken);
}

private IList<string> DetermineSolutionFolder(SlnFile slnFile, string fullProjectPath)
private void AddProject(SolutionModel solution, string solutionRelativeProjectPath, string fullPath, SolutionFolderModel? solutionFolder)
{
if (_inRoot)
// Open project instance to see if it is a valid project
ProjectRootElement projectRootElement = ProjectRootElement.Open(fullPath);
SolutionProjectModel project;
try
{
// The user requested all projects go to the root folder
return null;
project = solution.AddProject(solutionRelativeProjectPath, null, solutionFolder);
}

if (_relativeRootSolutionFolders != null)
catch (SolutionArgumentException ex) when (ex.ParamName == "projectTypeName")
{
// The user has specified an explicit root
return _relativeRootSolutionFolders;
// If guid is not identified by vs-solutionpersistence, check in project element itself
var guid = projectRootElement.GetProjectTypeGuid();
if (string.IsNullOrEmpty(guid))
{
Reporter.Error.WriteLine(CommonLocalizableStrings.UnsupportedProjectType, fullPath);
return;
}
project = solution.AddProject(solutionRelativeProjectPath, guid, solutionFolder);
}
// Add settings based on existing project instance
ProjectInstance projectInstance = new ProjectInstance(projectRootElement);
string projectInstanceId = projectInstance.GetProjectId();
if (!string.IsNullOrEmpty(projectInstanceId))
{
project.Id = new Guid(projectInstanceId);
}

// We determine the root for each individual project
var relativeProjectPath = Path.GetRelativePath(
PathUtility.EnsureTrailingSlash(slnFile.BaseDirectory),
fullProjectPath);

return GetSolutionFoldersFromProjectPath(relativeProjectPath);
}

private static bool IsPathInTreeRootedAtSolutionDirectory(string path)
{
return !path.StartsWith("..");
}
var projectInstanceBuildTypes = projectInstance.GetConfigurations();
var projectInstancePlatforms = projectInstance.GetPlatforms();

private static string TrimProject(string path)
{
return Path.GetDirectoryName(path);
}
foreach (var solutionPlatform in solution.Platforms)
{
var projectPlatform = projectInstancePlatforms.FirstOrDefault(
platform => platform.Replace(" ", string.Empty) == solutionPlatform.Replace(" ", string.Empty), projectInstancePlatforms.FirstOrDefault());
project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.Platform, "*", solutionPlatform, projectPlatform));
}

private static string TrimProjectDirectory(string path)
{
return Path.GetDirectoryName(path);
foreach (var solutionBuildType in solution.BuildTypes)
{
var projectBuildType = projectInstanceBuildTypes.FirstOrDefault(
buildType => buildType.Replace(" ", string.Empty) == solutionBuildType.Replace(" ", string.Empty), projectInstanceBuildTypes.FirstOrDefault());
project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.BuildType, solutionBuildType, "*", projectBuildType));
}
Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectAddedToTheSolution, solutionRelativeProjectPath);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<invalid>
</invalid>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a test of an invalid solution.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<invalid>
</invalid>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
</Solution>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26006.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "App\App.csproj", "{7072A694-548F-4CAE-A58F-12D257D5F486}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib", "Lib\Lib.csproj", "__LIB_PROJECT_GUID__"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86
__LIB_PROJECT_GUID__.Debug|Any CPU.ActiveCfg = Debug|Any CPU
__LIB_PROJECT_GUID__.Debug|Any CPU.Build.0 = Debug|Any CPU
__LIB_PROJECT_GUID__.Debug|x64.ActiveCfg = Debug|Any CPU
__LIB_PROJECT_GUID__.Debug|x64.Build.0 = Debug|Any CPU
__LIB_PROJECT_GUID__.Debug|x86.ActiveCfg = Debug|Any CPU
__LIB_PROJECT_GUID__.Debug|x86.Build.0 = Debug|Any CPU
__LIB_PROJECT_GUID__.Release|Any CPU.ActiveCfg = Release|Any CPU
__LIB_PROJECT_GUID__.Release|Any CPU.Build.0 = Release|Any CPU
__LIB_PROJECT_GUID__.Release|x64.ActiveCfg = Release|Any CPU
__LIB_PROJECT_GUID__.Release|x64.Build.0 = Release|Any CPU
__LIB_PROJECT_GUID__.Release|x86.ActiveCfg = Release|Any CPU
__LIB_PROJECT_GUID__.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Project Path="App/App.csproj">
<Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" />
</Project>
<Project Path="Lib/Lib.csproj" Id="__LIB_PROJECT_GUID__"/>
</Solution>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26006.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib", "Lib\Lib.csproj", "__LIB_PROJECT_GUID__"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
__LIB_PROJECT_GUID__.Debug|Any CPU.ActiveCfg = Debug|Any CPU
__LIB_PROJECT_GUID__.Debug|Any CPU.Build.0 = Debug|Any CPU
__LIB_PROJECT_GUID__.Debug|x64.ActiveCfg = Debug|Any CPU
__LIB_PROJECT_GUID__.Debug|x64.Build.0 = Debug|Any CPU
__LIB_PROJECT_GUID__.Debug|x86.ActiveCfg = Debug|Any CPU
__LIB_PROJECT_GUID__.Debug|x86.Build.0 = Debug|Any CPU
__LIB_PROJECT_GUID__.Release|Any CPU.ActiveCfg = Release|Any CPU
__LIB_PROJECT_GUID__.Release|Any CPU.Build.0 = Release|Any CPU
__LIB_PROJECT_GUID__.Release|x64.ActiveCfg = Release|Any CPU
__LIB_PROJECT_GUID__.Release|x64.Build.0 = Release|Any CPU
__LIB_PROJECT_GUID__.Release|x86.ActiveCfg = Release|Any CPU
__LIB_PROJECT_GUID__.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Project Path="Lib/Lib.csproj" Id="__LIB_PROJECT_GUID__" />
</Solution>
Loading

0 comments on commit e284644

Please sign in to comment.