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

Use new serializer library to parse solution files #6219

Merged
merged 7 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<PackageVersion Include="Microsoft.VisualStudio.ProjectSystem.Managed" Version="17.2.0-beta1-20502-01" />
<PackageVersion Include="Microsoft.VisualStudio.ProjectSystem.Managed.VS" Version="17.2.0-beta1-20502-01" />
<PackageVersion Include="Microsoft.VisualStudio.ProjectSystem.VS" Version="17.4.221-pre" />
<PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="1.0.28" />
baronfel marked this conversation as resolved.
Show resolved Hide resolved
<!-- Microsoft.VisualStudio.SDK has vulnerable dependencies System.Text.json and Microsoft.IO.Redist. When it's upgraded, try removing the pinned packages -->
<PackageVersion Include="Microsoft.VisualStudio.SDK" Version="17.11.39714" />
<PackageVersion Include="Microsoft.VisualStudio.Sdk.TestFramework.Xunit" Version="17.11.8" />
Expand Down Expand Up @@ -192,6 +193,7 @@
<_allowBuildFromSourcePackage Include="Microsoft.Extensions.CommandLineUtils.Sources" />
<_allowBuildFromSourcePackage Include="Microsoft.Extensions.FileProviders.Abstractions" />
<_allowBuildFromSourcePackage Include="Microsoft.Extensions.FileSystemGlobbing" />
<_allowBuildFromSourcePackage Include="Microsoft.VisualStudio.SolutionPersistence" />
<_allowBuildFromSourcePackage Include="Microsoft.Web.Xdt" />
<_allowBuildFromSourcePackage Include="Newtonsoft.Json" />
<_allowBuildFromSourcePackage Include="System.Collections.Immutable" />
Expand Down
1 change: 1 addition & 0 deletions NuGet.Config
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<package pattern="Microsoft.*" />
<package pattern="Microsoft.Build.Framework" />
<package pattern="Microsoft.NET.StringTools" />
<package pattern="Microsoft.VisualStudio.SolutionPersistence" />
<package pattern="Microsoft.VisualStudio.TemplateWizardInterface" />
<package pattern="moq" />
<package pattern="MSTest.TestAdapter" />
Expand Down
1 change: 1 addition & 0 deletions eng/SourceBuildPrebuiltBaseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<UsagePattern IdentityGlob="Microsoft.NETCore.Platforms/*" />
<UsagePattern IdentityGlob="Microsoft.NETCore.Targets/*" />
<UsagePattern IdentityGlob="Microsoft.VisualStudio.Setup.Configuration.Interop/*" />
<UsagePattern IdentityGlob="Microsoft.VisualStudio.SolutionPersistence/*" />
<UsagePattern IdentityGlob="Microsoft.Web.Xdt/*" />
<UsagePattern IdentityGlob="Microsoft.Win32.Registry/*" />
<UsagePattern IdentityGlob="Microsoft.Win32.SystemEvents/*" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ public async Task<int> ExecuteCommandAsync(ListPackageArgs listPackageArgs)

//If the given file is a solution, get the list of projects
//If not, then it's a project, which is put in a list
var projectsPaths = Path.GetExtension(listPackageArgs.Path).Equals(".sln", PathUtility.GetStringComparisonBasedOnOS()) ?
MSBuildAPIUtility.GetProjectsFromSolution(listPackageArgs.Path).Where(f => File.Exists(f)) :
var projectsPaths =
(Path.GetExtension(listPackageArgs.Path).Equals(".sln", PathUtility.GetStringComparisonBasedOnOS()) ||
Path.GetExtension(listPackageArgs.Path).Equals(".slnx", PathUtility.GetStringComparisonBasedOnOS())) ?
(await MSBuildAPIUtility.GetProjectsFromSolution(listPackageArgs.Path, listPackageArgs.CancellationToken)).Where(f => File.Exists(f)) :
new List<string>(new string[] { listPackageArgs.Path });

