diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index 8e70cb3b11db..2458c9b5bb28 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -100,6 +100,9 @@ To opt out, use `#:property PublishAot=false` directive in your `.cs` file. Command `dotnet clean file.cs` can be used to clean build artifacts of the file-based program. +Commands `dotnet package add PackageName --file app.cs` and `dotnet package remove PackageName --file app.cs` +can be used to manipulate `#:package` directives in the C# files, similarly to what the commands do for project-based apps. + ## Entry points If a file is given to `dotnet run`, it has to be an *entry-point file*, otherwise an error is reported. @@ -373,8 +376,7 @@ We could also add `dotnet compile` command that would be the equivalent of `dotn `dotnet clean` could be extended to support cleaning all file-based app outputs, e.g., `dotnet clean --all-file-based-apps`. -Adding references via `dotnet package add`/`dotnet reference add` could be supported for file-based programs as well, -i.e., the command would add a `#:package`/`#:project` directive to the top of a `.cs` file. +More NuGet commands (like `dotnet nuget why` or `dotnet package list`) could be supported for file-based programs as well. ### Explicit importing diff --git a/src/Cli/dotnet/CliStrings.resx b/src/Cli/dotnet/CliStrings.resx index 39e1a3e261df..a41cfa9c3ea1 100644 --- a/src/Cli/dotnet/CliStrings.resx +++ b/src/Cli/dotnet/CliStrings.resx @@ -273,9 +273,18 @@ PROJECT + + PROJECT | FILE + The project file to operate on. If a file is not specified, the command will search the current directory for one. + + The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file. + + + The file-based app to operate on. + FRAMEWORK @@ -814,4 +823,4 @@ The default is 'false.' However, when targeting .NET 7 or lower, the default is Display the command schema as JSON. - \ No newline at end of file + diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx index 75c76f009030..773e525cbdce 100644 --- a/src/Cli/dotnet/Commands/CliCommandStrings.resx +++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx @@ -1251,6 +1251,11 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man Specify only one package reference to remove. + + Removed '{0}' directives ({1}) for '{2}' from: {3} + {0} is a directive kind (like '#:package'). {1} is number of removed directives. + {2} is directive key (e.g., package name). {3} is file path from which directives were removed. + Command names conflict. Command names are case insensitive. {0} @@ -1530,14 +1535,14 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man Duplicate directives are not supported: {0} at {1} {0} is the directive type and name. {1} is the file path and line number. - - Cannot combine option '{0}' and '{1}'. - {0} and {1} are option names like '--no-build'. - Cannot specify option '{0}' when also using '-' to read the file from standard input. {0} is an option name like '--no-build'. + + Cannot specify option '{0}' when operating on a file-based app. + {0} is an option name like '--source'. + Warning: Binary log option was specified but build will be skipped because output is up to date, specify '--no-cache' to force build. {Locked="--no-cache"} diff --git a/src/Cli/dotnet/Commands/Hidden/Add/AddCommandParser.cs b/src/Cli/dotnet/Commands/Hidden/Add/AddCommandParser.cs index 89c12c174c08..43cb90737cd6 100644 --- a/src/Cli/dotnet/Commands/Hidden/Add/AddCommandParser.cs +++ b/src/Cli/dotnet/Commands/Hidden/Add/AddCommandParser.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Hidden.Add.Package; using Microsoft.DotNet.Cli.Commands.Hidden.Add.Reference; +using Microsoft.DotNet.Cli.Commands.Package; using Microsoft.DotNet.Cli.Extensions; namespace Microsoft.DotNet.Cli.Commands.Hidden.Add; @@ -14,11 +13,6 @@ internal static class AddCommandParser { public static readonly string DocsLink = "https://aka.ms/dotnet-add"; - public static readonly Argument ProjectArgument = new Argument(CliStrings.ProjectArgumentName) - { - Description = CliStrings.ProjectArgumentDescription - }.DefaultToCurrentDirectory(); - private static readonly Command Command = ConstructCommand(); public static Command GetCommand() @@ -33,7 +27,7 @@ private static Command ConstructCommand() Hidden = true }; - command.Arguments.Add(ProjectArgument); + command.Arguments.Add(PackageCommandParser.ProjectOrFileArgument); command.Subcommands.Add(AddPackageCommandParser.GetCommand()); command.Subcommands.Add(AddReferenceCommandParser.GetCommand()); diff --git a/src/Cli/dotnet/Commands/Hidden/Add/Package/AddPackageCommandParser.cs b/src/Cli/dotnet/Commands/Hidden/Add/Package/AddPackageCommandParser.cs index ca1e014bf4a6..f6cb1909b23d 100644 --- a/src/Cli/dotnet/Commands/Hidden/Add/Package/AddPackageCommandParser.cs +++ b/src/Cli/dotnet/Commands/Hidden/Add/Package/AddPackageCommandParser.cs @@ -1,12 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Package; using Microsoft.DotNet.Cli.Commands.Package.Add; -using Microsoft.DotNet.Cli.Extensions; namespace Microsoft.DotNet.Cli.Commands.Hidden.Add.Package; @@ -32,20 +29,9 @@ private static Command ConstructCommand() command.Options.Add(PackageAddCommandParser.InteractiveOption); command.Options.Add(PackageAddCommandParser.PrereleaseOption); command.Options.Add(PackageCommandParser.ProjectOption); + command.Options.Add(PackageCommandParser.FileOption); - command.SetAction((parseResult) => - { - // this command can be called with an argument or an option for the project path - we prefer the option. - // if the option is not present, we use the argument value instead. - if (parseResult.HasOption(PackageCommandParser.ProjectOption)) - { - return new PackageAddCommand(parseResult, parseResult.GetValue(PackageCommandParser.ProjectOption)).Execute(); - } - else - { - return new PackageAddCommand(parseResult, parseResult.GetValue(AddCommandParser.ProjectArgument) ?? Directory.GetCurrentDirectory()).Execute(); - } - }); + command.SetAction((parseResult) => new PackageAddCommand(parseResult).Execute()); return command; } diff --git a/src/Cli/dotnet/Commands/Hidden/Remove/Package/RemovePackageCommandParser.cs b/src/Cli/dotnet/Commands/Hidden/Remove/Package/RemovePackageCommandParser.cs index bc661b2a71da..51eb32535cc2 100644 --- a/src/Cli/dotnet/Commands/Hidden/Remove/Package/RemovePackageCommandParser.cs +++ b/src/Cli/dotnet/Commands/Hidden/Remove/Package/RemovePackageCommandParser.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Package.Remove; diff --git a/src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandParser.cs b/src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandParser.cs index 02ba2402c32f..0ac1f1834dbb 100644 --- a/src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandParser.cs +++ b/src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandParser.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Hidden.Remove.Package; using Microsoft.DotNet.Cli.Commands.Hidden.Remove.Reference; +using Microsoft.DotNet.Cli.Commands.Package; using Microsoft.DotNet.Cli.Extensions; namespace Microsoft.DotNet.Cli.Commands.Hidden.Remove; @@ -14,11 +13,6 @@ internal static class RemoveCommandParser { public static readonly string DocsLink = "https://aka.ms/dotnet-remove"; - public static readonly Argument ProjectArgument = new Argument(CliStrings.ProjectArgumentName) - { - Description = CliStrings.ProjectArgumentDescription - }.DefaultToCurrentDirectory(); - private static readonly Command Command = ConstructCommand(); public static Command GetCommand() @@ -33,7 +27,7 @@ private static Command ConstructCommand() Hidden = true }; - command.Arguments.Add(ProjectArgument); + command.Arguments.Add(PackageCommandParser.ProjectOrFileArgument); command.Subcommands.Add(RemovePackageCommandParser.GetCommand()); command.Subcommands.Add(RemoveReferenceCommandParser.GetCommand()); diff --git a/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs b/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs index fabaa4ba0787..46ddfe28c4b0 100644 --- a/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs +++ b/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs @@ -21,7 +21,7 @@ internal static bool AddPackageReference(string projectPath, string packageName, { commandArgs = commandArgs.Append(PackageAddCommandParser.VersionOption.Name).Append(version); } - var addPackageReferenceCommand = new PackageAddCommand(AddCommandParser.GetCommand().Parse([.. commandArgs]), projectPath); + var addPackageReferenceCommand = new PackageAddCommand(AddCommandParser.GetCommand().Parse([.. commandArgs])); return addPackageReferenceCommand.Execute() == 0; } diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs index 080fa97a52e6..e97898a0a5ec 100644 --- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs +++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs @@ -1,27 +1,33 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; +using System.Diagnostics; +using Microsoft.Build.Evaluation; using Microsoft.DotNet.Cli.Commands.MSBuild; using Microsoft.DotNet.Cli.Commands.NuGet; +using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; +using NuGet.ProjectModel; namespace Microsoft.DotNet.Cli.Commands.Package.Add; -/// -/// -/// Since this command is invoked via both 'package add' and 'add package', different symbols will control what the project path to search is. -/// It's cleaner for the separate callsites to know this instead of pushing that logic here. -/// -internal class PackageAddCommand(ParseResult parseResult, string fileOrDirectory) : CommandBase(parseResult) +internal class PackageAddCommand(ParseResult parseResult) : CommandBase(parseResult) { - private readonly PackageIdentityWithRange _packageId = parseResult.GetValue(PackageAddCommandParser.CmdPackageArgument); + private readonly PackageIdentityWithRange _packageId = parseResult.GetValue(PackageAddCommandParser.CmdPackageArgument)!; public override int Execute() { + var (fileOrDirectory, allowedAppKinds) = PackageCommandParser.ProcessPathOptions(_parseResult); + + if (allowedAppKinds.HasFlag(AppKinds.FileBased) && VirtualProjectBuildingCommand.IsValidEntryPointPath(fileOrDirectory)) + { + return ExecuteForFileBasedApp(fileOrDirectory); + } + + Debug.Assert(allowedAppKinds.HasFlag(AppKinds.ProjectBased)); + string projectFilePath; if (!File.Exists(fileOrDirectory)) { @@ -114,7 +120,7 @@ private string[] TransformArgs(PackageIdentityWithRange packageId, string tempDg if (packageId.HasVersion) { args.Add("--version"); - args.Add(packageId.VersionRange.OriginalString); + args.Add(packageId.VersionRange.OriginalString ?? string.Empty); } args.AddRange(_parseResult @@ -133,4 +139,214 @@ private string[] TransformArgs(PackageIdentityWithRange packageId, string tempDg return [.. args]; } + + // More logic should live in NuGet: https://github.com/NuGet/Home/issues/14390 + private int ExecuteForFileBasedApp(string path) + { + // Check disallowed options. + ReadOnlySpan