Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3d40dcc
Support file-based apps in place of projects in the xplat CLI
jjonescz Feb 17, 2026
90bdcb5
Add package reference
jjonescz Feb 18, 2026
3435c86
Pass project collection to the API
jjonescz Feb 18, 2026
9cc867e
Let SDK provide the API to avoid a circular dependency
jjonescz Feb 24, 2026
284c974
Load dotnet.dll
jjonescz Feb 24, 2026
170dfeb
Move interface to its own file
jjonescz Feb 24, 2026
d730f85
Add a comment
jjonescz Feb 24, 2026
68fc9e7
Add a parameter for project content file
jjonescz Feb 25, 2026
09bec45
Merge branch 'dev' into 14390-fbp-2
jjonescz Feb 27, 2026
dea79fe
Avoid hard-coding .cs extension
jjonescz Feb 27, 2026
3f5eb66
Use correct MSBuild
jjonescz Feb 27, 2026
5115070
Use API again instead of a command-line option
jjonescz Mar 6, 2026
d97bad3
Merge branch 'dev' into 14390-fbp-2
jjonescz Mar 6, 2026
0e72883
Fixup after merge
jjonescz Mar 6, 2026
0f8f395
Improve code
jjonescz Mar 13, 2026
bff2206
Avoid using IVirtualProjectBuilder singletons
jjonescz Mar 13, 2026
4dba4eb
Merge branch 'dev' into 14390-fbp-2
jjonescz Mar 13, 2026
ca21710
Improve reflection
jjonescz Mar 13, 2026
db37ef1
Remove unnecessary check
jjonescz Mar 18, 2026
92020d5
Use SDK-provided project path
jjonescz Mar 18, 2026
7eecf67
Add public Program.Run API
jjonescz Mar 19, 2026
54cc213
Add SaveProject API
jjonescz Mar 19, 2026
e4aef8c
Handle Why command API
jjonescz Mar 19, 2026
dde5c33
Add unit tests
jjonescz Mar 19, 2026
b4fa7b1
Update help texts
jjonescz Mar 19, 2026
44944b2
Fixup a test
jjonescz Mar 19, 2026
2fc9af8
Improve code
jjonescz Mar 20, 2026
ea57406
Fixup restore option passed to msbuild
jjonescz Mar 20, 2026
335a3a8
Link a tracking issue
jjonescz Mar 24, 2026
97908b6
Simplify unit test utility
jjonescz Mar 25, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ namespace NuGet.CommandLine.XPlat.Commands.Package.Update;

