Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sln-add: Support for slnx #44570

Open
wants to merge 55 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
1d1c4b0
[IMP] sln-list: Support for slnx
edvilme Oct 30, 2024
bdff950
sln-add: Support for slnx
edvilme Oct 31, 2024
01b3031
Handle solution folders
edvilme Nov 1, 2024
277a1d7
Fix tests
edvilme Nov 1, 2024
5e6e3ff
Fix UTF8 BOM tests
edvilme Nov 1, 2024
b4ac6a4
Catch errors
edvilme Nov 1, 2024
3f967f7
Fix additional tests
edvilme Nov 4, 2024
3d52972
Fix additional tests
edvilme Nov 4, 2024
de95e6d
Fix additional issues
edvilme Nov 5, 2024
a090ad4
Remove sdk.slnx file
edvilme Nov 5, 2024
c03cad7
Fix additional tests
edvilme Nov 6, 2024
90f4248
Fix additional tests
edvilme Nov 6, 2024
4929979
Fix additional tests
edvilme Nov 6, 2024
f88c3eb
Fix duplicate project tests
edvilme Nov 6, 2024
a900dc3
Fix tests
edvilme Nov 7, 2024
c394d75
108/133 tests passing
edvilme Nov 7, 2024
c8e5996
Work on tests
edvilme Nov 8, 2024
039e694
Nit
edvilme Nov 8, 2024
cef4d22
Refactored code to fix guid tests
edvilme Nov 8, 2024
51294e9
Fix tests
edvilme Nov 8, 2024
6aee575
Fix some config tests
edvilme Nov 8, 2024
e168158
Revert guid detection
edvilme Nov 8, 2024
07343f9
Update tests guids and translations
edvilme Nov 11, 2024
af2917a
Update translations (build)
edvilme Nov 11, 2024
6d8c3fe
Fix additional tests
edvilme Nov 11, 2024
b195880
Fix issues from pr
edvilme Nov 12, 2024
93775e8
Solve solution folder tests
edvilme Nov 12, 2024
3195e02
Fix additional tests
edvilme Nov 12, 2024
8975dbc
Refactor code
edvilme Nov 13, 2024
2e9b5ea
Update tests
edvilme Nov 13, 2024
76a2f1d
Fix tests
edvilme Nov 13, 2024
60d434c
Fix WhenProjectWithAdditionalConfigurationsIsAddedSolutionDoesNotMapThem
edvilme Nov 19, 2024
f79ddf3
Revert changed project guids
edvilme Nov 19, 2024
d90d9df
Revert changed project guids
edvilme Nov 20, 2024
63c1fda
Fix project config tests
edvilme Nov 21, 2024
6f0cc41
Fix tests
edvilme Nov 21, 2024
7cf1a37
Fix all tests + Update vs-solutionpersistence
edvilme Nov 21, 2024
0c83e51
Nit
edvilme Nov 21, 2024
32cae25
Nit
edvilme Nov 21, 2024
0b3b696
Nit
edvilme Nov 21, 2024
a4a9f74
Fix whitespaces
edvilme Nov 21, 2024
14bac00
[TEST] sln-add: Add example slnx files, and parameters
edvilme Nov 22, 2024
96274cf
[TEST] sln-add: Use vs-solutionpersistence on templates
edvilme Nov 22, 2024
68f8de2
[TEST] sln-add: Add SolutionFilesTemplates
edvilme Nov 22, 2024
f1a735f
[TEST] sln-add: Compare slnx templates
edvilme Nov 23, 2024
9492a03
[TEST] sln-add: Fix sln-templates tests
edvilme Nov 25, 2024
6f8c166
[TEST] sln-add: Migrate all tests
edvilme Nov 26, 2024
13c8cd1
Fix typo
edvilme Nov 26, 2024
2dd69b0
[TEST] sln-list: Update testAsset identifiers
edvilme Nov 26, 2024
6e62402
[TEST] restore: Add App.sln to argument list
edvilme Nov 26, 2024
8992fce
Merge branch 'main' into edvilme-slnx-add
edvilme Nov 26, 2024
12533a5
Merge branch 'main' into edvilme-slnx-add
edvilme Nov 27, 2024
d8fbc8e
sln-add: When working and solution directories are different, resolve…
edvilme Nov 27, 2024
c3bac1d
Merge branch 'edvilme-slnx-add' of https://github.com/edvilme/sdk int…
edvilme Nov 27, 2024
f7a4fb7
Nit
edvilme Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Cli/dotnet/commands/dotnet-sln/LocalizableStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@
<value>.slnx file {0} generated.</value>
</data>
<data name="CannotMigrateSlnx" xml:space="preserve">
<value>Only .sln files can be migrated to .slnx format.</value>
<value>Cannot migrate .slnx file.</value>
edvilme marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="SerializerNotFound" xml:space="preserve">
<value>Could not read solution file {0}. Supported files are .sln and .slnx valid solutions.</value>
<value>Could not find serializer for file {0}.</value>
edvilme marked this conversation as resolved.
Show resolved Hide resolved
</data>
</root>
191 changes: 104 additions & 87 deletions src/Cli/dotnet/commands/dotnet-sln/add/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,143 +2,160 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.Linq.Expressions;
edvilme marked this conversation as resolved.
Show resolved Hide resolved
using System.Text.RegularExpressions;
using Microsoft.Build.Construction;
using Microsoft.Build.Exceptions;
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 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;

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

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

