diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index ea907c0aa7cb..f1e572c46268 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -38,7 +38,10 @@ Additionally, the implicit project file has the following customizations: - `ArtifactsPath` is set to a [temp directory](#build-outputs). - - `RuntimeHostConfigurationOption`s are set for `EntryPointFilePath` and `EntryPointFileDirectoryPath` which can be accessed in the app via `AppContext`: + - `PublishDir` and `PackageOutputPath` are set to `./artifacts/` so the outputs of `dotnet publish` and `dotnet pack` are next to the file-based app. + + - `RuntimeHostConfigurationOption`s are set for `EntryPointFilePath` and `EntryPointFileDirectoryPath` (except for `Publish` and `Pack` targets) + which can be accessed in the app via `AppContext`: ```cs string? filePath = AppContext.GetData("EntryPointFilePath") as string; @@ -101,7 +104,7 @@ the compilation consists solely of the single file read from the standard input. Commands `dotnet restore file.cs` and `dotnet build file.cs` are needed for IDE support and hence work for file-based programs. -Command `dotnet publish file.cs` is also supported for file-based programs. +Commands `dotnet publish file.cs` and `dotnet pack file.cs` are also supported for file-based programs. Note that file-based apps have implicitly set `PublishAot=true`, so publishing uses Native AOT (and building reports AOT warnings). To opt out, use `#:property PublishAot=false` directive in your `.cs` file. @@ -373,7 +376,7 @@ so `dotnet file.cs` instead of `dotnet run file.cs` should be used in shebangs: ### Other possible commands -We can consider supporting other commands like `dotnet pack`, `dotnet watch`, +We can consider supporting other commands like `dotnet watch`, however the primary scenario is `dotnet run` and we might never support additional commands. All commands supporting file-based programs should have a way to receive the target path similarly to `dotnet run`, diff --git a/src/Cli/dotnet/Commands/Pack/PackCommand.cs b/src/Cli/dotnet/Commands/Pack/PackCommand.cs index 5dbbf56d7ae2..59d007661836 100644 --- a/src/Cli/dotnet/Commands/Pack/PackCommand.cs +++ b/src/Cli/dotnet/Commands/Pack/PackCommand.cs @@ -4,16 +4,15 @@ using System.Collections.ObjectModel; using System.CommandLine; using System.CommandLine.Parsing; -using System.Configuration; using Microsoft.DotNet.Cli.Commands.Build; using Microsoft.DotNet.Cli.Commands.Restore; +using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.Utils; using NuGet.Commands; using NuGet.Common; using NuGet.Packaging; -using NuGet.Packaging.Core; namespace Microsoft.DotNet.Cli.Commands.Pack; @@ -23,33 +22,54 @@ public class PackCommand( string? msbuildPath = null ) : RestoringCommand(msbuildArgs, noRestore, msbuildPath: msbuildPath) { - public static PackCommand FromArgs(string[] args, string? msbuildPath = null) + public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { var parseResult = Parser.Parse(["dotnet", "pack", ..args]); return FromParseResult(parseResult, msbuildPath); } - public static PackCommand FromParseResult(ParseResult parseResult, string? msbuildPath = null) + public static CommandBase FromParseResult(ParseResult parseResult, string? msbuildPath = null) { - var msbuildArgs = parseResult.OptionValuesToBeForwarded(PackCommandParser.GetCommand()).Concat(parseResult.GetValue(PackCommandParser.SlnOrProjectArgument) ?? []); - - ReleasePropertyProjectLocator projectLocator = new(parseResult, MSBuildPropertyNames.PACK_RELEASE, - new ReleasePropertyProjectLocator.DependentCommandOptions( - parseResult.GetValue(PackCommandParser.SlnOrProjectArgument), - parseResult.HasOption(PackCommandParser.ConfigurationOption) ? parseResult.GetValue(PackCommandParser.ConfigurationOption) : null - ) - ); - - bool noRestore = parseResult.HasOption(PackCommandParser.NoRestoreOption) || parseResult.HasOption(PackCommandParser.NoBuildOption); - var parsedMSBuildArgs = MSBuildArgs.AnalyzeMSBuildArguments( - msbuildArgs, - CommonOptions.PropertiesOption, - CommonOptions.RestorePropertiesOption, - PackCommandParser.TargetOption, - PackCommandParser.VerbosityOption); - return new PackCommand( - parsedMSBuildArgs.CloneWithAdditionalProperties(projectLocator.GetCustomDefaultConfigurationValueIfSpecified()), - noRestore, + var args = parseResult.GetValue(PackCommandParser.SlnOrProjectOrFileArgument) ?? []; + + LoggerUtility.SeparateBinLogArguments(args, out var binLogArgs, out var nonBinLogArgs); + + bool noBuild = parseResult.HasOption(PackCommandParser.NoBuildOption); + + bool noRestore = noBuild || parseResult.HasOption(PackCommandParser.NoRestoreOption); + + return CommandFactory.CreateVirtualOrPhysicalCommand( + PackCommandParser.GetCommand(), + PackCommandParser.SlnOrProjectOrFileArgument, + (msbuildArgs, appFilePath) => new VirtualProjectBuildingCommand( + entryPointFileFullPath: Path.GetFullPath(appFilePath), + msbuildArgs: msbuildArgs) + { + NoBuild = noBuild, + NoRestore = noRestore, + NoCache = true, + }, + (msbuildArgs, msbuildPath) => + { + ReleasePropertyProjectLocator projectLocator = new(parseResult, MSBuildPropertyNames.PACK_RELEASE, + new ReleasePropertyProjectLocator.DependentCommandOptions( + nonBinLogArgs, + parseResult.HasOption(PackCommandParser.ConfigurationOption) ? parseResult.GetValue(PackCommandParser.ConfigurationOption) : null + ) + ); + return new PackCommand( + msbuildArgs.CloneWithAdditionalProperties(projectLocator.GetCustomDefaultConfigurationValueIfSpecified()), + noRestore, + msbuildPath); + }, + optionsToUseWhenParsingMSBuildFlags: + [ + CommonOptions.PropertiesOption, + CommonOptions.RestorePropertiesOption, + PackCommandParser.TargetOption, + PackCommandParser.VerbosityOption, + ], + parseResult, msbuildPath); } @@ -67,7 +87,7 @@ private static LogLevel MappingVerbosityToNugetLogLevel(VerbosityOptions? verbos public static int RunPackCommand(ParseResult parseResult) { - var args = parseResult.GetValue(PackCommandParser.SlnOrProjectArgument)?.ToList() ?? new List(); + var args = parseResult.GetValue(PackCommandParser.SlnOrProjectOrFileArgument)?.ToList() ?? new List(); if (args.Count != 1) { @@ -112,7 +132,7 @@ public static int Run(ParseResult parseResult) parseResult.HandleDebugSwitch(); parseResult.ShowHelpOrErrorIfAppropriate(); - var args = parseResult.GetValue(PackCommandParser.SlnOrProjectArgument)?.ToList() ?? new List(); + var args = parseResult.GetValue(PackCommandParser.SlnOrProjectOrFileArgument)?.ToList() ?? new List(); if (args.Count > 0 && Path.GetExtension(args[0]).Equals(".nuspec", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs b/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs index a402c037f8f6..eae738584119 100644 --- a/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs +++ b/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs @@ -1,16 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.ObjectModel; using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Build; using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Extensions; -using Microsoft.DotNet.Cli.NuGetPackageDownloader; -using NuGet.Commands; -using NuGet.Common; using NuGet.Versioning; -using static Microsoft.DotNet.Cli.Commands.Run.CSharpDirective; namespace Microsoft.DotNet.Cli.Commands.Pack; @@ -18,9 +13,9 @@ internal static class PackCommandParser { public static readonly string DocsLink = "https://aka.ms/dotnet-pack"; - public static readonly Argument> SlnOrProjectArgument = new(CliStrings.SolutionOrProjectArgumentName) + public static readonly Argument SlnOrProjectOrFileArgument = new(CliStrings.SolutionOrProjectOrFileArgumentName) { - Description = CliStrings.SolutionOrProjectArgumentDescription, + Description = CliStrings.SolutionOrProjectOrFileArgumentDescription, Arity = ArgumentArity.ZeroOrMore }; @@ -97,7 +92,7 @@ private static Command ConstructCommand() { var command = new DocumentedCommand("pack", DocsLink, CliCommandStrings.PackAppFullName); - command.Arguments.Add(SlnOrProjectArgument); + command.Arguments.Add(SlnOrProjectOrFileArgument); command.Options.Add(OutputOption); command.Options.Add(CommonOptions.ArtifactsPathOption); command.Options.Add(NoBuildOption); diff --git a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs index 08e23bfe9eca..d7e844915401 100644 --- a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs +++ b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs @@ -37,8 +37,6 @@ public static CommandBase FromParseResult(ParseResult parseResult, string? msbui CommonOptions.ValidateSelfContainedOptions(parseResult.HasOption(PublishCommandParser.SelfContainedOption), parseResult.HasOption(PublishCommandParser.NoSelfContainedOption)); - var forwardedOptions = parseResult.OptionValuesToBeForwarded(PublishCommandParser.GetCommand()); - bool noBuild = parseResult.HasOption(PublishCommandParser.NoBuildOption); bool noRestore = noBuild || parseResult.HasOption(PublishCommandParser.NoRestoreOption); diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index fa2104c5ce4d..eb853ec0f2f8 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -753,7 +753,7 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) isVirtualProject: true, targetFilePath: EntryPointFileFullPath, artifactsPath: ArtifactsPath, - includeRuntimeConfigInformation: !MSBuildArgs.RequestedTargets?.Contains("Publish") ?? true); + includeRuntimeConfigInformation: MSBuildArgs.RequestedTargets?.ContainsAny("Publish", "Pack") != true); var projectFileText = projectFileWriter.ToString(); using var reader = new StringReader(projectFileText); @@ -858,6 +858,7 @@ public static void WriteProjectFile( false {EscapeValue(artifactsPath)} artifacts/$(MSBuildProjectName) + artifacts/$(MSBuildProjectName) true diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetPackInvocation.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetPackInvocation.cs index cbaff7b19c1c..7fd85cf6e177 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetPackInvocation.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetPackInvocation.cs @@ -41,7 +41,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona .ToArray(); var msbuildPath = ""; - var command = PackCommand.FromArgs(args, msbuildPath); + var command = (PackCommand)PackCommand.FromArgs(args, msbuildPath); var expectedPrefix = args.FirstOrDefault() == "--no-build" ? ExpectedNoBuildPrefix : [.. ExpectedPrefix, .. GivenDotnetBuildInvocation.RestoreExpectedPrefixForImplicitRestore]; command.SeparateRestoreCommand.Should().BeNull(); diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index 091e2b0ba6fd..adb5ef3b6ded 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -1566,6 +1566,88 @@ public void Publish_In_SubDir() .And.NotHaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly); // no deps.json file for AOT-published app } + [Fact] + public void Pack() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "MyFileBasedTool.cs"); + File.WriteAllText(programFile, """ + #:property PackAsTool=true + Console.WriteLine($"Hello; EntryPointFilePath set? {AppContext.GetData("EntryPointFilePath") is string}"); + """); + + // Run unpacked. + new DotnetCommand(Log, "run", "MyFileBasedTool.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello; EntryPointFilePath set? True"); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + var outputDir = Path.Join(testInstance.Path, "artifacts"); + if (Directory.Exists(outputDir)) Directory.Delete(outputDir, recursive: true); + + // Pack. + new DotnetCommand(Log, "pack", "MyFileBasedTool.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + var packageDir = new DirectoryInfo(outputDir).Sub("MyFileBasedTool"); + packageDir.File("MyFileBasedTool.1.0.0.nupkg").Should().Exist(); + new DirectoryInfo(artifactsDir).Sub("package").Should().NotExist(); + + // Run the packed tool. + new DotnetCommand(Log, "tool", "exec", "MyFileBasedTool", "--yes", "--add-source", packageDir.FullName) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello; EntryPointFilePath set? False"); + } + + [Fact] + public void Pack_CustomPath() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "MyFileBasedTool.cs"); + File.WriteAllText(programFile, """ + #:property PackAsTool=true + #:property PackageOutputPath=custom + Console.WriteLine($"Hello; EntryPointFilePath set? {AppContext.GetData("EntryPointFilePath") is string}"); + """); + + // Run unpacked. + new DotnetCommand(Log, "run", "MyFileBasedTool.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello; EntryPointFilePath set? True"); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + var outputDir = Path.Join(testInstance.Path, "custom"); + if (Directory.Exists(outputDir)) Directory.Delete(outputDir, recursive: true); + + // Pack. + new DotnetCommand(Log, "pack", "MyFileBasedTool.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(outputDir).File("MyFileBasedTool.1.0.0.nupkg").Should().Exist(); + new DirectoryInfo(artifactsDir).Sub("package").Should().NotExist(); + + // Run the packed tool. + new DotnetCommand(Log, "tool", "exec", "MyFileBasedTool", "--yes", "--add-source", outputDir) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello; EntryPointFilePath set? False"); + } + [Fact] public void Clean() { @@ -2756,6 +2838,7 @@ public void Api() false /artifacts artifacts/$(MSBuildProjectName) + artifacts/$(MSBuildProjectName) true @@ -2824,6 +2907,7 @@ public void Api_Diagnostic_01() false /artifacts artifacts/$(MSBuildProjectName) + artifacts/$(MSBuildProjectName) true @@ -2889,6 +2973,7 @@ public void Api_Diagnostic_02() false /artifacts artifacts/$(MSBuildProjectName) + artifacts/$(MSBuildProjectName) true diff --git a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh index 81e2a63dddc0..55a3fca2ab34 100644 --- a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh +++ b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh @@ -558,7 +558,7 @@ _testhost() { '-r=[The target runtime to build for.]:RUNTIME_IDENTIFIER:->dotnet_dynamic_complete' \ '--help[Show command line help.]' \ '-h[Show command line help.]' \ - '*::PROJECT | SOLUTION -- The project or solution file to operate on. If a file is not specified, the command will search the current directory for one.: ' \ + '*::PROJECT | SOLUTION | FILE -- The project or solution or C# (file-based program) file to operate on. If a file is not specified, the command will search the current directory for a project or solution.: ' \ && ret=0 case $state in (dotnet_dynamic_complete)