MSBuildAPIUtility msBuild = listPackageReportModel.MSBuildAPIUtility;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.CommandLine.Help;
using System.CommandLine.Parsing;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.CommandLineUtils;

namespace NuGet.CommandLine.XPlat.Commands.Why
Expand Down Expand Up @@ -36,7 +37,7 @@ public static void GetWhyCommand(CliCommand rootCommand)
Register(rootCommand, CommandOutputLogger.Create, WhyCommandRunner.ExecuteCommand);
}

internal static void Register(CliCommand rootCommand, Func<ILoggerWithColor> getLogger, Func<WhyCommandArgs, int> action)
internal static void Register(CliCommand rootCommand, Func<ILoggerWithColor> getLogger, Func<WhyCommandArgs, Task<int>> action)
{
var whyCommand = new DocumentedCommand("why", Strings.WhyCommand_Description, "https://aka.ms/dotnet/nuget/why");

Expand Down Expand Up @@ -97,7 +98,7 @@ bool HasPathArgument(ArgumentResult ar)
whyCommand.Options.Add(frameworks);
whyCommand.Options.Add(help);

whyCommand.SetAction((parseResult) =>
whyCommand.SetAction(async (parseResult, cancellationToken) =>
{
ILoggerWithColor logger = getLogger();

Expand All @@ -107,9 +108,10 @@ bool HasPathArgument(ArgumentResult ar)
parseResult.GetValue(path),
parseResult.GetValue(package),
parseResult.GetValue(frameworks),
logger);
logger,
cancellationToken);

int exitCode = action(whyCommandArgs);
int exitCode = await action(whyCommandArgs);
return exitCode;
}
catch (ArgumentException ex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System;
using System.Collections.Generic;
using System.Threading;

namespace NuGet.CommandLine.XPlat.Commands.Why
{
Expand All @@ -14,6 +15,7 @@ internal class WhyCommandArgs
public string Package { get; }
public List<string> Frameworks { get; }
public ILoggerWithColor Logger { get; }
public CancellationToken CancellationToken { get; }

/// <summary>
/// A constructor for the arguments of the 'why' command.
Expand All @@ -22,16 +24,19 @@ internal class WhyCommandArgs
/// <param name="package">The package for which we show the dependency graphs.</param>
/// <param name="frameworks">The target framework(s) for which we show the dependency graphs.</param>
/// <param name="logger"></param>
/// <param name="cancellationToken"></param>
public WhyCommandArgs(
string path,
string package,
List<string> frameworks,
ILoggerWithColor logger)
ILoggerWithColor logger,
CancellationToken cancellationToken)
{
Path = path ?? throw new ArgumentNullException(nameof(path));
Package = package ?? throw new ArgumentNullException(nameof(package));
Frameworks = frameworks ?? throw new ArgumentNullException(nameof(frameworks));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
CancellationToken = cancellationToken;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Evaluation;
using NuGet.ProjectModel;

Expand All @@ -20,7 +23,7 @@ internal static class WhyCommandRunner
/// Executes the 'why' command.
/// </summary>
/// <param name="whyCommandArgs">CLI arguments for the 'why' command.</param>
public static int ExecuteCommand(WhyCommandArgs whyCommandArgs)
public static async Task<int> ExecuteCommand(WhyCommandArgs whyCommandArgs)
{
bool validArgumentsUsed = ValidatePathArgument(whyCommandArgs.Path, whyCommandArgs.Logger)
&& ValidatePackageArgument(whyCommandArgs.Package, whyCommandArgs.Logger);
Expand All @@ -30,10 +33,10 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs)
}

string targetPackage = whyCommandArgs.Package;
IEnumerable<(string assetsFilePath, string? projectPath)> assetsFiles;
IAsyncEnumerable<(string assetsFilePath, string? projectPath)> assetsFiles;
try
{
assetsFiles = FindAssetsFiles(whyCommandArgs.Path, whyCommandArgs.Logger);
assetsFiles = FindAssetsFilesAsync(whyCommandArgs.Path, whyCommandArgs.Logger, whyCommandArgs.CancellationToken);
}
catch (ArgumentException ex)
{
Expand All @@ -47,7 +50,7 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs)
}

bool anyErrors = false;
foreach ((string assetsFilePath, string? projectPath) in assetsFiles)
await foreach ((string assetsFilePath, string? projectPath) in assetsFiles)
{
LockFile? assetsFile = GetProjectAssetsFile(assetsFilePath, projectPath, whyCommandArgs.Logger);

Expand Down Expand Up @@ -88,15 +91,15 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs)
return anyErrors ? ExitCodes.Error : ExitCodes.Success;
}