_inRoot = parseResult.GetValue(SlnAddParser.InRootOption);
string relativeRoot = parseResult.GetValue(SlnAddParser.SolutionFolderOption);
_solutionFolderPath = 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;
}
SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SlnArgumentValidator.CommandType.Add, _inRoot, _solutionFolderPath);
}

public override int Execute()
{
SlnFile slnFile = SlnFileFactory.CreateFromFileOrDirectory(_fileOrDirectory);
var solutionFileFullPath = SlnCommandParser.GetSlnFileFullPath(_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);
}

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

var fullProjectPaths = _arguments.Select(p =>
PathUtility.EnsureAllPathsExist(_projects, CommonLocalizableStrings.CouldNotFindProjectOrDirectory, true);
try
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: \n before this

(Also just anywhere you have }, I prefer a \n before the next thing with few exceptions)

{
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)
var fullProjectPaths = _projects.Select(project =>
{
var fullPath = Path.GetFullPath(project);
return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : fullPath;
});
AddProjectsToSolutionAsync(solutionFileFullPath, fullProjectPaths, CancellationToken.None).Wait();
return 0;
}
catch (GracefulException)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other catch explicitly excludes this case, and this case just throws, so we can skip this block.

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

slnFile.AddProject(fullProjectPath, solutionFolders);
throw;
}

if (slnFile.Projects.Count > preAddProjectCount)
catch (Exception ex)
{
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a new runtime concept? I don't see it in the SDK currently or in this PR, and a quick bing (google) search didn't reveal anything interesting. If so, should we put this behind #ifdefs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which part are you refering to here?

SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken);
// set UTF8 BOM encoding for .sln
if (serializer is ISolutionSerializer<SlnV12SerializerSettings> v12Serializer)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the significance of V12?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/microsoft/vs-solutionpersistence/tree/main/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12

i think the "v12" format ensures compatibility with Visual Studio 2013 and later versions. previous versions of vs might not understand solution files created in this schema unless explicitly backported. It has enhanced support for modern project types, including new sdk-style projects and multi-targeting and better integration with msbuild, with the solution file acting as a meta-structure over the projects it includes.

{
projectFilePath = projectFilePath.Substring(currentDirString.Length);
solution.SerializerExtension = v12Serializer.CreateModelExtension(new()
{
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)
});
}

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

return solutionFolders;
SolutionFolderModel? solutionFolder = (!_inRoot && _solutionFolderPath != null)
? solution.AddFolder(GetSolutionFolderPathWithForwardSlashes())
: null;
foreach (var projectPath in projectPaths)
{
// Get full project path
var relativePath = Path.GetRelativePath(Path.GetDirectoryName(solutionFileFullPath), projectPath);
try
edvilme marked this conversation as resolved.
Show resolved Hide resolved
{
// Try to open the project to see if it is valid
ProjectRootElement.Open(projectPath);
AddProjectWithDefaultGuid(solution, relativePath, solutionFolder);
Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectAddedToTheSolution, relativePath);
}
catch (InvalidProjectFileException ex)
{
Reporter.Error.WriteLine(string.Format(
CommonLocalizableStrings.InvalidProjectWithExceptionMessage, projectPath, ex.Message));
}
catch (ArgumentException ex)
{
// TODO: There are some cases where the project is not found but it already exists on the solution. So it is useful to check the error message. Will remove on future commit.
if (solution.FindProject(relativePath) != null || Regex.Match(ex.Message, @"Project name '.*' already exists in the solution folder.").Success)
edvilme marked this conversation as resolved.
Show resolved Hide resolved
{
Reporter.Output.WriteLine(CommonLocalizableStrings.SolutionAlreadyContainsProject, solutionFileFullPath, relativePath);
}
else
{
throw;
}
}
}
AddDefaultProjectConfigurations(solution);
await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken);
}

private IList<string> DetermineSolutionFolder(SlnFile slnFile, string fullProjectPath)
private void AddDefaultProjectConfigurations(SolutionModel solution)
{
if (_inRoot)
string[] defaultConfigurationPlatforms = { "Any CPU", "x86", "x64" };
foreach (var platform in defaultConfigurationPlatforms)
{
// The user requested all projects go to the root folder
return null;
solution.AddPlatform(platform);
}

if (_relativeRootSolutionFolders != null)
string[] defaultConfigurationBuildTypes = { "Debug", "Release" };
foreach (var buildType in defaultConfigurationBuildTypes)
{
// The user has specified an explicit root
return _relativeRootSolutionFolders;
solution.AddBuildType(buildType);
}

// 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("..");
solution.DistillProjectConfigurations();
}

private static string TrimProject(string path)
private string GetSolutionFolderPathWithForwardSlashes()
{
return Path.GetDirectoryName(path);
// SolutionModel::AddFolder expects path to have leading, trailing and inner forward slashes
return "/" + string.Join("/", PathUtility.GetPathWithDirectorySeparator(_solutionFolderPath).Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)) + "/";
}

private static string TrimProjectDirectory(string path)
private void AddProjectWithDefaultGuid(SolutionModel solution, string relativePath, SolutionFolderModel solutionFolder)
{
return Path.GetDirectoryName(path);
SolutionProjectModel project;
try
{
solution.AddProject(relativePath, null, solutionFolder);
}
catch (ArgumentException ex)
{
// TODO: Update with error codes from vs-solutionpersistence
if (ex.Message == "ProjectType '' not found. (Parameter 'projectTypeName')")
{
solution.AddProject(relativePath, "130159A9-F047-44B3-88CF-0CF7F02ED50F", solutionFolder);
}
else
{
throw;
}
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading