diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateCommand.cs index abced3c62f8..9be528df51d 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateCommand.cs @@ -15,9 +15,9 @@ namespace NuGet.CommandLine.XPlat.Commands.Package.Update; internal static class PackageUpdateCommand { - internal static void Register(Command packageCommand, Option interactiveOption) + internal static void Register(Command packageCommand, Option 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 interactiveOption, Func> action) @@ -32,7 +32,7 @@ internal static void Register(Command packageCommand, Option interactiveOp }; command.Arguments.Add(packagesArguments); - var projectOption = new Option("--project").AcceptExistingOnly(); + var projectOption = new Option("--project", "--file").AcceptExistingOnly(); projectOption.Description = Strings.PackageUpdateCommand_ProjectOptionDescription; command.Options.Add(projectOption); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateCommandRunner.cs index e50a0fcbcd0..48afa9ec3ca 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateCommandRunner.cs @@ -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 Run(PackageUpdateArgs args, CancellationToken cancellationToken) + internal static Task Run(PackageUpdateArgs args, IVirtualProjectBuilder? virtualProjectBuilder, CancellationToken cancellationToken) { ILoggerWithColor logger = new CommandOutputLogger(args.LogLevel) { @@ -39,7 +39,7 @@ internal static Task 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); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateIO.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateIO.cs index 6c36395514e..43b9b3a1b38 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateIO.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Update/PackageUpdateIO.cs @@ -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) + { + if (packageSpec.FilePath == virtualProjectPath) + { + packageSpec.FilePath = project; + } + } + } + return result; } finally @@ -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); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommand.cs index 722c3730b46..5824f320029 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommand.cs @@ -15,7 +15,8 @@ namespace NuGet.CommandLine.XPlat internal static class AddPackageReferenceCommand { public static void Register(CommandLineApplication app, Func getLogger, - Func getCommandRunner) + Func getCommandRunner, + Func? getVirtualProjectBuilder = null) { app.Command("add", addpkg => { @@ -79,9 +80,11 @@ public static void Register(CommandLineApplication app, Func 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); @@ -103,7 +106,7 @@ public static void Register(CommandLineApplication app, Func getLogger, PackageVersion = packageVersion, PackageId = id.Values[0] }; - var msBuild = new MSBuildAPIUtility(logger); + var msBuild = new MSBuildAPIUtility(logger, virtualProjectBuilder); X509TrustStore.InitializeForDotNetSdk(logger); @@ -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, diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommandRunner.cs index 276b95f7fb6..6b65c17df21 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommandRunner.cs @@ -78,6 +78,11 @@ public async Task 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 && @@ -104,7 +109,7 @@ public async Task 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; } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageArgs.cs index 2a5af5043d3..28b7f03cb7f 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageArgs.cs @@ -119,7 +119,7 @@ private string GetReportParameters() if (HighestPatch) { - sb.Append("--highest-patch"); + sb.Append(" --highest-patch"); } return sb.ToString().Trim(); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommand.cs index 3e2cd993a4a..3d37371dc42 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommand.cs @@ -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, @@ -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) { ReportOutputFormat outputFormat = ReportOutputFormat.Console; if (!string.IsNullOrEmpty(outputFormatOption) && @@ -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; @@ -200,7 +200,7 @@ private static IReportRenderer GetOutputType(string outputFormatOption, string o } else { - jsonReportRenderer = new ListPackageJsonRenderer(); + jsonReportRenderer = new ListPackageJsonRenderer(consoleOut); } return jsonReportRenderer; diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs index 67d61cdd668..08b6b396769 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs @@ -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 _sourceRepositoryCache; + private readonly MSBuildAPIUtility _msbuildUtility; + private readonly Dictionary _sourceRepositoryCache; - public ListPackageCommandRunner() + public ListPackageCommandRunner(MSBuildAPIUtility msbuildUtility) { + _msbuildUtility = msbuildUtility; _sourceRepositoryCache = new Dictionary(); } @@ -71,11 +73,9 @@ public async Task 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. @@ -90,12 +90,11 @@ public async Task 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); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/RemovePackageReferenceCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/RemovePackageReferenceCommand.cs index 17820be96bb..1d52ac7c25e 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/RemovePackageReferenceCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/RemovePackageReferenceCommand.cs @@ -14,7 +14,8 @@ namespace NuGet.CommandLine.XPlat internal class RemovePackageReferenceCommand { public static void Register(CommandLineApplication app, Func getLogger, - Func getCommandRunner) + Func getCommandRunner, + Func? getVirtualProjectBuilder = null) { app.Command("remove", removePkg => { @@ -43,16 +44,18 @@ public static void Register(CommandLineApplication app, Func 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); }); @@ -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, diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs index a3e8fe6f2c4..8beb68d88ab 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs @@ -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 @@ -25,9 +26,10 @@ internal static void Register(CommandLineApplication app) }); } - internal static void Register(Command rootCommand, Lazy console) + internal static void Register(Command rootCommand, Lazy console, IVirtualProjectBuilder? virtualProjectBuilder = null) { - Register(rootCommand, console, WhyCommandRunner.ExecuteCommand); + Register(rootCommand, console, + () => new WhyCommandRunner(new MSBuildAPIUtility(NullLogger.Instance, virtualProjectBuilder))); } /// @@ -35,11 +37,23 @@ internal static void Register(Command rootCommand, Lazy console) /// For now, this allows the dotnet CLI to invoke why directly, instead of running NuGet.CommandLine.XPlat as a child process. /// /// The dotnet nuget command handler, to add why to. - public static void GetWhyCommand(Command rootCommand) + /// For handling file-based apps. + public static void GetWhyCommand(Command rootCommand, IVirtualProjectBuilder? virtualProjectBuilder = null) { Register(rootCommand, new Lazy(() => 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 console, Func 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 @@ -48,7 +62,7 @@ internal static void Register(Command rootCommand, Lazy console, F { var whyCommand = new DocumentedCommand("why", Strings.WhyCommand_Description, "https://aka.ms/dotnet/nuget/why"); - Argument path = new Argument("PROJECT|SOLUTION") + Argument path = new Argument("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. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs index 950329cfffd..948fe8de8d6 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs @@ -14,15 +14,22 @@ namespace NuGet.CommandLine.XPlat.Commands.Why { - internal static class WhyCommandRunner + internal class WhyCommandRunner { private const string ProjectAssetsFile = "ProjectAssetsFile"; + private readonly MSBuildAPIUtility _msbuildUtility; + + public WhyCommandRunner(MSBuildAPIUtility msbuildUtility) + { + _msbuildUtility = msbuildUtility; + } + /// /// Executes the 'why' command. /// /// CLI arguments for the 'why' command. - public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) + public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) { bool validArgumentsUsed = ValidatePathArgument(whyCommandArgs.Path, whyCommandArgs.Logger) && ValidatePackageArgument(whyCommandArgs.Package, whyCommandArgs.Logger); @@ -90,7 +97,7 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) return Task.FromResult(anyErrors ? ExitCodes.Error : ExitCodes.Success); } - private static IEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFiles(string path, IAnsiConsole logger) + private IEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFiles(string path, IAnsiConsole logger) { if (XPlatUtility.IsJsonFile(path)) { @@ -98,10 +105,10 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) yield break; } - var projectPaths = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(path); + var projectPaths = _msbuildUtility.GetListOfProjectsFromPathArgument(path); foreach (string projectPath in projectPaths.NoAllocEnumerate()) { - Project project = MSBuildAPIUtility.GetProject(projectPath); + Project project = _msbuildUtility.GetProject(projectPath).Project; try { string usingNetSdk = project.GetPropertyValue("UsingMicrosoftNETSdk"); @@ -149,7 +156,7 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) /// /// Validates that the input 'path' argument is a valid path to a directory, solution file or project file. /// - private static bool ValidatePathArgument(string path, IAnsiConsole logger) + private bool ValidatePathArgument(string path, IAnsiConsole logger) { if (string.IsNullOrEmpty(path)) { @@ -177,10 +184,11 @@ private static bool ValidatePathArgument(string path, IAnsiConsole logger) return false; } - // Check that the path is a directory, solution file or project file + // Check that the path is a directory, solution file, project file, or a file-based app. if (Directory.Exists(fullPath) || (File.Exists(fullPath) - && (XPlatUtility.IsSolutionFile(fullPath) || XPlatUtility.IsProjectFile(fullPath) || XPlatUtility.IsJsonFile(fullPath)))) + && (XPlatUtility.IsSolutionFile(fullPath) || XPlatUtility.IsProjectFile(fullPath) || XPlatUtility.IsJsonFile(fullPath) || + _msbuildUtility.VirtualProjectBuilder?.IsValidEntryPointPath(fullPath) == true))) { return true; } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/GlobalSuppressions.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/GlobalSuppressions.cs index bf13bb7ce39..9c80ebf159e 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/GlobalSuppressions.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/GlobalSuppressions.cs @@ -8,7 +8,7 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'void AddPackageReferenceCommand.Register(CommandLineApplication app, Func getLogger, Func getCommandRunner)', validate parameter 'app' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.AddPackageReferenceCommand.Register(Microsoft.Extensions.CommandLineUtils.CommandLineApplication,System.Func{NuGet.Common.ILogger},System.Func{NuGet.CommandLine.XPlat.IPackageReferenceCommandRunner})")] +[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'void AddPackageReferenceCommand.Register(CommandLineApplication app, Func getLogger, Func getCommandRunner)', validate parameter 'app' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.AddPackageReferenceCommand.Register(Microsoft.Extensions.CommandLineUtils.CommandLineApplication,System.Func{NuGet.Common.ILogger},System.Func{NuGet.CommandLine.XPlat.IPackageReferenceCommandRunner},System.Func{NuGet.CommandLine.XPlat.IVirtualProjectBuilder})")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'Task AddPackageReferenceCommandRunner.ExecuteCommand(PackageReferenceArgs packageReferenceArgs, MSBuildAPIUtility msBuild)', validate parameter 'msBuild' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.AddPackageReferenceCommandRunner.ExecuteCommand(NuGet.CommandLine.XPlat.PackageReferenceArgs,NuGet.CommandLine.XPlat.MSBuildAPIUtility)~System.Threading.Tasks.Task{System.Int32}")] [assembly: SuppressMessage("Build", "CA1308:In method 'LogInternal', replace the call to 'ToLowerInvariant' with 'ToUpperInvariant'.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.CommandOutputLogger.LogInternal(NuGet.Common.LogLevel,System.String)")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'void CommandOutputLogger.LogInternal(LogLevel logLevel, string message)', validate parameter 'message' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.CommandOutputLogger.LogInternal(NuGet.Common.LogLevel,System.String)")] @@ -16,11 +16,11 @@ [assembly: SuppressMessage("Build", "CA1822:Member MeetsConstraints does not access instance data and can be marked as static (Shared in VisualBasic)", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.ListPackageCommandRunner.MeetsConstraints(NuGet.Versioning.NuGetVersion,NuGet.CommandLine.XPlat.InstalledPackageReference,NuGet.CommandLine.XPlat.ListPackageArgs)~System.Boolean")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'void MSBuildAPIUtility.AddPackageReferencePerTFM(string projectPath, LibraryDependency libraryDependency, IEnumerable frameworks, bool noVersion)', validate parameter 'libraryDependency' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.MSBuildAPIUtility.AddPackageReferencePerTFM(System.String,NuGet.LibraryModel.LibraryDependency,System.Collections.Generic.IEnumerable{System.String},System.Boolean)")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'int MSBuildAPIUtility.RemovePackageReference(string projectPath, LibraryDependency libraryDependency)', validate parameter 'libraryDependency' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.MSBuildAPIUtility.RemovePackageReference(System.String,NuGet.LibraryModel.LibraryDependency)~System.Int32")] -[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'int Program.MainInternal(string[] args, CommandOutputLogger log, NuGet.Common.IEnvironmentVariableReader)', validate parameter 'log' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.Program.MainInternal(System.String[],NuGet.CommandLine.XPlat.CommandOutputLogger,NuGet.Common.IEnvironmentVariableReader)~System.Int32")] -[assembly: SuppressMessage("Build", "CA1308:In method 'MainInternal', replace the call to 'ToLowerInvariant' with 'ToUpperInvariant'.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.Program.MainInternal(System.String[],NuGet.CommandLine.XPlat.CommandOutputLogger,NuGet.Common.IEnvironmentVariableReader)~System.Int32")] -[assembly: SuppressMessage("Build", "CA1031:Modify 'MainInternal' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.Program.MainInternal(System.String[],NuGet.CommandLine.XPlat.CommandOutputLogger,NuGet.Common.IEnvironmentVariableReader)~System.Int32")] -[assembly: SuppressMessage("Build", "CA1303:Method 'int Program.MainInternal(string[] args, CommandOutputLogger log)' passes a literal string as parameter 'value' of a call to 'void Console.WriteLine(string value)'. Retrieve the following string(s) from a resource table instead: \"Waiting for debugger to attach.\".", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.Program.MainInternal(System.String[],NuGet.CommandLine.XPlat.CommandOutputLogger,NuGet.Common.IEnvironmentVariableReader)~System.Int32")] -[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'void RemovePackageReferenceCommand.Register(CommandLineApplication app, Func getLogger, Func getCommandRunner)', validate parameter 'app' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.RemovePackageReferenceCommand.Register(Microsoft.Extensions.CommandLineUtils.CommandLineApplication,System.Func{NuGet.Common.ILogger},System.Func{NuGet.CommandLine.XPlat.IPackageReferenceCommandRunner})")] +[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'int Program.MainInternal(string[] args, CommandOutputLogger log, NuGet.Common.IEnvironmentVariableReader)', validate parameter 'log' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.Program.MainInternal(System.String[],NuGet.CommandLine.XPlat.CommandOutputLogger,NuGet.Common.IEnvironmentVariableReader,NuGet.CommandLine.XPlat.IVirtualProjectBuilder)~System.Int32")] +[assembly: SuppressMessage("Build", "CA1308:In method 'MainInternal', replace the call to 'ToLowerInvariant' with 'ToUpperInvariant'.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.Program.MainInternal(System.String[],NuGet.CommandLine.XPlat.CommandOutputLogger,NuGet.Common.IEnvironmentVariableReader,NuGet.CommandLine.XPlat.IVirtualProjectBuilder)~System.Int32")] +[assembly: SuppressMessage("Build", "CA1031:Modify 'MainInternal' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.Program.MainInternal(System.String[],NuGet.CommandLine.XPlat.CommandOutputLogger,NuGet.Common.IEnvironmentVariableReader,NuGet.CommandLine.XPlat.IVirtualProjectBuilder)~System.Int32")] +[assembly: SuppressMessage("Build", "CA1303:Method 'int Program.MainInternal(string[] args, CommandOutputLogger log)' passes a literal string as parameter 'value' of a call to 'void Console.WriteLine(string value)'. Retrieve the following string(s) from a resource table instead: \"Waiting for debugger to attach.\".", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.Program.MainInternal(System.String[],NuGet.CommandLine.XPlat.CommandOutputLogger,NuGet.Common.IEnvironmentVariableReader,NuGet.CommandLine.XPlat.IVirtualProjectBuilder)~System.Int32")] +[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'void RemovePackageReferenceCommand.Register(CommandLineApplication app, Func getLogger, Func getCommandRunner)', validate parameter 'app' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.RemovePackageReferenceCommand.Register(Microsoft.Extensions.CommandLineUtils.CommandLineApplication,System.Func{NuGet.Common.ILogger},System.Func{NuGet.CommandLine.XPlat.IPackageReferenceCommandRunner},System.Func{NuGet.CommandLine.XPlat.IVirtualProjectBuilder})")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'Task RemovePackageReferenceCommandRunner.ExecuteCommand(PackageReferenceArgs packageReferenceArgs, MSBuildAPIUtility msBuild)', validate parameter 'msBuild' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.RemovePackageReferenceCommandRunner.ExecuteCommand(NuGet.CommandLine.XPlat.PackageReferenceArgs,NuGet.CommandLine.XPlat.MSBuildAPIUtility)~System.Threading.Tasks.Task{System.Int32}")] [assembly: SuppressMessage("Build", "CA1819:Properties should not return arrays", Justification = "", Scope = "member", Target = "~P:NuGet.CommandLine.XPlat.PackageReferenceArgs.Frameworks")] [assembly: SuppressMessage("Build", "CA1819:Properties should not return arrays", Justification = "", Scope = "member", Target = "~P:NuGet.CommandLine.XPlat.PackageReferenceArgs.Sources")] @@ -36,5 +36,5 @@ [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.PackageSearchArgs.VerifyInt(System.String,System.Int32,System.String)~System.Int32")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.Table.SanitizeString(System.String)~System.String")] [assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.ListPackage.ListPackageConsoleRenderer.GetProjectHeader(System.String,NuGet.CommandLine.XPlat.ListPackageArgs)~System.String")] -[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.ListPackageCommand.GetOutputType(System.String,System.String)~NuGet.CommandLine.XPlat.ListPackage.IReportRenderer")] +[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.ListPackageCommand.GetOutputType(System.IO.TextWriter,System.IO.TextWriter,System.String,System.String)~NuGet.CommandLine.XPlat.ListPackage.IReportRenderer")] [assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.UILanguageOverride.SetIfNotAlreadySet(System.String,System.Int32,NuGet.Common.IEnvironmentVariableReader)")] diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/IVirtualProjectBuilder.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/IVirtualProjectBuilder.cs new file mode 100644 index 00000000000..f5657a2599d --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/IVirtualProjectBuilder.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; + +namespace NuGet.CommandLine.XPlat; + +/// +/// We cannot have a dependency on a package from SDK due to source build, +/// hence we invert the relationship and define the interface here, +/// SDK implements it and we load the implementation dynamically. +/// +public interface IVirtualProjectBuilder +{ + /// + /// Whether the given file path can be a file-based app. + /// + /// + /// Currently, files that exist and have the .cs file extension or #! (shebang) are valid file-based apps. + /// + bool IsValidEntryPointPath(string entryPointFilePath); + + /// + /// Returns the virtual project path (e.g., app.csproj) corresponding to the given entry point file + /// (e.g., app.cs). The returned path is used by MSBuild for DG specs and property evaluation. + /// + string GetVirtualProjectPath(string entryPointFilePath); + + ProjectRootElement CreateProjectRootElement(string entryPointFilePath, ProjectCollection projectCollection); + + void SaveProject(string entryPointFilePath, ProjectRootElement projectRootElement); +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageReportModel.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageReportModel.cs index 59ce9f486d0..927a0e0a88e 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageReportModel.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageReportModel.cs @@ -15,7 +15,6 @@ internal class ListPackageReportModel { internal ListPackageArgs ListPackageArgs { get; } internal List Projects { get; } = new(); - internal MSBuildAPIUtility MSBuildAPIUtility { get; } internal HashSet AuditSourcesUsed { get; set; } = new HashSet(); private ListPackageReportModel() @@ -24,7 +23,6 @@ private ListPackageReportModel() internal ListPackageReportModel(ListPackageArgs listPackageArgs) { ListPackageArgs = listPackageArgs; - MSBuildAPIUtility = new MSBuildAPIUtility(listPackageArgs.Logger); } internal ListPackageProjectModel CreateProjectReportData(string projectPath, string projectName) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs index c35fa9352a2..ce87fab16f1 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs @@ -21,9 +21,10 @@ public static class NuGetCommands /// /// The CLI's RootCommand instance /// The .NET SDK has code to detect when output is redirected or + /// For handling file-based apps. /// Many of NuGet's commands are defined in the dotnet/sdk repo, and those run NuGet.CommandLine.XPlat.dll as a child process. /// Those commands are not added by this method. - public static void Add(RootCommand rootCommand, Option interactiveOption) + public static void Add(RootCommand rootCommand, Option interactiveOption, IVirtualProjectBuilder? virtualProjectBuilder = null) { var packageCommand = rootCommand.Subcommands.FirstOrDefault(c => c.Name == "package"); if (packageCommand is null) @@ -32,11 +33,17 @@ public static void Add(RootCommand rootCommand, Option interactiveOption) rootCommand.Subcommands.Add(packageCommand); } - PackageUpdateCommand.Register(packageCommand, interactiveOption); + PackageUpdateCommand.Register(packageCommand, interactiveOption, virtualProjectBuilder); PackageDownloadCommand.Register(packageCommand, interactiveOption); } - // To delete once the SDK starts using the other overload. Joys of public APIs. + // For binary backcompat. To delete once the SDK starts using the first overload. + public static void Add(RootCommand rootCommand, Option interactiveOption) + { + Add(rootCommand, interactiveOption, virtualProjectBuilder: null); + } + + // To delete once the SDK starts using the first overload. Joys of public APIs. public static void Add(RootCommand rootCommand) { var interactiveOption = new Option("--interactive") diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index 38b201fe4fc..72c9a12c00c 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -20,7 +20,7 @@ namespace NuGet.CommandLine.XPlat { - internal class Program + public static class Program { #if DEBUG private const string DebugOption = "--debug"; @@ -30,16 +30,28 @@ internal class Program private const int DotnetPackageSearchTimeOut = 15; - public static int Main(string[] args) + internal static int Main(string[] args) + { + return MainInternal(args, virtualProjectBuilder: null); + } + +#nullable enable + public static int Run(string[] args, IVirtualProjectBuilder virtualProjectBuilder) + { + return MainInternal(args, virtualProjectBuilder); + } + + private static int MainInternal(string[] args, IVirtualProjectBuilder? virtualProjectBuilder) { var log = new CommandOutputLogger(LogLevel.Information); - return MainInternal(args, log, EnvironmentVariableWrapper.Instance); + return MainInternal(args, log, EnvironmentVariableWrapper.Instance, virtualProjectBuilder); } +#nullable disable /// /// Internal Main. This is used for testing. /// - public static int MainInternal(string[] args, CommandOutputLogger log, IEnvironmentVariableReader environmentVariableReader) + internal static int MainInternal(string[] args, CommandOutputLogger log, IEnvironmentVariableReader environmentVariableReader, IVirtualProjectBuilder virtualProjectBuilder = null) { #if USEMSBUILDLOCATOR try @@ -106,7 +118,7 @@ public static int MainInternal(string[] args, CommandOutputLogger log, IEnvironm PackageSearchCommand.Register(packageCommand, getHidePrefixLogger); #if DEBUG - PackageUpdateCommand.Register(packageCommand, interactiveOption); + PackageUpdateCommand.Register(packageCommand, interactiveOption, virtualProjectBuilder); PackageDownloadCommand.Register(packageCommand, interactiveOption); #endif } @@ -119,8 +131,8 @@ public static int MainInternal(string[] args, CommandOutputLogger log, IEnvironm ConfigCommand.Register(nugetCommand, getHidePrefixLogger); ConfigCommand.Register(rootCommand, getHidePrefixLogger); - Commands.Why.WhyCommand.Register(nugetCommand, lazyConsole); - Commands.Why.WhyCommand.Register(rootCommand, lazyConsole); + Commands.Why.WhyCommand.Register(nugetCommand, lazyConsole, virtualProjectBuilder); + Commands.Why.WhyCommand.Register(rootCommand, lazyConsole, virtualProjectBuilder); } CancellationTokenSource tokenSource = new CancellationTokenSource(); @@ -141,7 +153,7 @@ public static int MainInternal(string[] args, CommandOutputLogger log, IEnvironm return exitCodeValue; } - var app = InitializeApp(args, log); + var app = InitializeApp(args, log, virtualProjectBuilder); // Remove the correct item in array for "package" commands. Only do this when "add package", "remove package", etc... are being run. if (app.Name == DotnetPackageAppName) @@ -289,7 +301,7 @@ internal static void LogException(Exception e, ILogger log) log.LogVerbose(e.ToString()); } - private static CommandLineApplication InitializeApp(string[] args, CommandOutputLogger log) + private static CommandLineApplication InitializeApp(string[] args, CommandOutputLogger log, IVirtualProjectBuilder virtualProjectBuilder) { // Many commands don't want prefixes output. Use this func instead of () => log to set the HidePrefix property first. Func getHidePrefixLogger = () => @@ -302,14 +314,15 @@ private static CommandLineApplication InitializeApp(string[] args, CommandOutput Action setLogLevel = (logLevel) => log.VerbosityLevel = logLevel; var app = new CommandLineApplication(); + var msbuild = new MSBuildAPIUtility(log, virtualProjectBuilder); if (args.Any() && args[0] == "package") { // "dotnet * package" commands app.Name = DotnetPackageAppName; - AddPackageReferenceCommand.Register(app, () => log, () => new AddPackageReferenceCommandRunner()); - RemovePackageReferenceCommand.Register(app, () => log, () => new RemovePackageReferenceCommandRunner()); - ListPackageCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new ListPackageCommandRunner()); + AddPackageReferenceCommand.Register(app, () => log, () => new AddPackageReferenceCommandRunner(), () => msbuild.VirtualProjectBuilder); + RemovePackageReferenceCommand.Register(app, () => log, () => new RemovePackageReferenceCommandRunner(), () => msbuild.VirtualProjectBuilder); + ListPackageCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new ListPackageCommandRunner(msbuild)); } else { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.CommandLine.XPlat/PublicAPI.Unshipped.txt index 7dc5c58110b..094ff890e12 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/PublicAPI.Unshipped.txt +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/PublicAPI.Unshipped.txt @@ -1 +1,10 @@ #nullable enable +NuGet.CommandLine.XPlat.IVirtualProjectBuilder +NuGet.CommandLine.XPlat.IVirtualProjectBuilder.CreateProjectRootElement(string! entryPointFilePath, Microsoft.Build.Evaluation.ProjectCollection! projectCollection) -> Microsoft.Build.Construction.ProjectRootElement! +NuGet.CommandLine.XPlat.IVirtualProjectBuilder.GetVirtualProjectPath(string! entryPointFilePath) -> string! +NuGet.CommandLine.XPlat.IVirtualProjectBuilder.IsValidEntryPointPath(string! entryPointFilePath) -> bool +NuGet.CommandLine.XPlat.IVirtualProjectBuilder.SaveProject(string! entryPointFilePath, Microsoft.Build.Construction.ProjectRootElement! projectRootElement) -> void +NuGet.CommandLine.XPlat.Program +static NuGet.CommandLine.XPlat.Commands.Why.WhyCommand.GetWhyCommand(System.CommandLine.Command! rootCommand, NuGet.CommandLine.XPlat.IVirtualProjectBuilder? virtualProjectBuilder = null) -> void +static NuGet.CommandLine.XPlat.NuGetCommands.Add(System.CommandLine.RootCommand! rootCommand, System.CommandLine.Option! interactiveOption, NuGet.CommandLine.XPlat.IVirtualProjectBuilder? virtualProjectBuilder = null) -> void +static NuGet.CommandLine.XPlat.Program.Run(string![]! args, NuGet.CommandLine.XPlat.IVirtualProjectBuilder! virtualProjectBuilder) -> int diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index e75f8cf3580..2e5ae2a77d7 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -773,7 +773,7 @@ internal static string Error_PackageSourceMappingNotFound { } /// - /// Looks up a localized string similar to Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory.. + /// Looks up a localized string similar to Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory.. /// internal static string Error_PathIsMissingOrInvalid { get { @@ -1860,7 +1860,7 @@ internal static string PackageUpdateCommand_Description { } /// - /// Looks up a localized string similar to Path to a project or solution file, or a directory.. + /// Looks up a localized string similar to Path to a project or solution file or file-based app, or a project directory.. /// internal static string PackageUpdateCommand_ProjectOptionDescription { get { @@ -2782,7 +2782,7 @@ internal static string WhyCommand_PackageArgument_Description { } /// - /// Looks up a localized string similar to A path to a project, solution file, or directory.. + /// Looks up a localized string similar to A path to a project, solution file, file-based app, or project directory.. /// internal static string WhyCommand_PathArgument_Description { get { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index a2eeb57f791..2cdfc6d6ae0 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -922,7 +922,7 @@ NuGet requires HTTPS sources. To use HTTP sources, you must explicitly set 'allo Shows the dependency graph for a particular package for a given project or solution. - A path to a project, solution file, or directory. + A path to a project, solution file, file-based app, or project directory. The package name to lookup in the dependency graph. @@ -935,7 +935,7 @@ NuGet requires HTTPS sources. To use HTTP sources, you must explicitly set 'allo {0} - Argument that was not provided - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. {0} - Input path argument @@ -1003,7 +1003,7 @@ Do not translate "PackageVersion" Update referenced packages in a project or solution. - Path to a project or solution file, or a directory. + Path to a project or solution file or file-based app, or a project directory. The package {0} is already referencing the highest version {1} in project {2} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs index 8bc4e08a27c..a23df054e14 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs @@ -5,12 +5,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; -using System.Threading; using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; @@ -46,24 +46,26 @@ internal class MSBuildAPIUtility public ILogger Logger { get; } - public MSBuildAPIUtility(ILogger logger) + public IVirtualProjectBuilder VirtualProjectBuilder { get; } + + public MSBuildAPIUtility(ILogger logger, IVirtualProjectBuilder virtualProjectBuilder = null) { Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + VirtualProjectBuilder = virtualProjectBuilder; } /// /// Opens an MSBuild.Evaluation.Project type from a csproj file. /// /// CSProj file which needs to be evaluated - /// MSBuild.Evaluation.Project - internal static Project GetProject(string projectCSProjPath) + internal SaveableProject GetProject(string projectCSProjPath) { - var projectRootElement = TryOpenProjectRootElement(projectCSProjPath); - if (projectCSProjPath == null) + var (projectRootElement, isVirtual) = TryOpenProjectRootElement(projectCSProjPath); + if (projectRootElement is null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_MsBuildUnableToOpenProject, projectCSProjPath)); } - return new Project(projectRootElement); + return new SaveableProject { Project = new Project(projectRootElement), VirtualProject = isVirtual ? (projectCSProjPath, VirtualProjectBuilder) : null }; } /// @@ -71,15 +73,14 @@ internal static Project GetProject(string projectCSProjPath) /// /// CSProj file which needs to be evaluated /// Global properties that should be used to evaluate the project while opening. - /// MSBuild.Evaluation.Project - private static Project GetProject(string projectCSProjPath, IDictionary globalProperties) + private SaveableProject GetProject(string projectCSProjPath, IDictionary globalProperties) { - var projectRootElement = TryOpenProjectRootElement(projectCSProjPath); - if (projectCSProjPath == null) + var (projectRootElement, isVirtual) = TryOpenProjectRootElement(projectCSProjPath); + if (projectRootElement is null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_MsBuildUnableToOpenProject, projectCSProjPath)); } - return new Project(projectRootElement, globalProperties, toolsVersion: null); + return new SaveableProject { Project = new Project(projectRootElement, globalProperties, toolsVersion: null), VirtualProject = isVirtual ? (projectCSProjPath, VirtualProjectBuilder) : null }; } private static bool IsCentralPackageManagementEnabled(Project project) @@ -118,10 +119,15 @@ internal static IEnumerable GetProjectsFromSolution(string solutionPath) /// /// List of project paths. Returns null if path was a directory with none or multiple project/solution files. /// Throws an exception if the directory has none or multiple project/solution files. - internal static IEnumerable GetListOfProjectsFromPathArgument(string path, CancellationToken cancellationToken = default) + internal IEnumerable GetListOfProjectsFromPathArgument(string path) { string fullPath = Path.GetFullPath(path); + if (VirtualProjectBuilder?.IsValidEntryPointPath(fullPath) == true) + { + return [fullPath]; + } + string projectOrSolutionFile; // the path points to a directory @@ -157,7 +163,7 @@ public int RemovePackageReference(string projectPath, LibraryDependency libraryD { var project = GetProject(projectPath); - var existingPackageReferences = project.ItemsIgnoringCondition + var existingPackageReferences = project.Project.ItemsIgnoringCondition .Where(item => item.ItemType.Equals(PACKAGE_REFERENCE_TYPE_TAG, StringComparison.OrdinalIgnoreCase) && item.EvaluatedInclude.Equals(libraryDependency.Name, StringComparison.OrdinalIgnoreCase)); @@ -167,9 +173,9 @@ public int RemovePackageReference(string projectPath, LibraryDependency libraryD // If it does then we throw a user friendly exception without making any changes ValidateNoImportedItemsAreUpdated(existingPackageReferences, libraryDependency, REMOVE_OPERATION); - project.RemoveItems(existingPackageReferences); + project.Project.RemoveItems(existingPackageReferences); project.Save(); - ProjectCollection.GlobalProjectCollection.UnloadProject(project); + ProjectCollection.GlobalProjectCollection.UnloadProject(project.Project); return 0; } @@ -177,10 +183,10 @@ public int RemovePackageReference(string projectPath, LibraryDependency libraryD { Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_UpdatePkgNoSuchPackage, - project.FullPath, + project.Project.FullPath, libraryDependency.Name, REMOVE_OPERATION)); - ProjectCollection.GlobalProjectCollection.UnloadProject(project); + ProjectCollection.GlobalProjectCollection.UnloadProject(project.Project); return 1; } @@ -192,9 +198,9 @@ public int RemovePackageReference(string projectPath, LibraryDependency libraryD /// Arguments used in the command /// /// - public static bool AreCentralVersionRequirementsSatisfied(PackageReferenceArgs packageReferenceArgs, PackageSpec packageSpec) + public bool AreCentralVersionRequirementsSatisfied(PackageReferenceArgs packageReferenceArgs, PackageSpec packageSpec) { - var project = GetProject(packageReferenceArgs.ProjectPath); + var project = GetProject(packageReferenceArgs.ProjectPath).Project; string directoryPackagesPropsPath = project.GetPropertyValue(DirectoryPackagesPropsPathPropertyName); // Get VersionOverride if it exisits in the package reference. @@ -286,7 +292,7 @@ public void AddPackageReference(string projectPath, LibraryDependency libraryDep var existingPackageReferences = GetPackageReferencesForAllFrameworks(project, libraryDependency); AddPackageReference(project, libraryDependency, existingPackageReferences, noVersion); - ProjectCollection.GlobalProjectCollection.UnloadProject(project); + ProjectCollection.GlobalProjectCollection.UnloadProject(project.Project); } /// @@ -304,9 +310,9 @@ public void AddPackageReferencePerTFM(string projectPath, LibraryDependency libr var globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "TargetFramework", framework } }; var project = GetProject(projectPath, globalProperties); - var existingPackageReferences = GetPackageReferences(project, libraryDependency); + var existingPackageReferences = GetPackageReferences(project.Project, libraryDependency); AddPackageReference(project, libraryDependency, existingPackageReferences, noVersion, framework); - ProjectCollection.GlobalProjectCollection.UnloadProject(project); + ProjectCollection.GlobalProjectCollection.UnloadProject(project.Project); } } @@ -318,14 +324,14 @@ public void AddPackageReferencePerTFM(string projectPath, LibraryDependency libr /// Package references that already exist in the project. /// If a version is passed in as a CLI argument. /// Target Framework for which the package reference should be added. - private void AddPackageReference(Project project, + private void AddPackageReference(SaveableProject project, LibraryDependency libraryDependency, IEnumerable existingPackageReferences, bool noVersion, string framework = null) { // Determine CPM status from the loaded project so callers don't need to check separately. - bool isCentralPackageManagementEnabled = IsCentralPackageManagementEnabled(project); + bool isCentralPackageManagementEnabled = IsCentralPackageManagementEnabled(project.Project); // Add packageReference to the project file only if it does not exist. if (!isCentralPackageManagementEnabled) @@ -333,7 +339,7 @@ private void AddPackageReference(Project project, if (!existingPackageReferences.Any()) { //Modify the project file. - ProjectItemGroupElement itemGroup = GetOrCreateItemGroup(framework, project); + ProjectItemGroupElement itemGroup = GetOrCreateItemGroup(framework, project.Project); AddPackageReferenceIntoItemGroup(itemGroup, libraryDependency); } else @@ -345,14 +351,14 @@ private void AddPackageReference(Project project, else { // Get package version and VersionOverride if it already exists in the props file. Returns null if there is no matching package version. - ProjectItem packageReferenceInProps = project.Items.LastOrDefault(i => i.ItemType == PACKAGE_REFERENCE_TYPE_TAG && i.EvaluatedInclude.Equals(libraryDependency.Name)); + ProjectItem packageReferenceInProps = project.Project.Items.LastOrDefault(i => i.ItemType == PACKAGE_REFERENCE_TYPE_TAG && i.EvaluatedInclude.Equals(libraryDependency.Name)); var versionOverrideExists = packageReferenceInProps?.Metadata.FirstOrDefault(i => i.Name.Equals("VersionOverride") && !string.IsNullOrWhiteSpace(i.EvaluatedValue)); if (!existingPackageReferences.Any()) { //Add to the project file. - ProjectItemGroupElement itemGroup = GetOrCreateItemGroup(framework, project); - AddPackageReferenceIntoItemGroupCPM(project, itemGroup, libraryDependency); + ProjectItemGroupElement itemGroup = GetOrCreateItemGroup(framework, project.Project); + AddPackageReferenceIntoItemGroupCPM(project.Project, itemGroup, libraryDependency); } if (versionOverrideExists != null) @@ -364,13 +370,13 @@ private void AddPackageReference(Project project, else { // Get package version if it already exists in the props file. Returns null if there is no matching package version. - ProjectItem packageVersionInProps = project.Items.LastOrDefault(i => i.ItemType == PACKAGE_VERSION_TYPE_TAG && i.EvaluatedInclude.Equals(libraryDependency.Name)); + ProjectItem packageVersionInProps = project.Project.Items.LastOrDefault(i => i.ItemType == PACKAGE_VERSION_TYPE_TAG && i.EvaluatedInclude.Equals(libraryDependency.Name)); // If no exists in the Directory.Packages.props file. if (packageVersionInProps == null) { // Modifying the props file if project is onboarded to CPM. - AddPackageVersionIntoItemGroupCPM(project, libraryDependency); + AddPackageVersionIntoItemGroupCPM(project.Project, libraryDependency); } else { @@ -412,6 +418,7 @@ private void AddPackageVersionIntoItemGroupCPM(Project project, LibraryDependenc AddPackageVersionIntoPropsItemGroup(propsItemGroup, libraryDependency); // Save the updated props file. + Debug.Assert(directoryBuildPropsRootElement.ContainingProject.FullPath != project.FullPath); directoryBuildPropsRootElement.Save(); } @@ -605,17 +612,17 @@ private void UpdatePackageReferenceItems(IEnumerable packageReferen /// /// /// - internal static void UpdateVersionOverride(Project project, ProjectItem packageReference, string versionCLIArgument) + internal static void UpdateVersionOverride(SaveableProject project, ProjectItem packageReference, string versionCLIArgument) { // Determine where the item is decalred - ProjectItemElement packageReferenceItemElement = project.GetItemProvenance(packageReference).LastOrDefault()?.ItemElement; + ProjectItemElement packageReferenceItemElement = project.Project.GetItemProvenance(packageReference).LastOrDefault()?.ItemElement; // Get the Version attribute on the packageVersionItemElement. ProjectMetadataElement versionOverrideAttribute = packageReferenceItemElement.Metadata.FirstOrDefault(i => i.Name.Equals("VersionOverride")); // Update the version versionOverrideAttribute.Value = versionCLIArgument; - packageReferenceItemElement.ContainingProject.Save(); + project.Save(packageReferenceItemElement.ContainingProject); } /// @@ -624,16 +631,16 @@ internal static void UpdateVersionOverride(Project project, ProjectItem packageR /// /// item with a matching package ID. /// Version that is passed in as a CLI argument. - internal static void UpdatePackageVersion(Project project, ProjectItem packageVersion, string versionCLIArgument) + internal static void UpdatePackageVersion(SaveableProject project, ProjectItem packageVersion, string versionCLIArgument) { // Determine where the item is decalred - ProjectItemElement packageVersionItemElement = project.GetItemProvenance(packageVersion).LastOrDefault()?.ItemElement; + ProjectItemElement packageVersionItemElement = project.Project.GetItemProvenance(packageVersion).LastOrDefault()?.ItemElement; // Get the Version attribute on the packageVersionItemElement. ProjectMetadataElement versionAttribute = packageVersionItemElement.Metadata.FirstOrDefault(i => i.Name.Equals("Version", StringComparison.OrdinalIgnoreCase)); // Update the version versionAttribute.Value = versionCLIArgument; - packageVersionItemElement.ContainingProject.Save(); + project.Save(packageVersionItemElement.ContainingProject); } /// @@ -734,7 +741,6 @@ internal static List GetResolvedVersions( throw new ArgumentNullException(nameof(assetsFile)); } - var projectPath = project.FullPath; var resultPackages = new List(); var requestedTargetFrameworks = assetsFile.PackageSpec.TargetFrameworks; var requestedTargets = assetsFile.Targets; @@ -794,7 +800,7 @@ internal static List GetResolvedVersions( //The packages for the framework that were retrieved with GetRequestedVersions var frameworkDependencies = tfmInformation.Dependencies; var targetAlias = tfmInformation.TargetAlias; - var projectPackages = GetPackageReferencesFromTargets(projectPath, targetAlias); + var projectPackages = GetPackageReferencesFromTargets(project, targetAlias); var topLevelPackages = new List(); var transitivePackages = new List(); @@ -821,7 +827,7 @@ internal static List GetResolvedVersions( } catch (Exception) { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingReferenceFromProject, projectPath)); + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingReferenceFromProject, project.FullPath)); } } else @@ -914,18 +920,18 @@ private static IEnumerable GetPackageReferences(Project project, Li /// /// Returns all package references after invoking the target CollectPackageReferences. /// - /// Path to the project for which the package references have to be obtained. + /// The project for which the package references have to be obtained. /// Framework to get reference(s) for /// List of Items containing the package reference for the package. /// If the libraryDependency is null then it returns all package references - private static IEnumerable GetPackageReferencesFromTargets(string projectPath, string framework) + private static IEnumerable GetPackageReferencesFromTargets(Project project, string framework) { var globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "TargetFramework", framework }, { "ExcludeRestorePackageImports", bool.TrueString } }; - var newProject = new ProjectInstance(projectPath, globalProperties, null); + var newProject = new ProjectInstance(project.Xml, globalProperties, null, ProjectCollection.GlobalProjectCollection); newProject.Build(new[] { CollectPackageReferences, CollectCentralPackageVersions }, new List { }, out var targetOutputs); // Find the first target output that matches `CollectPackageReferences` @@ -982,12 +988,12 @@ private static IEnumerable GetPackageReferencesFromTa /// Framework to get reference(s) for /// List of Items containing the package reference for the package. /// If the libraryDependency is null then it returns all package references - private static IEnumerable GetPackageReferencesPerFramework(Project project, + private static IEnumerable GetPackageReferencesPerFramework(SaveableProject project, string libraryName, string framework) { var globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "TargetFramework", framework } }; - var projectPerFramework = GetProject(project.FullPath, globalProperties); + var projectPerFramework = project.WithGlobalProperties(globalProperties).Project; var packages = GetPackageReferences(projectPerFramework, libraryName); ProjectCollection.GlobalProjectCollection.UnloadProject(projectPerFramework); @@ -1004,7 +1010,7 @@ private static IEnumerable GetPackageReferencesPerFramework(Project /// Specific framework to look at /// List of Items containing the package reference for the package. /// If the libraryDependency is null then it returns all package references - private static IEnumerable GetPackageReferencesPerFramework(Project project, + private static IEnumerable GetPackageReferencesPerFramework(SaveableProject project, LibraryDependency libraryDependency, string framework) { return GetPackageReferencesPerFramework(project, libraryDependency.Name, framework); @@ -1019,10 +1025,10 @@ private static IEnumerable GetPackageReferencesPerFramework(Project /// Dependency of the package. /// List of Items containing the package reference for the package. /// If the libraryDependency is null then it returns all package reference - private static IEnumerable GetPackageReferencesForAllFrameworks(Project project, + private static IEnumerable GetPackageReferencesForAllFrameworks(SaveableProject project, LibraryDependency libraryDependency) { - var frameworks = GetProjectFrameworks(project); + var frameworks = GetProjectFrameworks(project.Project); var mergedPackageReferences = new List(); foreach (var framework in frameworks) @@ -1052,17 +1058,24 @@ private static IEnumerable GetProjectFrameworks(Project project) return frameworks; } - private static ProjectRootElement TryOpenProjectRootElement(string filename) + private (ProjectRootElement, bool isVirtual) TryOpenProjectRootElement(string filename) { try { + if (VirtualProjectBuilder?.IsValidEntryPointPath(filename) == true) + { + var fullPath = Path.GetFullPath(filename); + var element = VirtualProjectBuilder.CreateProjectRootElement(fullPath, ProjectCollection.GlobalProjectCollection); + return (element, true); + } + // There is ProjectRootElement.TryOpen but it does not work as expected // I.e. it returns null for some valid projects - return ProjectRootElement.Open(filename, ProjectCollection.GlobalProjectCollection, preserveFormatting: true); + return (ProjectRootElement.Open(filename, ProjectCollection.GlobalProjectCollection, preserveFormatting: true), false); } catch (Microsoft.Build.Exceptions.InvalidProjectFileException) { - return null; + return (null, false); } } @@ -1071,4 +1084,44 @@ private static string GetTargetFrameworkCondition(string targetFramework) return string.Format(CultureInfo.CurrentCulture, "'$(TargetFramework)' == '{0}'", targetFramework); } } + +#nullable enable + internal readonly struct SaveableProject + { + public required Project Project { get; init; } + + /// + /// Set when this project represents a virtual project (e.g., a file-based app). + /// + public (string EntryPointFilePath, IVirtualProjectBuilder Builder)? VirtualProject { get; init; } + + public void Save() + { + if (VirtualProject is { } virtualProject) + { + virtualProject.Builder.SaveProject(virtualProject.EntryPointFilePath, Project.Xml); + } + else + { + Project.Save(); + } + } + + public void Save(ProjectRootElement projectRootElement) + { + if (projectRootElement == Project.Xml) + { + Save(); + } + else + { + projectRootElement.Save(); + } + } + + public SaveableProject WithGlobalProperties(IDictionary globalProperties) + { + return this with { Project = new Project(Project.Xml, globalProperties, toolsVersion: null) }; + } + } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf index 00b0debb426..a838959ba1a 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf @@ -409,8 +409,8 @@ NuGet vyžaduje zdroje HTTPS. Pokud chcete používat zdroje HTTP, musíte v sou {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - Chybějící nebo neplatná cesta {0}. Zadejte prosím cestu k projektu, souboru řešení nebo adresáři. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + Chybějící nebo neplatná cesta {0}. Zadejte prosím cestu k projektu, souboru řešení nebo adresáři. {0} - Input path argument @@ -972,8 +972,8 @@ Další informace najdete tady: https://docs.nuget.org/docs/reference/command-li - Path to a project or solution file, or a directory. - Cesta k souboru projektu nebo řešení nebo k adresáři + Path to a project or solution file or file-based app, or a project directory. + Cesta k souboru projektu nebo řešení nebo k adresáři @@ -1522,8 +1522,8 @@ Hledání porovnává řetězce bez rozlišování malých a velkých písmen po - A path to a project, solution file, or directory. - Cesta k projektu, souboru řešení nebo adresáři + A path to a project, solution file, file-based app, or project directory. + Cesta k projektu, souboru řešení nebo adresáři diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf index 748a42fd5a8..89239ad2a4f 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf @@ -409,8 +409,8 @@ NuGet erfordert HTTPS-Quellen. Um HTTP-Quellen zu verwenden, müssen Sie „allo {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - Fehlende oder ungültige Eingabe "{0}". Geben Sie einen Pfad zu einem Projekt, einer Projektmappendatei oder einem Verzeichnis an. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + Fehlende oder ungültige Eingabe "{0}". Geben Sie einen Pfad zu einem Projekt, einer Projektmappendatei oder einem Verzeichnis an. {0} - Input path argument @@ -972,8 +972,8 @@ Weitere Informationen finden Sie unter: https://docs.nuget.org/docs/reference/co - Path to a project or solution file, or a directory. - Pfad zu einer Projekt- oder Projektmappendatei oder einem Verzeichnis + Path to a project or solution file or file-based app, or a project directory. + Pfad zu einer Projekt- oder Projektmappendatei oder einem Verzeichnis @@ -1522,8 +1522,8 @@ Bei der Suche handelt es sich um einen ungültigen Zeichenfolgenvergleich mit de - A path to a project, solution file, or directory. - Ein Pfad zu einem Projekt, einer Projektmappendatei oder einem Verzeichnis. + A path to a project, solution file, file-based app, or project directory. + Ein Pfad zu einem Projekt, einer Projektmappendatei oder einem Verzeichnis. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf index 62a31a9a445..f025c920026 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf @@ -409,8 +409,8 @@ NuGet requiere orígenes HTTPS. Para usar orígenes HTTP, es necesario establece {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - Falta la ruta de acceso "{0}" o no es válida. Proporcione una ruta de acceso a un proyecto, archivo de solución o directorio. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + Falta la ruta de acceso "{0}" o no es válida. Proporcione una ruta de acceso a un proyecto, archivo de solución o directorio. {0} - Input path argument @@ -972,8 +972,8 @@ Para obtener más información, visite https://docs.nuget.org/docs/reference/com - Path to a project or solution file, or a directory. - Ruta de acceso a un archivo de proyecto o solución, o a un directorio. + Path to a project or solution file or file-based app, or a project directory. + Ruta de acceso a un archivo de proyecto o solución, o a un directorio. @@ -1522,8 +1522,8 @@ La búsqueda es una comparación de cadenas que no diferencia mayúsculas de min - A path to a project, solution file, or directory. - Ruta de acceso a un proyecto, archivo de solución o directorio. + A path to a project, solution file, file-based app, or project directory. + Ruta de acceso a un proyecto, archivo de solución o directorio. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf index 814768824e7..cdf4dd74f34 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf @@ -409,8 +409,8 @@ NuGet nécessite des sources HTTPS. Pour utiliser des sources HTTP, vous devez d {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - Entrée manquante ou incorrecte « {0} ». Veuillez fournir un chemin d’un projet, d’un fichier solution ou d’un répertoire. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + Entrée manquante ou incorrecte « {0} ». Veuillez fournir un chemin d’un projet, d’un fichier solution ou d’un répertoire. {0} - Input path argument @@ -972,8 +972,8 @@ Pour plus d'informations, visitez https://docs.nuget.org/docs/reference/command- - Path to a project or solution file, or a directory. - Chemin d’accès à un fichier de projet ou de solution, ou à un répertoire. + Path to a project or solution file or file-based app, or a project directory. + Chemin d’accès à un fichier de projet ou de solution, ou à un répertoire. @@ -1522,8 +1522,8 @@ La recherche est une comparaison de chaînes qui ne respecte pas la casse à l - A path to a project, solution file, or directory. - Chemin d’un projet, d’un fichier solution ou d’un répertoire. + A path to a project, solution file, file-based app, or project directory. + Chemin d’un projet, d’un fichier solution ou d’un répertoire. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf index 5bb3a44f3c1..5dc5aef0d74 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf @@ -409,8 +409,8 @@ NuGet richiede origini HTTPS. Per utilizzare origini HTTP, è necessario imposta {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - Percorso mancante o non valido: '{0}'. Specificare un percorso di un progetto, di un file di soluzione o una directory. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + Percorso mancante o non valido: '{0}'. Specificare un percorso di un progetto, di un file di soluzione o una directory. {0} - Input path argument @@ -972,8 +972,8 @@ Per altre informazioni, vedere https://docs.nuget.org/docs/reference/command-lin - Path to a project or solution file, or a directory. - Percorso di un progetto, un file di soluzione o una directory. + Path to a project or solution file or file-based app, or a project directory. + Percorso di un progetto, un file di soluzione o una directory. @@ -1522,8 +1522,8 @@ La ricerca viene eseguita in un confronto di stringhe senza distinzione tra maiu - A path to a project, solution file, or directory. - Percorso di un progetto, un file di soluzione o una directory. + A path to a project, solution file, file-based app, or project directory. + Percorso di un progetto, un file di soluzione o una directory. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf index 89bf4e758be..b965870ecb0 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf @@ -409,8 +409,8 @@ NuGet には HTTPS ソースが必要です。HTTP ソースを使用するに {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - パス '{0}' が見つからないか無効です。プロジェクト、ソリューション ファイル、またはディレクトリへのパスを指定してください。 + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + パス '{0}' が見つからないか無効です。プロジェクト、ソリューション ファイル、またはディレクトリへのパスを指定してください。 {0} - Input path argument @@ -972,8 +972,8 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r - Path to a project or solution file, or a directory. - プロジェクトまたはソリューション ファイル、またはディレクトリへのパス。 + Path to a project or solution file or file-based app, or a project directory. + プロジェクトまたはソリューション ファイル、またはディレクトリへのパス。 @@ -1522,8 +1522,8 @@ The search is a case-insensitive string comparison using the supplied value, whi - A path to a project, solution file, or directory. - プロジェクト、プロジェクト ディレクトリ、またはディレクトリへのパスです。 + A path to a project, solution file, file-based app, or project directory. + プロジェクト、プロジェクト ディレクトリ、またはディレクトリへのパスです。 diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf index 473ef09ab73..315016012b9 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf @@ -409,8 +409,8 @@ NuGet에는 HTTPS 원본이 필요합니다. HTTP 원본을 사용하려면 NuGe {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - '{0}' 경로가 없거나 잘못되었습니다. 프로젝트, 솔루션 파일 또는 디렉터리의 경로를 제공하세요. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + '{0}' 경로가 없거나 잘못되었습니다. 프로젝트, 솔루션 파일 또는 디렉터리의 경로를 제공하세요. {0} - Input path argument @@ -972,8 +972,8 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r - Path to a project or solution file, or a directory. - 프로젝트, 솔루션 파일 또는 디렉터리의 경로입니다. + Path to a project or solution file or file-based app, or a project directory. + 프로젝트, 솔루션 파일 또는 디렉터리의 경로입니다. @@ -1523,8 +1523,8 @@ The search is a case-insensitive string comparison using the supplied value, whi - A path to a project, solution file, or directory. - 프로젝트, 솔루션 파일 또는 디렉터리의 경로입니다. + A path to a project, solution file, file-based app, or project directory. + 프로젝트, 솔루션 파일 또는 디렉터리의 경로입니다. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf index 61fc17251e0..8719c9335e7 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf @@ -409,8 +409,8 @@ Menedżer NuGet wymaga źródeł HTTPS. Aby użyć źródeł HTTP, musisz wyraź {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - Brak ścieżki „{0}” lub jest ona nieprawidłowa. Podaj ścieżkę do projektu, pliku rozwiązania lub katalogu. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + Brak ścieżki „{0}” lub jest ona nieprawidłowa. Podaj ścieżkę do projektu, pliku rozwiązania lub katalogu. {0} - Input path argument @@ -972,8 +972,8 @@ Aby uzyskać więcej informacji, odwiedź stronę https://docs.nuget.org/docs/re - Path to a project or solution file, or a directory. - Ścieżka do pliku projektu lub rozwiązania albo katalogu. + Path to a project or solution file or file-based app, or a project directory. + Ścieżka do pliku projektu lub rozwiązania albo katalogu. @@ -1522,8 +1522,8 @@ Wyszukiwanie polega na porównywaniu łańcuchów bez rozróżniania wielkości - A path to a project, solution file, or directory. - Ścieżka do projektu, pliku rozwiązania lub katalogu. + A path to a project, solution file, file-based app, or project directory. + Ścieżka do projektu, pliku rozwiązania lub katalogu. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf index df45cc8578c..437761ed14a 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf @@ -409,8 +409,8 @@ O NuGet requer fontes HTTPS. Para usar fontes HTTP, você deve definir explicita {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - Caminho "{0}" ausente ou inválido. Forneça um caminho para um projeto, arquivo de solução ou diretório. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + Caminho "{0}" ausente ou inválido. Forneça um caminho para um projeto, arquivo de solução ou diretório. {0} - Input path argument @@ -972,8 +972,8 @@ Para obter mais informações, acesse https://docs.nuget.org/docs/reference/comm - Path to a project or solution file, or a directory. - Caminho para um projeto ou arquivo de solução ou um diretório. + Path to a project or solution file or file-based app, or a project directory. + Caminho para um projeto ou arquivo de solução ou um diretório. @@ -1522,8 +1522,8 @@ A pesquisa é uma comparação de cadeia de caracteres que não faz distinção - A path to a project, solution file, or directory. - Um caminho para um projeto, arquivo de solução ou diretório. + A path to a project, solution file, file-based app, or project directory. + Um caminho para um projeto, arquivo de solução ou diretório. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf index 4b12c5359a6..dc1a9d424e9 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf @@ -409,8 +409,8 @@ NuGet requires HTTPS sources. To use HTTP sources, you must explicitly set 'allo {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - Путь "{0}" отсутствует или является недопустимым. Укажите путь к проекту, файлу решения или каталогу. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + Путь "{0}" отсутствует или является недопустимым. Укажите путь к проекту, файлу решения или каталогу. {0} - Input path argument @@ -972,8 +972,8 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r - Path to a project or solution file, or a directory. - Путь к проекту, файлу решения или каталогу. + Path to a project or solution file or file-based app, or a project directory. + Путь к проекту, файлу решения или каталогу. @@ -1522,8 +1522,8 @@ The search is a case-insensitive string comparison using the supplied value, whi - A path to a project, solution file, or directory. - Путь к проекту, файлу решения или каталогу. + A path to a project, solution file, file-based app, or project directory. + Путь к проекту, файлу решения или каталогу. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf index 193499623e6..b9ce9b58a63 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf @@ -410,8 +410,8 @@ NuGet için HTTPS kaynakları gereklidir. HTTP kaynaklarını kullanmak için Nu {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - '{0}' yolu eksik veya geçersiz. Bir projeye, çözüm dosyasına veya dizine giden yolu sağlayın. + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + '{0}' yolu eksik veya geçersiz. Bir projeye, çözüm dosyasına veya dizine giden yolu sağlayın. {0} - Input path argument @@ -973,8 +973,8 @@ Daha fazla bilgi için bkz. https://docs.nuget.org/docs/reference/command-line-r - Path to a project or solution file, or a directory. - Bir proje veya çözüm dosyasına ya da dizine giden yol. + Path to a project or solution file or file-based app, or a project directory. + Bir proje veya çözüm dosyasına ya da dizine giden yol. @@ -1523,8 +1523,8 @@ Arama, sağlanan değeri kullanan büyük/küçük harfe duyarsız bir dize kar - A path to a project, solution file, or directory. - Bir projeye, çözüm dosyasına veya dizine giden yol. + A path to a project, solution file, file-based app, or project directory. + Bir projeye, çözüm dosyasına veya dizine giden yol. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf index 6e757969431..83ad433af1a 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf @@ -409,8 +409,8 @@ NuGet 需要 HTTPS 源。要使用 HTTP 源,必须在 NuGet.Config 文件中 {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - 路径“{0}”缺失或无效。请提供项目、解决方案文件或目录的路径。 + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + 路径“{0}”缺失或无效。请提供项目、解决方案文件或目录的路径。 {0} - Input path argument @@ -972,8 +972,8 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r - Path to a project or solution file, or a directory. - 项目或解决方案文件或目录的路径。 + Path to a project or solution file or file-based app, or a project directory. + 项目或解决方案文件或目录的路径。 @@ -1522,8 +1522,8 @@ The search is a case-insensitive string comparison using the supplied value, whi - A path to a project, solution file, or directory. - 项目、解决方案文件或目录的路径。 + A path to a project, solution file, file-based app, or project directory. + 项目、解决方案文件或目录的路径。 diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf index e879bd170ec..77d908f1487 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf @@ -409,8 +409,8 @@ NuGet 需要 HTTPS 來源。您必須在 NuGet.Config 檔案中將 'allowInsecur {0} - package id - Missing or invalid path '{0}'. Please provide a path to a project, solution file, or directory. - 路徑 '{0}' 遺漏或無效。請提供專案檔、方案檔或目錄的路徑。 + Missing or invalid path '{0}'. Please provide a path to a project, solution file, file-based app, or project directory. + 路徑 '{0}' 遺漏或無效。請提供專案檔、方案檔或目錄的路徑。 {0} - Input path argument @@ -972,8 +972,8 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r - Path to a project or solution file, or a directory. - 專案、解決方案檔案或目錄的路徑。 + Path to a project or solution file or file-based app, or a project directory. + 專案、解決方案檔案或目錄的路徑。 @@ -1522,8 +1522,8 @@ The search is a case-insensitive string comparison using the supplied value, whi - A path to a project, solution file, or directory. - 專案、解決方案檔案或目錄的路徑。 + A path to a project, solution file, file-based app, or project directory. + 專案、解決方案檔案或目錄的路徑。 diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs index 46b3dea1462..cc70be3db0c 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs @@ -7,7 +7,9 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Extensions.CommandLineUtils; using Microsoft.Internal.NuGet.Testing.SignedPackages.ChildProcess; +using NuGet.CommandLine.XPlat; using NuGet.Common; using NuGet.Packaging; using NuGet.Packaging.Core; @@ -18,6 +20,7 @@ using Test.Utility; using Xunit; using Xunit.Abstractions; +using Strings = NuGet.Packaging.Strings; namespace Dotnet.Integration.Test { @@ -76,6 +79,74 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } } + // https://github.com/NuGet/Home/issues/14823: This should use `dotnet package add` when it supports file-based apps. + [Fact] + public async Task AddPkg_FileBasedApp() + { + using var pathContext = _fixture.CreateSimpleTestPathContext(); + + // Create the file-based app. + var fbaDir = Path.Join(pathContext.SolutionRoot, "fba"); + Directory.CreateDirectory(fbaDir); + + var appFile = Path.Join(fbaDir, "app.cs"); + File.WriteAllText(appFile, """ + #:property PublishAot=false + Console.WriteLine(); + """); + + var tempDir = Path.Join(pathContext.WorkingDirectory, "temp"); + Directory.CreateDirectory(tempDir); + + // Generate DG file. + var dgFile = Path.Join(tempDir, "dg.json"); + _fixture.RunDotnetExpectSuccess(fbaDir, $"build app.cs -t:GenerateRestoreGraphFile -p:RestoreGraphOutputPath={ArgumentEscaper.EscapeAndConcatenate([dgFile])}", testOutputHelper: _testOutputHelper); + + // Get project content. + var virtualProject = _fixture.GetFileBasedAppVirtualProject(appFile, _testOutputHelper); + _testOutputHelper.WriteLine("before:\n" + virtualProject.Content); + Assert.DoesNotContain("PackageReference", virtualProject.Content); + using var builder = new TestVirtualProjectBuilder(virtualProject); + + // Create a package. + var packageX = XPlatTestUtils.CreatePackage(); + await SimpleTestPackageUtility.CreateFolderFeedV3Async(pathContext.PackageSource, PackageSaveMode.Defaultv3, packageX); + + // Add the package. + using var outWriter = new StringWriter(); + using var errorWriter = new StringWriter(); + var testApp = new CommandLineApplication + { + Out = outWriter, + Error = errorWriter, + }; + AddPackageReferenceCommand.Register( + testApp, + () => new TestLogger(_testOutputHelper), + () => new AddPackageReferenceCommandRunner(), + () => builder); + int result = testApp.Execute([ + "add", + "--project", appFile, + "--package", "packageX", + "--dg-file", dgFile, + ]); + + var output = outWriter.ToString(); + var error = errorWriter.ToString(); + + _testOutputHelper.WriteLine(output); + _testOutputHelper.WriteLine(error); + + Assert.Equal(0, result); + + Assert.Empty(error); + + var modifiedProjectContent = builder.ModifiedContent; + _testOutputHelper.WriteLine("after:\n" + modifiedProjectContent); + Assert.Contains("""""", modifiedProjectContent); + } + [Fact] public async Task AddPkg_V3LocalSourceFeed_WithRelativePath_NoVersionSpecified_Fail() { diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetIntegrationTestFixture.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetIntegrationTestFixture.cs index 95e549e2b3e..4a020132e62 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetIntegrationTestFixture.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetIntegrationTestFixture.cs @@ -11,8 +11,11 @@ using System.Linq; using System.Reflection; using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading; using FluentAssertions; +using Microsoft.Build.Locator; using Microsoft.Internal.NuGet.Testing.SignedPackages.ChildProcess; using NuGet.Commands; using NuGet.Common; @@ -54,6 +57,9 @@ public DotnetIntegrationTestFixture() SdkDirectory = new DirectoryInfo(sdkPath); MsBuildSdksPath = Path.Combine(sdkPath, "Sdks"); + // https://github.com/NuGet/Home/issues/14823: This can be removed when we migrate to `dotnet.exe`-only integration tests for file-based apps. + MSBuildLocator.RegisterMSBuildPath(sdkPath); + _templateDirectory = new SimpleTestPathContext(); TestDotnetCLiUtility.WriteGlobalJson(_templateDirectory.WorkingDirectory); @@ -154,8 +160,8 @@ private CommandRunnerResult RestoreProjectOrSolution(string workingDirectory, st /// The working directory to use when executing the command. /// The command-line arguments to pass to dotnet. /// An optional containing environment variables to use when executing the command. - internal CommandRunnerResult RunDotnetExpectSuccess(string workingDirectory, string args = "", IReadOnlyDictionary environmentVariables = null, ITestOutputHelper testOutputHelper = null) - => RunDotnet(workingDirectory, args, expectSuccess: true, environmentVariables, testOutputHelper: testOutputHelper); + internal CommandRunnerResult RunDotnetExpectSuccess(string workingDirectory, string args = "", IReadOnlyDictionary environmentVariables = null, ITestOutputHelper testOutputHelper = null, Action inputAction = null) + => RunDotnet(workingDirectory, args, expectSuccess: true, environmentVariables, testOutputHelper, inputAction); /// /// Runs dotnet with the specified arguments and expects the command to fail. If dotnet returns an exit code of zero, an assertion is thrown with diagnostic information. @@ -163,10 +169,10 @@ internal CommandRunnerResult RunDotnetExpectSuccess(string workingDirectory, str /// The working directory to use when executing the command. /// The command-line arguments to pass to dotnet. /// An optional containing environment variables to use when executing the command. - internal CommandRunnerResult RunDotnetExpectFailure(string workingDirectory, string args = "", IReadOnlyDictionary environmentVariables = null, ITestOutputHelper testOutputHelper = null) - => RunDotnet(workingDirectory, args, expectSuccess: false, environmentVariables, testOutputHelper); + internal CommandRunnerResult RunDotnetExpectFailure(string workingDirectory, string args = "", IReadOnlyDictionary environmentVariables = null, ITestOutputHelper testOutputHelper = null, Action inputAction = null) + => RunDotnet(workingDirectory, args, expectSuccess: false, environmentVariables, testOutputHelper, inputAction); - internal CommandRunnerResult RunDotnet(string workingDirectory, string args = "", bool expectSuccess = true, IReadOnlyDictionary environmentVariables = null, ITestOutputHelper testOutputHelper = null) + internal CommandRunnerResult RunDotnet(string workingDirectory, string args = "", bool expectSuccess = true, IReadOnlyDictionary environmentVariables = null, ITestOutputHelper testOutputHelper = null, Action inputAction = null) { bool enableDiagnostics = CIDebug && !string.IsNullOrWhiteSpace(BinLogDirectory); @@ -218,7 +224,7 @@ internal CommandRunnerResult RunDotnet(string workingDirectory, string args = "" { Stopwatch stopwatch = Stopwatch.StartNew(); - result = CommandRunner.Run(TestDotnetCli, workingDirectory, args, environmentVariables: finalEnvironmentVariables, testOutputHelper: testOutputHelper); + result = CommandRunner.Run(TestDotnetCli, workingDirectory, args, inputAction: inputAction, environmentVariables: finalEnvironmentVariables, testOutputHelper: testOutputHelper); stopwatch.Stop(); @@ -355,6 +361,18 @@ private CommandRunnerResult BuildProjectOrSolution(string workingDirectory, stri return RunDotnet(workingDirectory, $"msbuild {file} {args}", expectSuccess, testOutputHelper: testOutputHelper); } + internal (string Content, string ProjectPath, string FilePath) GetFileBasedAppVirtualProject(string entryPointFileFullPath, ITestOutputHelper testOutputHelper) + { + var runApi = RunDotnetExpectSuccess(Path.GetDirectoryName(entryPointFileFullPath), "run-api", testOutputHelper: testOutputHelper, inputAction: writer => + { + writer.Write($$"""{ "$type": "GetProject", "EntryPointFileFullPath": {{JsonSerializer.Serialize(entryPointFileFullPath)}} }"""); + }); + var node = JsonNode.Parse(runApi.AllOutput); + return (Content: node["Content"].GetValue(), + ProjectPath: node["ProjectPath"].GetValue(), + FilePath: entryPointFileFullPath); + } + internal TestDirectory CreateTestDirectory() { var testDirectory = TestDirectory.Create(); diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetListPackageTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetListPackageTests.cs index ad7dd0eeba3..dcbeacf91a4 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetListPackageTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetListPackageTests.cs @@ -12,8 +12,10 @@ using System.Threading.Tasks; using System.Xml.Linq; using FluentAssertions; +using Microsoft.Extensions.CommandLineUtils; using Microsoft.Internal.NuGet.Testing.SignedPackages.ChildProcess; using Newtonsoft.Json.Linq; +using NuGet.CommandLine.XPlat; using NuGet.Common; using NuGet.Configuration; using NuGet.Frameworks; @@ -77,6 +79,70 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } } + // https://github.com/NuGet/Home/issues/14823: This should use `dotnet package list` when it supports file-based apps. + [Fact] + public async Task DotnetListPackage_FileBasedApp() + { + using var pathContext = _fixture.CreateSimpleTestPathContext(); + + // Create the file-based app. + var fbaDir = Path.Join(pathContext.SolutionRoot, "fba"); + Directory.CreateDirectory(fbaDir); + + var appFile = Path.Join(fbaDir, "app.cs"); + File.WriteAllText(appFile, """ + #:property PublishAot=false + #:package packageX@1.0.0 + Console.WriteLine(); + """); + + var packageX = XPlatTestUtils.CreatePackage(); + await SimpleTestPackageUtility.CreateFolderFeedV3Async(pathContext.PackageSource, PackageSaveMode.Defaultv3, packageX); + + // Restore. + _fixture.RunDotnetExpectSuccess(fbaDir, "restore app.cs", testOutputHelper: _testOutputHelper); + + // Get project content. + var virtualProject = _fixture.GetFileBasedAppVirtualProject(appFile, _testOutputHelper); + using var builder = new TestVirtualProjectBuilder(virtualProject); + + // List packages. + using var outWriter = new StringWriter(); + using var errorWriter = new StringWriter(); + var testApp = new CommandLineApplication + { + Out = outWriter, + Error = errorWriter, + }; + var logger = new TestLogger(_testOutputHelper); + var msbuild = new MSBuildAPIUtility(logger, builder); + ListPackageCommand.Register( + testApp, + () => logger, + (_) => { }, + () => new ListPackageCommandRunner(msbuild)); + int result = testApp.Execute([ + "list", appFile, + "--source", pathContext.PackageSource, + "--format", "json", + ]); + + var output = outWriter.ToString(); + var error = errorWriter.ToString(); + + _testOutputHelper.WriteLine(output); + _testOutputHelper.WriteLine(error); + + Assert.Equal(0, result); + + Assert.Empty(error); + + Assert.Contains("packageX", output); + Assert.Contains("1.0.0", output); + + Assert.Null(builder.ModifiedContent); + } + [PlatformFact(Platform.Windows)] public async Task DotnetListPackage_NoRestore_Fail() { diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetPackageUpdateTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetPackageUpdateTests.cs index 0719c913fd9..7def49101f1 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetPackageUpdateTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetPackageUpdateTests.cs @@ -4,6 +4,7 @@ #nullable disable using System.Collections.Generic; +using System.CommandLine; using System.IO; using System.Linq; using System.Text.Json; @@ -12,10 +13,14 @@ using System.Xml.XPath; using FluentAssertions; using NuGet.CommandLine.Xplat.Tests; +using NuGet.CommandLine.XPlat; +using NuGet.CommandLine.XPlat.Commands.Package.Update; using NuGet.Frameworks; using NuGet.ProjectModel; using NuGet.Test.Utility; using NuGet.Versioning; +using NuGet.XPlat.FuncTest; +using Test.Utility; using Xunit; using Xunit.Abstractions; @@ -95,6 +100,69 @@ public async Task SingleTfmProject_PackageVersionUpdated() version.Should().Be("2.0.0"); } + // https://github.com/NuGet/Home/issues/14823: This should use `dotnet package update` when it supports file-based apps. + [Fact] + public async Task FileBasedApp() + { + using var pathContext = _testFixture.CreateSimpleTestPathContext(); + + // Create packages. + var a1 = new SimpleTestPackageContext("NuGet.Internal.Test.a", "1.0.0"); + var a2 = new SimpleTestPackageContext("NuGet.Internal.Test.a", "2.0.0"); + + SimpleTestPackageContext[] packages = [a1, a2]; + await SimpleTestPackageUtility.CreatePackagesAsync(pathContext.PackageSource, packages); + + // Create the file-based app. + var fbaDir = Path.Join(pathContext.SolutionRoot, "fba"); + Directory.CreateDirectory(fbaDir); + + var appFile = Path.Join(fbaDir, "app.cs"); + File.WriteAllText(appFile, """ + #:property PublishAot=false + #:package NuGet.Internal.Test.a@1.0.0 + Console.WriteLine(); + """); + + // Get project content. + var virtualProject = _testFixture.GetFileBasedAppVirtualProject(appFile, _testOutputHelper); + _testOutputHelper.WriteLine("before:\n" + virtualProject.Content); + Assert.Contains("""""", virtualProject.Content); + using var builder = new TestVirtualProjectBuilder(virtualProject); + + // Update the package. + using var outWriter = new StringWriter(); + using var errorWriter = new StringWriter(); + var rootCommand = new RootCommand(); + PackageUpdateCommand.Register( + rootCommand, + new Option("--interactive"), + (args, ct) => + { + var msbuildUtility = new MSBuildAPIUtility(new TestLogger(_testOutputHelper), builder); + var packageUpdateIO = new PackageUpdateIO(args.Project, msbuildUtility, new TestEnvironmentVariableReader(_envVars)); + return PackageUpdateCommandRunner.Run(args, new TestCommandOutputLogger(_testOutputHelper), packageUpdateIO, ct); + }); + int result = rootCommand.Parse([ + "update", + "--project", appFile, + ]).Invoke(new() { Output = outWriter, Error = errorWriter }); + + var output = outWriter.ToString(); + var error = errorWriter.ToString(); + + _testOutputHelper.WriteLine(output); + _testOutputHelper.WriteLine(error); + + Assert.Equal(0, result); + + Assert.Empty(error); + + var modifiedProjectContent = builder.ModifiedContent; + _testOutputHelper.WriteLine("after:\n" + modifiedProjectContent); + Assert.Contains("""""", modifiedProjectContent); + } + [Fact] public async Task SingleTfmCpmProject_PackageVersionUpdated() { diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetRemovePackageTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetRemovePackageTests.cs new file mode 100644 index 00000000000..edb70291fa7 --- /dev/null +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetRemovePackageTests.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.CommandLineUtils; +using NuGet.CommandLine.XPlat; +using NuGet.Test.Utility; +using NuGet.XPlat.FuncTest; +using Xunit; +using Xunit.Abstractions; + +namespace Dotnet.Integration.Test; + +[Collection(DotnetIntegrationCollection.Name)] +public sealed class DotnetRemovePackageTests(DotnetIntegrationTestFixture fixture, ITestOutputHelper testOutputHelper) +{ + private readonly DotnetIntegrationTestFixture _fixture = fixture; + private readonly ITestOutputHelper _testOutputHelper = testOutputHelper; + + // https://github.com/NuGet/Home/issues/14823: This should use `dotnet package remove` when it supports file-based apps. + [Fact] + public async Task RemovePkg_FileBasedApp() + { + using var pathContext = _fixture.CreateSimpleTestPathContext(); + + // Create the file-based app. + var fbaDir = Path.Join(pathContext.SolutionRoot, "fba"); + Directory.CreateDirectory(fbaDir); + + var appFile = Path.Join(fbaDir, "app.cs"); + File.WriteAllText(appFile, """ + #:package packageX@1.0.0 + Console.WriteLine(); + """); + + // Get project content. + var virtualProject = _fixture.GetFileBasedAppVirtualProject(appFile, _testOutputHelper); + _testOutputHelper.WriteLine("before:\n" + virtualProject.Content); + Assert.Contains("""""", virtualProject.Content); + using var builder = new TestVirtualProjectBuilder(virtualProject); + + // Remove the package. + using var outWriter = new StringWriter(); + using var errorWriter = new StringWriter(); + var testApp = new CommandLineApplication + { + Out = outWriter, + Error = errorWriter, + }; + RemovePackageReferenceCommand.Register( + testApp, + () => new TestLogger(_testOutputHelper), + () => new RemovePackageReferenceCommandRunner(), + () => builder); + int result = testApp.Execute([ + "remove", + "--project", appFile, + "--package", "packageX", + ]); + + var output = outWriter.ToString(); + var error = errorWriter.ToString(); + + _testOutputHelper.WriteLine(output); + _testOutputHelper.WriteLine(error); + + Assert.Equal(0, result); + + Assert.Empty(error); + + var modifiedProjectContent = builder.ModifiedContent; + _testOutputHelper.WriteLine("after:\n" + modifiedProjectContent); + Assert.DoesNotContain("PackageReference", modifiedProjectContent); + } +} diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs index a21610db896..c0659f4757e 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs @@ -3,13 +3,20 @@ #nullable disable +using System; +using System.CommandLine; using System.IO; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Internal.NuGet.Testing.SignedPackages.ChildProcess; using NuGet.CommandLine.XPlat; +using NuGet.CommandLine.XPlat.Commands.Why; +using NuGet.Common; +using NuGet.Packaging; using NuGet.Test.Utility; using NuGet.XPlat.FuncTest; +using Spectre.Console; +using Spectre.Console.Testing; using Test.Utility; using Xunit; using Xunit.Abstractions; @@ -62,6 +69,65 @@ await SimpleTestPackageUtility.CreatePackagesAsync( Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput.Replace("\n", "").Replace("\r", "")); } + // https://github.com/NuGet/Home/issues/14823: This should use `dotnet nuget why` when it supports file-based apps. + [Fact] + public async Task WhyCommand_FileBasedApp() + { + using var pathContext = _testFixture.CreateSimpleTestPathContext(); + + // Create packages. + var packageA = XPlatTestUtils.CreatePackage("packageA", "1.0.0"); + var packageB = XPlatTestUtils.CreatePackage("packageB", "1.0.1"); + packageA.Dependencies.Add(packageB); + await SimpleTestPackageUtility.CreateFolderFeedV3Async(pathContext.PackageSource, PackageSaveMode.Defaultv3, packageA); + + // Create the file-based app. + var fbaDir = Path.Join(pathContext.SolutionRoot, "fba"); + Directory.CreateDirectory(fbaDir); + + var appFile = Path.Join(fbaDir, "app.cs"); + File.WriteAllText(appFile, """ + #:property PublishAot=false + #:package PackageA@1.0.0 + Console.WriteLine(); + """); + + // Restore. + _testFixture.RunDotnetExpectSuccess(fbaDir, "restore app.cs", testOutputHelper: _testOutputHelper); + + // Get project content. + var virtualProject = _testFixture.GetFileBasedAppVirtualProject(appFile, _testOutputHelper); + using var builder = new TestVirtualProjectBuilder(virtualProject); + + // Run "why" command. + var console = new TestConsole(); + using var outWriter = new StringWriter(); + using var errorWriter = new StringWriter(); + var rootCommand = new RootCommand(); + WhyCommand.Register( + rootCommand, + new Lazy(console), + () => new WhyCommandRunner(new MSBuildAPIUtility(NullLogger.Instance, builder))); + int result = rootCommand.Parse([ + "why", appFile, "PackageB", + ]).Invoke(new() { Output = outWriter, Error = errorWriter }); + + var output = outWriter.ToString() + console.Output; + var error = errorWriter.ToString(); + + _testOutputHelper.WriteLine(output); + _testOutputHelper.WriteLine(error); + + Assert.Equal(0, result); + + Assert.Empty(error); + + Assert.Contains("PackageA (v1.0.0)", output); + Assert.Contains("packageB (v1.0.1)", output); + + Assert.Null(builder.ModifiedContent); + } + [Fact] public async Task WhyCommand_ProjectHasNoDependencyOnTargetPackage_PathDoesNotExist() { diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Commands/Package/Update/PackageUpdateIOTests/UpdatePackageReferenceTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Commands/Package/Update/PackageUpdateIOTests/UpdatePackageReferenceTests.cs index 75d5c8acab7..87e8b7c3b8a 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Commands/Package/Update/PackageUpdateIOTests/UpdatePackageReferenceTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Commands/Package/Update/PackageUpdateIOTests/UpdatePackageReferenceTests.cs @@ -24,9 +24,9 @@ namespace NuGet.XPlat.FuncTest.Commands.Package.Update.PackageUpdateIOTests; [Collection(XPlatCollection.Name)] public class UpdatePackageReferenceTests { - private static PackageUpdateIO CreatePackageUpdateIO(string solutionRoot) + private static PackageUpdateIO CreatePackageUpdateIO(string solutionRoot, IVirtualProjectBuilder? virtualProjectBuilder = null) { - var msbuildUtility = new MSBuildAPIUtility(NullLogger.Instance); + var msbuildUtility = new MSBuildAPIUtility(NullLogger.Instance, virtualProjectBuilder); var packageUpdateIO = new PackageUpdateIO(solutionRoot, msbuildUtility, TestEnvironmentVariableReader.EmptyInstance); return packageUpdateIO; } @@ -136,4 +136,86 @@ public async Task UpdatePackageReference_DgSpecWithTwoProjects_UpdatesOneProject var project2PackageRef = project2PackageReferences.First(); project2PackageRef.Attribute("Version")?.Value.Should().Be("1.0.0"); } + + [Fact] + public async Task UpdatePackageReference_FileBasedApp() + { + // Arrange + using var testContext = new SimpleTestPathContext(); + + var packageA_v1 = new SimpleTestPackageContext("PackageA", "1.0.0"); + var packageA_v2 = new SimpleTestPackageContext("PackageA", "2.0.0"); + await SimpleTestPackageUtility.CreatePackagesAsync(testContext.PackageSource, packageA_v1, packageA_v2); + + var project = XPlatTestUtils.CreateProject("TestProject1", testContext, "net9.0", fileBasedApp: true); + + project.AddPackageToAllFrameworks(packageA_v1); + project.Properties.Add("RestorePackagesPath", testContext.UserPackagesFolder); + project.Sources = new List { new PackageSource(testContext.PackageSource) }; + project.FallbackFolders = new List { testContext.FallbackFolder }; + project.GlobalPackagesFolder = testContext.UserPackagesFolder; + + using var builder = TestVirtualProjectBuilder.From(project); + using var packageUpdateIO = CreatePackageUpdateIO(testContext.SolutionRoot, builder); + + var updatedDgSpec = new ProjectModel.DependencyGraphSpec(); + var originalProjectSpec1 = project.PackageSpec; + var projectSpec1 = originalProjectSpec1.Clone(); + projectSpec1.RestoreMetadata.Sources = new List { new PackageSource(testContext.PackageSource) }; + projectSpec1.RestoreMetadata.FallbackFolders = new List { testContext.FallbackFolder }; + projectSpec1.RestoreMetadata.PackagesPath = testContext.UserPackagesFolder; + var framework1 = projectSpec1.TargetFrameworks.First(); + framework1.Dependencies.Clear(); + framework1.Dependencies.Add(new LibraryModel.LibraryDependency + { + LibraryRange = new LibraryModel.LibraryRange( + "PackageA", + VersionRange.Parse("2.0.0"), + NuGet.LibraryModel.LibraryDependencyTarget.Package) + }); + updatedDgSpec.AddProject(projectSpec1); + updatedDgSpec.AddRestore(projectSpec1.RestoreMetadata.ProjectUniqueName); + + foreach (var packageSpec in updatedDgSpec.Projects) + { + if (packageSpec.FilePath == project.VirtualProjectPath) + { + packageSpec.FilePath = project.ProjectPath; + } + } + + var previewResult = await packageUpdateIO.PreviewUpdatePackageReferenceAsync( + updatedDgSpec, + NullLogger.Instance, + CancellationToken.None); + previewResult.Success.Should().BeTrue(); + + var packageToUpdate = new PackageToUpdate + { + Id = "PackageA", + CurrentVersion = VersionRange.Parse("1.0.0"), + NewVersion = VersionRange.Parse("2.0.0") + }; + + var tfmAliases = projectSpec1.TargetFrameworks.Select(tf => tf.TargetAlias).ToList(); + + // Act + packageUpdateIO.UpdatePackageReference( + projectSpec1, + previewResult, + tfmAliases, + packageToUpdate, + NullLogger.Instance); + + // Assert + var project1Xml = XDocument.Parse(builder!.ModifiedContent!); + var ns = project1Xml.Root!.GetDefaultNamespace(); + var project1PackageReferences = project1Xml.Descendants(ns + "PackageReference") + .Where(e => e.Attribute("Include")?.Value == "PackageA") + .ToList(); + + project1PackageReferences.Should().ContainSingle(); + var project1PackageRef = project1PackageReferences.First(); + project1PackageRef.Attribute("Version")?.Value.Should().Be("2.0.0"); + } } diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs index e73e0287815..36062cd7d6f 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs @@ -224,7 +224,7 @@ await SimpleTestPackageUtility.CreatePackagesAsync( using TextWriter consoleOut = new StringWriter(output); using TextWriter consoleError = new StringWriter(error); var logger = new TestLogger(_testOutputHelper); - ListPackageCommandRunner listPackageCommandRunner = new(); + ListPackageCommandRunner listPackageCommandRunner = new(new MSBuildAPIUtility(logger)); var packageRefArgs = new ListPackageArgs( path: Path.Combine(pathContext.SolutionRoot, "solution.sln"), packageSources: [new(mockServer.ServiceIndexUri)], @@ -333,7 +333,7 @@ await SimpleTestPackageUtility.CreatePackagesAsync( using TextWriter consoleOut = new StringWriter(output); using TextWriter consoleError = new StringWriter(error); var logger = new TestLogger(_testOutputHelper); - ListPackageCommandRunner listPackageCommandRunner = new(); + ListPackageCommandRunner listPackageCommandRunner = new(new MSBuildAPIUtility(logger)); var packageRefArgs = new ListPackageArgs( path: solution.SolutionPath, packageSources: [new PackageSource(pathContext.PackageSource)], @@ -352,6 +352,53 @@ await SimpleTestPackageUtility.CreatePackagesAsync( Assert.True(result == 0, userMessage: logger.ShowMessages()); } + [Fact] + public async Task CanListPackagesForFileBasedApp() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + + var packageA100 = new SimpleTestPackageContext("A", "1.0.0"); + + await SimpleTestPackageUtility.CreatePackagesAsync( + pathContext.PackageSource, + packageA100); + + var projectA = XPlatTestUtils.CreateProject("ProjectA", pathContext, "net6.0", fileBasedApp: true); + projectA.AddPackageToAllFrameworks(packageA100); + var projectB = SimpleTestProjectContext.CreateNETCore("ProjectB", pathContext.SolutionRoot, "net6.0"); + + projectA.Save(); + projectB.Save(); + + // 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); + using var builder = TestVirtualProjectBuilder.From(projectA); + ListPackageCommandRunner listPackageCommandRunner = new(new MSBuildAPIUtility(logger, builder)); + var packageRefArgs = new ListPackageArgs( + path: builder.FilePath, + packageSources: [new PackageSource(pathContext.PackageSource)], + frameworks: ["net6.0"], + reportType: ReportType.Outdated, + renderer: new ListPackageConsoleRenderer(consoleOut, consoleError), + includeTransitive: false, + prerelease: false, + highestPatch: false, + highestMinor: false, + auditSources: null, + logger: logger, + cancellationToken: CancellationToken.None); + + int result = await listPackageCommandRunner.ExecuteCommandAsync(packageRefArgs); + Assert.True(result == 0, userMessage: $"{output}\n{error}\n{logger.ShowMessages()}"); + } + [Fact] public async Task GetReportDataAsync_WhenReportTypeIsVulnerable_ShouldUseAuditSources() { @@ -381,7 +428,7 @@ public async Task GetReportDataAsync_WhenReportTypeIsVulnerable_ShouldUseAuditSo CancellationToken.None ); - var listPackageCommandRunner = new ListPackageCommandRunner(); + var listPackageCommandRunner = new ListPackageCommandRunner(new MSBuildAPIUtility(mockLogger.Object)); // Act @@ -427,7 +474,7 @@ public async Task GetReportDataAsync_WithSolutionFilePassed_ShouldList() CancellationToken.None ); - var listPackageCommandRunner = new ListPackageCommandRunner(); + var listPackageCommandRunner = new ListPackageCommandRunner(new MSBuildAPIUtility(mockLogger.Object)); // Act var result = await listPackageCommandRunner.GetReportDataAsync(listPackageArgs); @@ -482,7 +529,7 @@ public async Task GetReportDataAsync_WhenReportTypeIsVulnerableAuditSourcesWithN CancellationToken.None ); - var listPackageCommandRunner = new ListPackageCommandRunner(); + var listPackageCommandRunner = new ListPackageCommandRunner(new MSBuildAPIUtility(mockLogger.Object)); // Act var result = await listPackageCommandRunner.GetReportDataAsync(listPackageArgs); diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/NuGet.XPlat.FuncTest.csproj b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/NuGet.XPlat.FuncTest.csproj index 4d55c7a887b..ef3f708b537 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/NuGet.XPlat.FuncTest.csproj +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/NuGet.XPlat.FuncTest.csproj @@ -25,4 +25,8 @@ + + + + diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/TestVirtualProjectBuilder.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/TestVirtualProjectBuilder.cs new file mode 100644 index 00000000000..5f44c68546a --- /dev/null +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/TestVirtualProjectBuilder.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using NuGet.CommandLine.XPlat; +using NuGet.Test.Utility; +using Xunit; + +namespace NuGet.XPlat.FuncTest; + +/// +/// Test implementation of . +/// +internal sealed class TestVirtualProjectBuilder : IVirtualProjectBuilder, IDisposable +{ + private readonly (string Content, string ProjectPath, string FilePath) _virtualProject; + + public string? ModifiedContent { get; private set; } + + public TestVirtualProjectBuilder((string Content, string ProjectPath, string FilePath) virtualProject) + { + _virtualProject = virtualProject; + } + + public static TestVirtualProjectBuilder? From(SimpleTestProjectContext project) + { + if (project.VirtualProjectContent != null) + { + Assert.EndsWith(".cs", project.ProjectPath, StringComparison.OrdinalIgnoreCase); + return new TestVirtualProjectBuilder((project.VirtualProjectContent, project.VirtualProjectPath, project.ProjectPath)); + } + + return null; + } + + public string FilePath => _virtualProject.FilePath; + + public bool IsValidEntryPointPath(string entryPointFilePath) + { + return entryPointFilePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase); + } + + public string GetVirtualProjectPath(string entryPointFilePath) + { + return _virtualProject.ProjectPath; + } + + public ProjectRootElement CreateProjectRootElement(string entryPointFilePath, ProjectCollection projectCollection) + { + using var stringReader = new StringReader(_virtualProject.Content); + using var xmlReader = XmlReader.Create(stringReader); + var element = ProjectRootElement.Create(xmlReader, projectCollection, preserveFormatting: true); + element.FullPath = GetVirtualProjectPath(entryPointFilePath); + return element; + } + + public void SaveProject(string entryPointFilePath, ProjectRootElement projectRootElement) + { + Assert.Equal(_virtualProject.FilePath, entryPointFilePath); + Assert.Null(ModifiedContent); // ideally we should not be saving twice + ModifiedContent = projectRootElement.RawXml; + } + + public void Dispose() + { + Assert.False(File.Exists(_virtualProject.ProjectPath)); + } +} diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatAddPkgTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatAddPkgTests.cs index 3a2e93362dd..4275b43dda4 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatAddPkgTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatAddPkgTests.cs @@ -810,14 +810,16 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } } - [Fact] - public async Task AddPkg_V3LocalSourceFeed_WithAbsolutePath_NoVersionSpecified_Success() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task AddPkg_V3LocalSourceFeed_WithAbsolutePath_NoVersionSpecified_Success(bool fileBasedApp) { using (var pathContext = new SimpleTestPathContext()) { var projectFrameworks = "net472"; var packageFrameworks = "net472; netcoreapp2.0"; - var projectA = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFrameworks); + var projectA = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFrameworks, fileBasedApp); var packageX = "packageX"; var packageX_V1 = new PackageIdentity(packageX, new NuGetVersion("1.0.0")); var packageX_V2 = new PackageIdentity(packageX, new NuGetVersion("2.0.0")); @@ -840,8 +842,10 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( var commandRunner = new AddPackageReferenceCommandRunner(); + using var virtualProjectBuilder = TestVirtualProjectBuilder.From(projectA); + // Act - var result = await commandRunner.ExecuteCommand(packageArgs, new MSBuildAPIUtility(logger)); + var result = await commandRunner.ExecuteCommand(packageArgs, new MSBuildAPIUtility(logger, virtualProjectBuilder)); // Assert Assert.Equal(0, result); diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatTestUtils.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatTestUtils.cs index c5293447881..6e8d3a69e6c 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatTestUtils.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatTestUtils.cs @@ -110,7 +110,8 @@ public static SimpleTestProjectContext CreateProject(string projectName, public static SimpleTestProjectContext CreateProject(string projectName, SimpleTestPathContext pathContext, - string projectFrameworks) + string projectFrameworks, + bool fileBasedApp = false) { var settings = Settings.LoadDefaultSettings(Path.GetDirectoryName(pathContext.NuGetConfig), Path.GetFileName(pathContext.NuGetConfig), null); var project = SimpleTestProjectContext.CreateNETCoreWithSDK( @@ -123,7 +124,16 @@ public static SimpleTestProjectContext CreateProject(string projectName, var packageSourceProvider = new PackageSourceProvider(settings); project.Sources = packageSourceProvider.LoadPackageSources(); + if (fileBasedApp) + { + project.VirtualProjectPath = project.ProjectPath; + project.ProjectPath = Path.ChangeExtension(project.ProjectPath, ".cs"); + Directory.CreateDirectory(Path.GetDirectoryName(project.ProjectPath)!); + File.WriteAllText(project.ProjectPath, ""); // commands might check the file's existence + } + project.Save(); + return project; } @@ -378,6 +388,16 @@ public static string GetCommonFramework(string frameworkStringA, string framewor .First(); } + public static XDocument LoadCSProj(SimpleTestProjectContext project) + { + if (project.VirtualProjectContent != null) + { + return XDocument.Parse(project.VirtualProjectContent); + } + + return LoadCSProj(project.ProjectPath); + } + public static XDocument LoadCSProj(string path) { return LoadSafe(path); diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index abbecc6bb2f..8d600621033 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -6,6 +6,7 @@ using FluentAssertions; using NuGet.CommandLine.XPlat; using NuGet.CommandLine.XPlat.Commands.Why; +using NuGet.Common; using NuGet.Packaging; using NuGet.Test.Utility; using Spectre.Console.Testing; @@ -26,13 +27,15 @@ public XPlatWhyTests(ITestOutputHelper testOutputHelper) _testOutputHelper = testOutputHelper; } - [Fact] - public async Task WhyCommand_ProjectHasTransitiveDependency_DependencyPathExists() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task WhyCommand_ProjectHasTransitiveDependency_DependencyPathExists(bool fileBasedApp) { // Arrange var pathContext = new SimpleTestPathContext(); var projectFramework = "net472"; - var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework, fileBasedApp); var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); @@ -48,9 +51,11 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( packageY); var logger = new TestCommandOutputLogger(_testOutputHelper); + using var builder = TestVirtualProjectBuilder.From(project); + var msbuild = new MSBuildAPIUtility(logger, builder); var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(logger, packageX.Id, packageX.Version, project); var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); - var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, new MSBuildAPIUtility(logger)); + var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, msbuild); var console = new TestConsole(); console.Width(100); @@ -63,7 +68,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( CancellationToken.None); // Act - var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await new WhyCommandRunner(msbuild).ExecuteCommand(whyCommandArgs); // Assert var output = console.Output; @@ -118,7 +123,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( CancellationToken.None); // Act - var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await new WhyCommandRunner(new MSBuildAPIUtility(logger)).ExecuteCommand(whyCommandArgs); // Assert var output = console.Output; @@ -153,7 +158,7 @@ public async Task WhyCommand_ProjectDidNotRunRestore_Fails() CancellationToken.None); // Act - var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await new WhyCommandRunner(new MSBuildAPIUtility(NullLogger.Instance)).ExecuteCommand(whyCommandArgs); // Assert var output = logger.Lines; @@ -177,7 +182,7 @@ public async Task WhyCommand_EmptyProjectArgument_Fails() CancellationToken.None); // Act - var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await new WhyCommandRunner(new MSBuildAPIUtility(NullLogger.Instance)).ExecuteCommand(whyCommandArgs); // Assert var errorOutput = logger.Lines; @@ -204,7 +209,7 @@ public async Task WhyCommand_EmptyPackageArgument_Fails() CancellationToken.None); // Act - var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await new WhyCommandRunner(new MSBuildAPIUtility(NullLogger.Instance)).ExecuteCommand(whyCommandArgs); // Assert var errorOutput = logger.Lines; @@ -230,13 +235,13 @@ public async Task WhyCommand_InvalidProject_Fails() CancellationToken.None); // Act - var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await new WhyCommandRunner(new MSBuildAPIUtility(NullLogger.Instance)).ExecuteCommand(whyCommandArgs); // Assert var errorOutput = logger.Lines; Assert.Equal(ExitCodes.InvalidArguments, result); - Assert.Contains($"Unable to run 'dotnet nuget why'. Missing or invalid path '{fakeProjectPath}'. Please provide a path to a project, solution file, or directory.", errorOutput); + Assert.Contains($"Unable to run 'dotnet nuget why'. Missing or invalid path '{fakeProjectPath}'. Please provide a path to a project, solution file, file-based app, or project directory.", errorOutput); } [Fact] @@ -277,7 +282,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( CancellationToken.None); // Act - var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await new WhyCommandRunner(new MSBuildAPIUtility(logger)).ExecuteCommand(whyCommandArgs); // Assert var output = console.Output; diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XplatRemovePkgTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XplatRemovePkgTests.cs index 8bd9119207c..7bd47bb36db 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XplatRemovePkgTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XplatRemovePkgTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using System.Xml.Linq; using Microsoft.Extensions.CommandLineUtils; using Moq; using NuGet.CommandLine.XPlat; @@ -76,8 +77,10 @@ public void AddPkg_RemoveParsing(string packageOption, string package, // Remove Related Tests - [Fact] - public async Task RemovePkg_UnconditionalRemove_Success() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task RemovePkg_UnconditionalRemove_Success(bool fileBasedApp) { // Arrange @@ -90,11 +93,13 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( PackageSaveMode.Defaultv3, packageX); - var projectA = XPlatTestUtils.CreateProject(ProjectName, pathContext, packageX, "net46"); + var projectA = XPlatTestUtils.CreateProject(ProjectName, pathContext, "net46", fileBasedApp); + projectA.AddPackageToAllFrameworks(packageX); + projectA.Save(); var logger = new TestCommandOutputLogger(_testOutputHelper); // Verify that the package reference exists before removing. - var projectXmlRoot = XPlatTestUtils.LoadCSProj(projectA.ProjectPath).Root; + var projectXmlRoot = XPlatTestUtils.LoadCSProj(projectA).Root; var itemGroup = XPlatTestUtils.GetItemGroupForAllFrameworks(projectXmlRoot); Assert.NotNull(itemGroup); @@ -103,9 +108,13 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( var packageArgs = XPlatTestUtils.GetPackageReferenceArgs(logger, packageX.Id, projectA); var commandRunner = new RemovePackageReferenceCommandRunner(); + using var builder = TestVirtualProjectBuilder.From(projectA); + // Act - var result = await commandRunner.ExecuteCommand(packageArgs, new MSBuildAPIUtility(logger)); - projectXmlRoot = XPlatTestUtils.LoadCSProj(projectA.ProjectPath).Root; + var result = await commandRunner.ExecuteCommand(packageArgs, new MSBuildAPIUtility(logger, builder)); + projectXmlRoot = builder != null + ? XDocument.Parse(builder.ModifiedContent).Root + : XPlatTestUtils.LoadCSProj(projectA).Root; // Assert Assert.Equal(0, result); diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/ListPackageCommandRunnerTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/ListPackageCommandRunnerTests.cs index dd2027adf4a..3200575d33f 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/ListPackageCommandRunnerTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/ListPackageCommandRunnerTests.cs @@ -158,7 +158,7 @@ public void FiltersFrameworkPackagesCollectionWithOutdatedMetadata( // Act var isFilteredSetNonEmpty = ListPackageCommandRunner.FilterPackages(allPackages, listPackageArgs); - var a = new ListPackageCommandRunner(); + var a = new ListPackageCommandRunner(new MSBuildAPIUtility(NullLogger.Instance)); var b = a.UpdatePackagesWithSourceMetadata(allPackages, null, listPackageArgs); // Assert @@ -171,7 +171,7 @@ public void FiltersFrameworkPackagesCollectionWithOutdatedMetadata( public async Task UpdatePackages_WithNullSourceMetadata_Succeeds() { // Arrange - ListPackageCommandRunner listPackageRunner = new ListPackageCommandRunner(); + ListPackageCommandRunner listPackageRunner = new ListPackageCommandRunner(new MSBuildAPIUtility(NullLogger.Instance)); FrameworkPackages packages = new FrameworkPackages("net40", "net40"); List topLevelPackages = new List @@ -402,7 +402,7 @@ public async Task GetPackageMetadataAsync_WithEmptyPackageSources_DoesNotThrowDi logger: new Mock().Object, cancellationToken: CancellationToken.None); - var listPackageRunner = new ListPackageCommandRunner(); + var listPackageRunner = new ListPackageCommandRunner(new MSBuildAPIUtility(NullLogger.Instance)); // Act & Assert - Call the method directly since it's now internal Exception exception = await Record.ExceptionAsync(async () => diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs index 1160c400975..0c2d1633c1c 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs @@ -383,7 +383,7 @@ public void UpdatePackageVersionInPropsFileWhenItExists_Success() }; // Act - MSBuildAPIUtility.UpdatePackageVersion(project, packageVersionInProps, "2.0.0"); + MSBuildAPIUtility.UpdatePackageVersion(new SaveableProject { Project = project }, packageVersionInProps, "2.0.0"); // Assert Assert.Equal(projectContent, File.ReadAllText(Path.Combine(testDirectory, "projectA.csproj"))); @@ -452,7 +452,7 @@ public void UpdateVersionOverrideInPropsFileWhenItExists_Success() }; // Act - MSBuildAPIUtility.UpdateVersionOverride(project, packageVersionInProps, "3.0.0"); + MSBuildAPIUtility.UpdateVersionOverride(new SaveableProject { Project = project }, packageVersionInProps, "3.0.0"); // Assert Assert.Equal(projectContent, File.ReadAllText(Path.Combine(testDirectory, "projectA.csproj"))); @@ -555,8 +555,10 @@ public void GetListOfProjectsFromPathArgument_WithProjectFile_ReturnsCorrectPath projectA.Save(); + var msObject = new MSBuildAPIUtility(logger: new TestLogger()); + // Act - var projectList = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(projectA.ProjectPath); + var projectList = msObject.GetListOfProjectsFromPathArgument(projectA.ProjectPath); // Assert Assert.Equal(projectList.Count(), 1); @@ -574,8 +576,10 @@ public void GetListOfProjectsFromPathArgument_WithProjectDirectory_ReturnsCorrec projectA.Save(); + var msObject = new MSBuildAPIUtility(logger: new TestLogger()); + // Act - var projectList = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(Path.GetDirectoryName(projectA.ProjectPath)); + var projectList = msObject.GetListOfProjectsFromPathArgument(Path.GetDirectoryName(projectA.ProjectPath)); // Assert Assert.Equal(projectList.Count(), 1); @@ -597,8 +601,10 @@ public void GetListOfProjectsFromPathArgument_WithSolutionFile_ReturnsCorrectPat solution.Projects.Add(projectB); solution.Create(); + var msObject = new MSBuildAPIUtility(logger: new TestLogger()); + // Act - var projectList = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(Path.GetDirectoryName(solution.SolutionPath)); + var projectList = msObject.GetListOfProjectsFromPathArgument(Path.GetDirectoryName(solution.SolutionPath)); // Assert Assert.Equal(projectList.Count(), 2); @@ -621,8 +627,10 @@ public void GetListOfProjectsFromPathArgument_WithSolutionDirectory_ReturnsCorre solution.Projects.Add(projectB); solution.Create(); + var msObject = new MSBuildAPIUtility(logger: new TestLogger()); + // Act - var projectList = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(pathContext.SolutionRoot); + var projectList = msObject.GetListOfProjectsFromPathArgument(pathContext.SolutionRoot); // Assert Assert.Equal(projectList.Count(), 2); @@ -711,8 +719,10 @@ public void GetListOfProjectsFromPathArgument_WithDirectoryWithInvalidNumberOfSo File.Create(filePath); } + var msObject = new MSBuildAPIUtility(logger: new TestLogger()); + // Act & Assert - Assert.Throws(() => MSBuildAPIUtility.GetListOfProjectsFromPathArgument(pathContext.SolutionRoot)); + Assert.Throws(() => msObject.GetListOfProjectsFromPathArgument(pathContext.SolutionRoot)); } [Fact] diff --git a/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestProjectContext.cs b/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestProjectContext.cs index 15143cf9062..9ccd2ab14a9 100644 --- a/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestProjectContext.cs +++ b/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestProjectContext.cs @@ -66,6 +66,17 @@ public SimpleTestProjectContext(string projectName, ProjectStyle type, string so /// public string ProjectPath { get; set; } + /// + /// If this represents a file-based app, this is the content (XML text) of the virtual project. + /// + public string VirtualProjectContent { get; set; } + + /// + /// If this represents a file-based app, this is the path of the virtual project + /// (and is the path of the entry-point .cs file). + /// + public string VirtualProjectPath { get; set; } + /// /// MSBuildProjectExtensionsPath /// @@ -250,6 +261,7 @@ public PackageSpec PackageSpec { get { + var filePath = VirtualProjectPath ?? ProjectPath; var _packageSpec = new PackageSpec(Frameworks .Select(f => new TargetFrameworkInformation() { @@ -259,10 +271,10 @@ public PackageSpec PackageSpec }).ToList()); _packageSpec.RestoreMetadata = new ProjectRestoreMetadata(); _packageSpec.Name = ProjectName; - _packageSpec.FilePath = ProjectPath; + _packageSpec.FilePath = filePath; _packageSpec.RestoreMetadata.ProjectUniqueName = ProjectName; _packageSpec.RestoreMetadata.ProjectName = ProjectName; - _packageSpec.RestoreMetadata.ProjectPath = ProjectPath; + _packageSpec.RestoreMetadata.ProjectPath = filePath; _packageSpec.RestoreMetadata.ProjectStyle = Type; _packageSpec.RestoreMetadata.OutputPath = ProjectExtensionsPath; _packageSpec.RestoreMetadata.OriginalTargetFrameworks = _packageSpec.TargetFrameworks.Select(e => e.TargetAlias).ToList(); @@ -359,7 +371,14 @@ public void AddProjectToAllFrameworks(params SimpleTestProjectContext[] projects public void Save() { - Save(ProjectPath); + if (VirtualProjectPath != null) + { + VirtualProjectContent = GetXML().ToString(); + } + else + { + Save(ProjectPath); + } } public void Save(string path)