internal static class PackageUpdateCommand
{
internal static void Register(Command packageCommand, Option<bool> interactiveOption)
internal static void Register(Command packageCommand, Option<bool> interactiveOption, IVirtualProjectBuilder? virtualProjectBuilder = null)
{
Register(packageCommand, interactiveOption, PackageUpdateCommandRunner.Run);
Register(packageCommand, interactiveOption, (args, ct) => PackageUpdateCommandRunner.Run(args, virtualProjectBuilder, ct));
}

internal static void Register(Command packageCommand, Option<bool> interactiveOption, Func<PackageUpdateArgs, CancellationToken, Task<int>> action)
Expand All @@ -32,7 +32,7 @@ internal static void Register(Command packageCommand, Option<bool> interactiveOp
};
command.Arguments.Add(packagesArguments);

var projectOption = new Option<FileSystemInfo>("--project").AcceptExistingOnly();
var projectOption = new Option<FileSystemInfo>("--project", "--file").AcceptExistingOnly();
Comment thread
nkolev92 marked this conversation as resolved.
projectOption.Description = Strings.PackageUpdateCommand_ProjectOptionDescription;
command.Options.Add(projectOption);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ namespace NuGet.CommandLine.XPlat.Commands.Package.Update;
internal static class PackageUpdateCommandRunner
{
// This overload sets static state, so should not be used in tests.
internal static Task<int> Run(PackageUpdateArgs args, CancellationToken cancellationToken)
internal static Task<int> Run(PackageUpdateArgs args, IVirtualProjectBuilder? virtualProjectBuilder, CancellationToken cancellationToken)
{
ILoggerWithColor logger = new CommandOutputLogger(args.LogLevel)
{
Expand All @@ -39,7 +39,7 @@ internal static Task<int> Run(PackageUpdateArgs args, CancellationToken cancella
// MSBuildAPIUtility's output is different to what we want for package update.
// While it would probably be a good idea to align the output of all commands using MSBuildAPIUtility,
// in order to meet deadlines, we'll suppress its output, and leave improvements for later.
MSBuildAPIUtility msBuild = new(NullLogger.Instance);
MSBuildAPIUtility msBuild = new(NullLogger.Instance, virtualProjectBuilder);

var restoreHelper = new PackageUpdateIO(args.Project, msBuild, EnvironmentVariableWrapper.Instance);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ public void Dispose()

DependencyGraphSpec result = DependencyGraphSpec.Load(tempFile);

// Fixup virtual project paths.
if (_msbuildUtility.VirtualProjectBuilder?.GetVirtualProjectPath(project) is { } virtualProjectPath)
{
foreach (var packageSpec in result.Projects)
Comment thread
jjonescz marked this conversation as resolved.
{
if (packageSpec.FilePath == virtualProjectPath)
{
packageSpec.FilePath = project;
Comment thread
nkolev92 marked this conversation as resolved.
}
}
}

return result;
}
finally
Expand All @@ -86,20 +98,25 @@ bool RunMsbuildTarget(string project, string tempFile)
// But when NuGet.CommandLine.XPlat is being called directly, call dotnet on the path, so this code is debuggable.
string dotnetPath = _environmentVariableReader.GetEnvironmentVariable("DOTNET_HOST_PATH") ?? "dotnet";

bool isFileBasedApp = _msbuildUtility.VirtualProjectBuilder?.IsValidEntryPointPath(project) == true;

// don't redirect stdout or stderr, so errors are output. But use quiet verbosity, so that success has no output.
ProcessStartInfo processStartInfo = new ProcessStartInfo(dotnetPath)
{
Arguments = $"msbuild " +
Arguments = (isFileBasedApp ? "build " : "msbuild ") +
$"\"{project}\" " +
$"-restore:false " +
$"-target:GenerateRestoreGraphFile " +
(isFileBasedApp ? "--no-restore " : "-restore:false ") +
"-target:GenerateRestoreGraphFile " +
$"-property:RestoreGraphOutputPath=\"{tempFile}\" " +
$"-property:RestoreRecursive=false " +
$"-nologo " +
$"-verbosity:quiet " +
$"-tl:false " +
$"-noautoresponse",
"-property:RestoreRecursive=false " +
"-nologo " +
"-verbosity:quiet " +
(!isFileBasedApp ? $"-noautoresponse" : null), // currently not supported for file-based apps
UseShellExecute = false,
Environment =
{
{ "MSBUILDTERMINALLOGGER", "off" },
},
};

using var process = Process.Start(processStartInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ namespace NuGet.CommandLine.XPlat
internal static class AddPackageReferenceCommand
{
public static void Register(CommandLineApplication app, Func<ILogger> getLogger,
Func<IPackageReferenceCommandRunner> getCommandRunner)
Func<IPackageReferenceCommandRunner> getCommandRunner,
Func<IVirtualProjectBuilder?>? getVirtualProjectBuilder = null)
{
app.Command("add", addpkg =>
{
Expand Down Expand Up @@ -79,9 +80,11 @@ public static void Register(CommandLineApplication app, Func<ILogger> getLogger,

addpkg.OnExecute(() =>
{
var virtualProjectBuilder = getVirtualProjectBuilder?.Invoke();

ValidateArgument(id, addpkg.Name);
ValidateArgument(projectPath, addpkg.Name);
ValidateProjectPath(projectPath, addpkg.Name);
ValidateProjectPath(projectPath, addpkg.Name, virtualProjectBuilder);
if (!noRestore.HasValue())
{
ValidateArgument(dgFilePath, addpkg.Name);
Expand All @@ -103,7 +106,7 @@ public static void Register(CommandLineApplication app, Func<ILogger> getLogger,
PackageVersion = packageVersion,
PackageId = id.Values[0]
};
var msBuild = new MSBuildAPIUtility(logger);
var msBuild = new MSBuildAPIUtility(logger, virtualProjectBuilder);

X509TrustStore.InitializeForDotNetSdk(logger);

Expand Down Expand Up @@ -132,9 +135,11 @@ private static void ValidateArgument(CommandOption arg, string commandName)
}
}

private static void ValidateProjectPath(CommandOption projectPath, string commandName)
private static void ValidateProjectPath(CommandOption projectPath, string commandName, IVirtualProjectBuilder? virtualProjectBuilder)
{
if (!File.Exists(projectPath.Value()) || !projectPath.Value().EndsWith("proj", StringComparison.OrdinalIgnoreCase))
if (!File.Exists(projectPath.Value())
|| (!projectPath.Value().EndsWith("proj", StringComparison.OrdinalIgnoreCase)
&& virtualProjectBuilder?.IsValidEntryPointPath(projectPath.Value()) != true))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
Strings.Error_PkgMissingOrInvalidProjectFile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ public async Task<int> ExecuteCommand(PackageReferenceArgs packageReferenceArgs,

var projectFullPath = Path.GetFullPath(packageReferenceArgs.ProjectPath);

if (msBuild.VirtualProjectBuilder?.IsValidEntryPointPath(projectFullPath) == true)
{
projectFullPath = msBuild.VirtualProjectBuilder.GetVirtualProjectPath(projectFullPath);
}

var matchingPackageSpecs = dgSpec
.Projects
.Where(p => p.RestoreMetadata.ProjectStyle == ProjectStyle.PackageReference &&
Expand All @@ -104,7 +109,7 @@ public async Task<int> ExecuteCommand(PackageReferenceArgs packageReferenceArgs,
var originalPackageSpec = matchingPackageSpecs.FirstOrDefault();

// Check if the project files are correct for CPM
if (originalPackageSpec.RestoreMetadata.CentralPackageVersionsEnabled && !MSBuildAPIUtility.AreCentralVersionRequirementsSatisfied(packageReferenceArgs, originalPackageSpec))
if (originalPackageSpec.RestoreMetadata.CentralPackageVersionsEnabled && !msBuild.AreCentralVersionRequirementsSatisfied(packageReferenceArgs, originalPackageSpec))
{
return 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private string GetReportParameters()

if (HighestPatch)
{
sb.Append("--highest-patch");
sb.Append(" --highest-patch");
}

return sb.ToString().Trim();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public static void Register(
isDeprecated: deprecatedReport.HasValue(),
isVulnerable: vulnerableReport.HasValue());

IReportRenderer reportRenderer = GetOutputType(outputFormat.Value(), outputVersionOption: outputVersion.Value());
IReportRenderer reportRenderer = GetOutputType(app.Out, app.Error, outputFormat.Value(), outputVersionOption: outputVersion.Value());
var provider = new PackageSourceProvider(settings);
var packageRefArgs = new ListPackageArgs(
path.Value,
Expand Down Expand Up @@ -171,7 +171,7 @@ private static ReportType GetReportType(bool isDeprecated, bool isOutdated, bool
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_InvalidOptions));
}

private static IReportRenderer GetOutputType(string outputFormatOption, string outputVersionOption)
private static IReportRenderer GetOutputType(TextWriter consoleOut, TextWriter consoleError, string outputFormatOption, string outputVersionOption)
Comment thread
nkolev92 marked this conversation as resolved.
{
ReportOutputFormat outputFormat = ReportOutputFormat.Console;
if (!string.IsNullOrEmpty(outputFormatOption) &&
Expand All @@ -187,7 +187,7 @@ private static IReportRenderer GetOutputType(string outputFormatOption, string o
{
throw new ArgumentException(string.Format(Strings.ListPkg_OutputVersionNotApplicable));
}
return new ListPackageConsoleRenderer();
return new ListPackageConsoleRenderer(consoleOut, consoleError);
}

IReportRenderer jsonReportRenderer;
Expand All @@ -200,7 +200,7 @@ private static IReportRenderer GetOutputType(string outputFormatOption, string o
}
else
{
jsonReportRenderer = new ListPackageJsonRenderer();
jsonReportRenderer = new ListPackageJsonRenderer(consoleOut);
}

return jsonReportRenderer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ internal class ListPackageCommandRunner : IListPackageCommandRunner
private const string ProjectName = "MSBuildProjectName";
private const int GenericSuccessExitCode = 0;
private const int GenericFailureExitCode = 1;
private Dictionary<PackageSource, SourceRepository> _sourceRepositoryCache;
private readonly MSBuildAPIUtility _msbuildUtility;
private readonly Dictionary<PackageSource, SourceRepository> _sourceRepositoryCache;

public ListPackageCommandRunner()
public ListPackageCommandRunner(MSBuildAPIUtility msbuildUtility)
{
_msbuildUtility = msbuildUtility;
_sourceRepositoryCache = new Dictionary<PackageSource, SourceRepository>();
}

Expand Down Expand Up @@ -71,11 +73,9 @@ public async Task<int> ExecuteCommandAsync(ListPackageArgs listPackageArgs)
? MSBuildAPIUtility.GetProjectsFromSolution(listPackageArgs.Path).Where(File.Exists)
: [listPackageArgs.Path];

MSBuildAPIUtility msBuild = listPackageReportModel.MSBuildAPIUtility;

foreach (string projectPath in projectsPaths)
{
await GetProjectMetadataAsync(projectPath, listPackageReportModel, msBuild, listPackageArgs);
await GetProjectMetadataAsync(projectPath, listPackageReportModel, listPackageArgs);
}

// if there is any error then return failure code.
Expand All @@ -90,12 +90,11 @@ public async Task<int> ExecuteCommandAsync(ListPackageArgs listPackageArgs)
private async Task GetProjectMetadataAsync(
string projectPath,
ListPackageReportModel listPackageReportModel,
MSBuildAPIUtility msBuild,
ListPackageArgs listPackageArgs)
{
//Open project to evaluate properties for the assets
//file and the name of the project
Project project = MSBuildAPIUtility.GetProject(projectPath);
Project project = _msbuildUtility.GetProject(projectPath).Project;
var projectName = project.GetPropertyValue(ProjectName);
ListPackageProjectModel projectModel = listPackageReportModel.CreateProjectReportData(projectPath: projectPath, projectName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ namespace NuGet.CommandLine.XPlat
internal class RemovePackageReferenceCommand
{
public static void Register(CommandLineApplication app, Func<ILogger> getLogger,
Func<IPackageReferenceCommandRunner> getCommandRunner)
Func<IPackageReferenceCommandRunner> getCommandRunner,
Func<IVirtualProjectBuilder?>? getVirtualProjectBuilder = null)
{
app.Command("remove", removePkg =>
{
Expand Down Expand Up @@ -43,16 +44,18 @@ public static void Register(CommandLineApplication app, Func<ILogger> getLogger,

removePkg.OnExecute(() =>
{
var virtualProjectBuilder = getVirtualProjectBuilder?.Invoke();

ValidateArgument(id, removePkg.Name);
ValidateArgument(projectPath, removePkg.Name);
ValidateProjectPath(projectPath, removePkg.Name);
ValidateProjectPath(projectPath, removePkg.Name, virtualProjectBuilder);
var logger = getLogger();
var packageRefArgs = new PackageReferenceArgs(projectPath.Value(), logger)
{
Interactive = interactive.HasValue(),
PackageId = id.Value()
};
var msBuild = new MSBuildAPIUtility(logger);
var msBuild = new MSBuildAPIUtility(logger, virtualProjectBuilder);
var removePackageRefCommandRunner = getCommandRunner();
return removePackageRefCommandRunner.ExecuteCommand(packageRefArgs, msBuild);
});
Expand All @@ -69,9 +72,11 @@ private static void ValidateArgument(CommandOption arg, string commandName)
}
}

private static void ValidateProjectPath(CommandOption projectPath, string commandName)
private static void ValidateProjectPath(CommandOption projectPath, string commandName, IVirtualProjectBuilder? virtualProjectBuilder)
{
if (!File.Exists(projectPath.Value()) || !projectPath.Value().EndsWith("proj", StringComparison.OrdinalIgnoreCase))
if (!File.Exists(projectPath.Value())
|| (!projectPath.Value().EndsWith("proj", StringComparison.OrdinalIgnoreCase)
&& virtualProjectBuilder?.IsValidEntryPointPath(projectPath.Value()) != true))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
Strings.Error_PkgMissingOrInvalidProjectFile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.CommandLineUtils;
using NuGet.Common;
using Spectre.Console;

namespace NuGet.CommandLine.XPlat.Commands.Why
Expand All @@ -25,21 +26,34 @@ internal static void Register(CommandLineApplication app)
});
}

internal static void Register(Command rootCommand, Lazy<IAnsiConsole> console)
internal static void Register(Command rootCommand, Lazy<IAnsiConsole> console, IVirtualProjectBuilder? virtualProjectBuilder = null)
{
Register(rootCommand, console, WhyCommandRunner.ExecuteCommand);
Register(rootCommand, console,
() => new WhyCommandRunner(new MSBuildAPIUtility(NullLogger.Instance, virtualProjectBuilder)));
}

/// <summary>
/// This is a temporary API until NuGet migrates all our commands to System.CommandLine, at which time I suspect we'll have a NuGetParser.GetNuGetCommand for all the `dotnet nuget *` commands.
/// For now, this allows the dotnet CLI to invoke why directly, instead of running NuGet.CommandLine.XPlat as a child process.
/// </summary>
/// <param name="rootCommand">The <c>dotnet nuget</c> command handler, to add <c>why</c> to.</param>
public static void GetWhyCommand(Command rootCommand)
/// <param name="virtualProjectBuilder">For handling file-based apps.</param>
public static void GetWhyCommand(Command rootCommand, IVirtualProjectBuilder? virtualProjectBuilder = null)
Comment thread
nkolev92 marked this conversation as resolved.
{
Register(rootCommand,
new Lazy<IAnsiConsole>(() => Spectre.Console.AnsiConsole.Console),
WhyCommandRunner.ExecuteCommand);
virtualProjectBuilder);
}

// For binary backcompat. To delete once the SDK starts using the other overload.
public static void GetWhyCommand(Command rootCommand)
{
GetWhyCommand(rootCommand, virtualProjectBuilder: null);
}

internal static void Register(Command rootCommand, Lazy<IAnsiConsole> console, Func<WhyCommandRunner> getCommandRunner)
{
Register(rootCommand, console, action: (args) => getCommandRunner().ExecuteCommand(args));
}

// console must be lazy, because Spectre.Console's AnsiConsole will send VT sequences to the output
Expand All @@ -48,7 +62,7 @@ internal static void Register(Command rootCommand, Lazy<IAnsiConsole> console, F
{
var whyCommand = new DocumentedCommand("why", Strings.WhyCommand_Description, "https://aka.ms/dotnet/nuget/why");

Argument<string> path = new Argument<string>("PROJECT|SOLUTION")
Argument<string> path = new Argument<string>("PROJECT|SOLUTION|FILE")
{
Description = Strings.WhyCommand_PathArgument_Description,
// We really want this to be zero or one, however, because this is the first argument, it doesn't work.
Expand Down
Loading