private static IEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFiles(string path, ILoggerWithColor logger)
private static async IAsyncEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFilesAsync(string path, ILoggerWithColor logger, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (XPlatUtility.IsJsonFile(path))
{
yield return (path, null);
yield break;
}

var projectPaths = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(path);
var projectPaths = await MSBuildAPIUtility.GetListOfProjectsFromPathArgumentAsync(path, cancellationToken);
foreach (string projectPath in projectPaths.NoAllocEnumerate())
{
Project project = MSBuildAPIUtility.GetProject(projectPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" PrivateAssets="All" />
<PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
</ItemGroup>

<!-- Microsoft.Build.Locator is needed when debugging, but should not be used in the assemblies we insert. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.VisualStudio.SolutionPersistence;
using Microsoft.VisualStudio.SolutionPersistence.Model;
using Microsoft.VisualStudio.SolutionPersistence.Serializer;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.LibraryModel;
Expand Down Expand Up @@ -79,18 +84,19 @@ private static Project GetProject(string projectCSProjPath, IDictionary<string,
return new Project(projectRootElement, globalProperties, toolsVersion: null);
}

internal static IEnumerable<string> GetProjectsFromSolution(string solutionPath)
internal static async Task<IEnumerable<string>> GetProjectsFromSolution(string solutionPath, CancellationToken cancellationToken = default)
{
var sln = SolutionFile.Parse(solutionPath);
return sln.ProjectsInOrder.Select(p => p.AbsolutePath);
ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath);
SolutionModel solution = await serializer.OpenAsync(solutionPath, cancellationToken);
return solution.SolutionProjects.Select(p => p.FilePath);
}

/// <summary>
/// Get the list of project paths from the input 'path' argument. Path must be a directory, solution file or project file.
/// </summary>
/// <returns>List of project paths. Returns null if path was a directory with none or multiple project/solution files.</returns>
/// <exception cref="ArgumentException">Throws an exception if the directory has none or multiple project/solution files.</exception>
internal static IEnumerable<string> GetListOfProjectsFromPathArgument(string path)
internal static async Task<IEnumerable<string>> GetListOfProjectsFromPathArgumentAsync(string path, CancellationToken cancellationToken = default)
{
string fullPath = Path.GetFullPath(path);

Expand All @@ -116,7 +122,7 @@ internal static IEnumerable<string> GetListOfProjectsFromPathArgument(string pat
}

return XPlatUtility.IsSolutionFile(projectOrSolutionFile)
? MSBuildAPIUtility.GetProjectsFromSolution(projectOrSolutionFile).Where(f => File.Exists(f))
? (await MSBuildAPIUtility.GetProjectsFromSolution(projectOrSolutionFile, cancellationToken)).Where(f => File.Exists(f))
: [projectOrSolutionFile];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ internal static bool IsSolutionFile(string fileName)
{
var extension = System.IO.Path.GetExtension(fileName);

return string.Equals(extension, ".sln", StringComparison.OrdinalIgnoreCase);
return string.Equals(extension, ".sln", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".slnx", StringComparison.OrdinalIgnoreCase);
}

return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,17 @@ static void SetupCredentialServiceMock(Mock<ICredentialService> mockedCredential
return outCredentials != null;
});
}
}

static async Task RestoreProjectsAsync(SimpleTestPathContext pathContext, SimpleTestProjectContext projectA, SimpleTestProjectContext projectB, ITestOutputHelper testOutputHelper)
{
var settings = Settings.LoadDefaultSettings(Path.GetDirectoryName(pathContext.SolutionRoot), Path.GetFileName(pathContext.NuGetConfig), null);
var packageSourceProvider = new PackageSourceProvider(settings);
static async Task RestoreProjectsAsync(SimpleTestPathContext pathContext, SimpleTestProjectContext projectA, SimpleTestProjectContext projectB, ITestOutputHelper testOutputHelper)
{
var settings = Settings.LoadDefaultSettings(Path.GetDirectoryName(pathContext.SolutionRoot), Path.GetFileName(pathContext.NuGetConfig), null);
var packageSourceProvider = new PackageSourceProvider(settings);

var sources = packageSourceProvider.LoadPackageSources();
var sources = packageSourceProvider.LoadPackageSources();

await RestoreProjectAsync(settings, pathContext, projectA, sources, testOutputHelper);
await RestoreProjectAsync(settings, pathContext, projectB, sources, testOutputHelper);
}
await RestoreProjectAsync(settings, pathContext, projectA, sources, testOutputHelper);
await RestoreProjectAsync(settings, pathContext, projectB, sources, testOutputHelper);

static async Task RestoreProjectAsync(ISettings settings,
SimpleTestPathContext pathContext,
Expand All @@ -290,6 +290,63 @@ static async Task RestoreProjectAsync(ISettings settings,
}
}

[InlineData(true)]
[InlineData(false)]
[Theory]
public async Task CanListPackagesForProjectsInSolutions(bool useSlnx)
{
// Arrange
using var pathContext = new SimpleTestPathContext();

var packageA100 = new SimpleTestPackageContext("A", "1.0.0");
var packageB100 = new SimpleTestPackageContext("B", "1.0.0");

await SimpleTestPackageUtility.CreatePackagesAsync(
pathContext.PackageSource,
packageA100,
packageB100);

var projectA = SimpleTestProjectContext.CreateNETCore("ProjectA", pathContext.SolutionRoot, "net6.0");
var projectB = SimpleTestProjectContext.CreateNETCore("ProjectB", pathContext.SolutionRoot, "net6.0");

projectA.AddPackageToAllFrameworks(packageA100);
projectB.AddPackageToAllFrameworks(packageB100);

var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot, useSlnx);
solution.Projects.Add(projectA);
solution.Projects.Add(projectB);
solution.Create(pathContext.SolutionRoot);

using var mockServer = new FileSystemBackedV3MockServer(pathContext.PackageSource, isPrivateFeed: true);
mockServer.Start();
pathContext.Settings.AddSource(sourceName: "private-source", sourceUri: mockServer.ServiceIndexUri, allowInsecureConnectionsValue: bool.TrueString);

// List package command requires restore to be run before it can list packages.
await RestoreProjectsAsync(pathContext, projectA, projectB, _testOutputHelper);

var output = new StringBuilder();
var error = new StringBuilder();
using TextWriter consoleOut = new StringWriter(output);
using TextWriter consoleError = new StringWriter(error);
var logger = new TestLogger(_testOutputHelper);
ListPackageCommandRunner listPackageCommandRunner = new();
var packageRefArgs = new ListPackageArgs(
path: solution.SolutionPath,
packageSources: [new(mockServer.ServiceIndexUri)],
frameworks: ["net6.0"],
reportType: ReportType.Vulnerable,
renderer: new ListPackageConsoleRenderer(consoleOut, consoleError),
includeTransitive: false,
prerelease: false,
highestPatch: false,
highestMinor: false,
logger: logger,
cancellationToken: CancellationToken.None);

int result = await listPackageCommandRunner.ExecuteCommandAsync(packageRefArgs);
Assert.True(result == 0, userMessage: logger.ShowMessages());
}

private void VerifyCommand(Action<string, Mock<IListPackageCommandRunner>, CommandLineApplication, Func<LogLevel>> verify)
{
// Arrange
Expand Down
Loading
Loading