Skip to content

Commit df34bde

Browse files
authored
Support dotnet pack file.cs (#50168)
1 parent 3b8cc56 commit df34bde

File tree

8 files changed

+143
-41
lines changed

8 files changed

+143
-41
lines changed

documentation/general/dotnet-run-file.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ Additionally, the implicit project file has the following customizations:
3838

3939
- `ArtifactsPath` is set to a [temp directory](#build-outputs).
4040

41-
- `RuntimeHostConfigurationOption`s are set for `EntryPointFilePath` and `EntryPointFileDirectoryPath` which can be accessed in the app via `AppContext`:
41+
- `PublishDir` and `PackageOutputPath` are set to `./artifacts/` so the outputs of `dotnet publish` and `dotnet pack` are next to the file-based app.
42+
43+
- `RuntimeHostConfigurationOption`s are set for `EntryPointFilePath` and `EntryPointFileDirectoryPath` (except for `Publish` and `Pack` targets)
44+
which can be accessed in the app via `AppContext`:
4245

4346
```cs
4447
string? filePath = AppContext.GetData("EntryPointFilePath") as string;
@@ -101,7 +104,7 @@ the compilation consists solely of the single file read from the standard input.
101104

102105
Commands `dotnet restore file.cs` and `dotnet build file.cs` are needed for IDE support and hence work for file-based programs.
103106

104-
Command `dotnet publish file.cs` is also supported for file-based programs.
107+
Commands `dotnet publish file.cs` and `dotnet pack file.cs` are also supported for file-based programs.
105108
Note that file-based apps have implicitly set `PublishAot=true`, so publishing uses Native AOT (and building reports AOT warnings).
106109
To opt out, use `#:property PublishAot=false` directive in your `.cs` file.
107110

@@ -373,7 +376,7 @@ so `dotnet file.cs` instead of `dotnet run file.cs` should be used in shebangs:
373376

374377
### Other possible commands
375378

376-
We can consider supporting other commands like `dotnet pack`, `dotnet watch`,
379+
We can consider supporting other commands like `dotnet watch`,
377380
however the primary scenario is `dotnet run` and we might never support additional commands.
378381

379382
All commands supporting file-based programs should have a way to receive the target path similarly to `dotnet run`,

src/Cli/dotnet/Commands/Pack/PackCommand.cs

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@
44
using System.Collections.ObjectModel;
55
using System.CommandLine;
66
using System.CommandLine.Parsing;
7-
using System.Configuration;
87
using Microsoft.DotNet.Cli.Commands.Build;
98
using Microsoft.DotNet.Cli.Commands.Restore;
9+
using Microsoft.DotNet.Cli.Commands.Run;
1010
using Microsoft.DotNet.Cli.Extensions;
1111
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
1212
using Microsoft.DotNet.Cli.Utils;
1313
using NuGet.Commands;
1414
using NuGet.Common;
1515
using NuGet.Packaging;
16-
using NuGet.Packaging.Core;
1716

1817
namespace Microsoft.DotNet.Cli.Commands.Pack;
1918

@@ -23,33 +22,54 @@ public class PackCommand(
2322
string? msbuildPath = null
2423
) : RestoringCommand(msbuildArgs, noRestore, msbuildPath: msbuildPath)
2524
{
26-
public static PackCommand FromArgs(string[] args, string? msbuildPath = null)
25+
public static CommandBase FromArgs(string[] args, string? msbuildPath = null)
2726
{
2827
var parseResult = Parser.Parse(["dotnet", "pack", ..args]);
2928
return FromParseResult(parseResult, msbuildPath);
3029
}
3130

32-
public static PackCommand FromParseResult(ParseResult parseResult, string? msbuildPath = null)
31+
public static CommandBase FromParseResult(ParseResult parseResult, string? msbuildPath = null)
3332
{
34-
var msbuildArgs = parseResult.OptionValuesToBeForwarded(PackCommandParser.GetCommand()).Concat(parseResult.GetValue(PackCommandParser.SlnOrProjectArgument) ?? []);
35-
36-
ReleasePropertyProjectLocator projectLocator = new(parseResult, MSBuildPropertyNames.PACK_RELEASE,
37-
new ReleasePropertyProjectLocator.DependentCommandOptions(
38-
parseResult.GetValue(PackCommandParser.SlnOrProjectArgument),
39-
parseResult.HasOption(PackCommandParser.ConfigurationOption) ? parseResult.GetValue(PackCommandParser.ConfigurationOption) : null
40-
)
41-
);
42-
43-
bool noRestore = parseResult.HasOption(PackCommandParser.NoRestoreOption) || parseResult.HasOption(PackCommandParser.NoBuildOption);
44-
var parsedMSBuildArgs = MSBuildArgs.AnalyzeMSBuildArguments(
45-
msbuildArgs,
46-
CommonOptions.PropertiesOption,
47-
CommonOptions.RestorePropertiesOption,
48-
PackCommandParser.TargetOption,
49-
PackCommandParser.VerbosityOption);
50-
return new PackCommand(
51-
parsedMSBuildArgs.CloneWithAdditionalProperties(projectLocator.GetCustomDefaultConfigurationValueIfSpecified()),
52-
noRestore,
33+
var args = parseResult.GetValue(PackCommandParser.SlnOrProjectOrFileArgument) ?? [];
34+
35+
LoggerUtility.SeparateBinLogArguments(args, out var binLogArgs, out var nonBinLogArgs);
36+
37+
bool noBuild = parseResult.HasOption(PackCommandParser.NoBuildOption);
38+
39+
bool noRestore = noBuild || parseResult.HasOption(PackCommandParser.NoRestoreOption);
40+
41+
return CommandFactory.CreateVirtualOrPhysicalCommand(
42+
PackCommandParser.GetCommand(),
43+
PackCommandParser.SlnOrProjectOrFileArgument,
44+
(msbuildArgs, appFilePath) => new VirtualProjectBuildingCommand(
45+
entryPointFileFullPath: Path.GetFullPath(appFilePath),
46+
msbuildArgs: msbuildArgs)
47+
{
48+
NoBuild = noBuild,
49+
NoRestore = noRestore,
50+
NoCache = true,
51+
},
52+
(msbuildArgs, msbuildPath) =>
53+
{
54+
ReleasePropertyProjectLocator projectLocator = new(parseResult, MSBuildPropertyNames.PACK_RELEASE,
55+
new ReleasePropertyProjectLocator.DependentCommandOptions(
56+
nonBinLogArgs,
57+
parseResult.HasOption(PackCommandParser.ConfigurationOption) ? parseResult.GetValue(PackCommandParser.ConfigurationOption) : null
58+
)
59+
);
60+
return new PackCommand(
61+
msbuildArgs.CloneWithAdditionalProperties(projectLocator.GetCustomDefaultConfigurationValueIfSpecified()),
62+
noRestore,
63+
msbuildPath);
64+
},
65+
optionsToUseWhenParsingMSBuildFlags:
66+
[
67+
CommonOptions.PropertiesOption,
68+
CommonOptions.RestorePropertiesOption,
69+
PackCommandParser.TargetOption,
70+
PackCommandParser.VerbosityOption,
71+
],
72+
parseResult,
5373
msbuildPath);
5474
}
5575

@@ -67,7 +87,7 @@ private static LogLevel MappingVerbosityToNugetLogLevel(VerbosityOptions? verbos
6787

6888
public static int RunPackCommand(ParseResult parseResult)
6989
{
70-
var args = parseResult.GetValue(PackCommandParser.SlnOrProjectArgument)?.ToList() ?? new List<string>();
90+
var args = parseResult.GetValue(PackCommandParser.SlnOrProjectOrFileArgument)?.ToList() ?? new List<string>();
7191

7292
if (args.Count != 1)
7393
{
@@ -112,7 +132,7 @@ public static int Run(ParseResult parseResult)
112132
parseResult.HandleDebugSwitch();
113133
parseResult.ShowHelpOrErrorIfAppropriate();
114134

115-
var args = parseResult.GetValue(PackCommandParser.SlnOrProjectArgument)?.ToList() ?? new List<string>();
135+
var args = parseResult.GetValue(PackCommandParser.SlnOrProjectOrFileArgument)?.ToList() ?? new List<string>();
116136

117137
if (args.Count > 0 && Path.GetExtension(args[0]).Equals(".nuspec", StringComparison.OrdinalIgnoreCase))
118138
{

src/Cli/dotnet/Commands/Pack/PackCommandParser.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.ObjectModel;
54
using System.CommandLine;
65
using Microsoft.DotNet.Cli.Commands.Build;
76
using Microsoft.DotNet.Cli.Commands.Restore;
87
using Microsoft.DotNet.Cli.Extensions;
9-
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
10-
using NuGet.Commands;
11-
using NuGet.Common;
128
using NuGet.Versioning;
13-
using static Microsoft.DotNet.Cli.Commands.Run.CSharpDirective;
149

1510
namespace Microsoft.DotNet.Cli.Commands.Pack;
1611

1712
internal static class PackCommandParser
1813
{
1914
public static readonly string DocsLink = "https://aka.ms/dotnet-pack";
2015

21-
public static readonly Argument<IEnumerable<string>> SlnOrProjectArgument = new(CliStrings.SolutionOrProjectArgumentName)
16+
public static readonly Argument<string[]> SlnOrProjectOrFileArgument = new(CliStrings.SolutionOrProjectOrFileArgumentName)
2217
{
23-
Description = CliStrings.SolutionOrProjectArgumentDescription,
18+
Description = CliStrings.SolutionOrProjectOrFileArgumentDescription,
2419
Arity = ArgumentArity.ZeroOrMore
2520
};
2621

@@ -97,7 +92,7 @@ private static Command ConstructCommand()
9792
{
9893
var command = new DocumentedCommand("pack", DocsLink, CliCommandStrings.PackAppFullName);
9994

100-
command.Arguments.Add(SlnOrProjectArgument);
95+
command.Arguments.Add(SlnOrProjectOrFileArgument);
10196
command.Options.Add(OutputOption);
10297
command.Options.Add(CommonOptions.ArtifactsPathOption);
10398
command.Options.Add(NoBuildOption);

src/Cli/dotnet/Commands/Publish/PublishCommand.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ public static CommandBase FromParseResult(ParseResult parseResult, string? msbui
3737
CommonOptions.ValidateSelfContainedOptions(parseResult.HasOption(PublishCommandParser.SelfContainedOption),
3838
parseResult.HasOption(PublishCommandParser.NoSelfContainedOption));
3939

40-
var forwardedOptions = parseResult.OptionValuesToBeForwarded(PublishCommandParser.GetCommand());
41-
4240
bool noBuild = parseResult.HasOption(PublishCommandParser.NoBuildOption);
4341

4442
bool noRestore = noBuild || parseResult.HasOption(PublishCommandParser.NoRestoreOption);

src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection)
753753
isVirtualProject: true,
754754
targetFilePath: EntryPointFileFullPath,
755755
artifactsPath: ArtifactsPath,
756-
includeRuntimeConfigInformation: !MSBuildArgs.RequestedTargets?.Contains("Publish") ?? true);
756+
includeRuntimeConfigInformation: MSBuildArgs.RequestedTargets?.ContainsAny("Publish", "Pack") != true);
757757
var projectFileText = projectFileWriter.ToString();
758758

759759
using var reader = new StringReader(projectFileText);
@@ -858,6 +858,7 @@ public static void WriteProjectFile(
858858
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
859859
<ArtifactsPath>{EscapeValue(artifactsPath)}</ArtifactsPath>
860860
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
861+
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
861862
<FileBasedProgram>true</FileBasedProgram>
862863
</PropertyGroup>
863864

test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetPackInvocation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona
4141
.ToArray();
4242

4343
var msbuildPath = "<msbuildpath>";
44-
var command = PackCommand.FromArgs(args, msbuildPath);
44+
var command = (PackCommand)PackCommand.FromArgs(args, msbuildPath);
4545
var expectedPrefix = args.FirstOrDefault() == "--no-build" ? ExpectedNoBuildPrefix : [.. ExpectedPrefix, .. GivenDotnetBuildInvocation.RestoreExpectedPrefixForImplicitRestore];
4646

4747
command.SeparateRestoreCommand.Should().BeNull();

test/dotnet.Tests/CommandTests/Run/RunFileTests.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,88 @@ public void Publish_In_SubDir()
15661566
.And.NotHaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly); // no deps.json file for AOT-published app
15671567
}
15681568

1569+
[Fact]
1570+
public void Pack()
1571+
{
1572+
var testInstance = _testAssetsManager.CreateTestDirectory();
1573+
var programFile = Path.Join(testInstance.Path, "MyFileBasedTool.cs");
1574+
File.WriteAllText(programFile, """
1575+
#:property PackAsTool=true
1576+
Console.WriteLine($"Hello; EntryPointFilePath set? {AppContext.GetData("EntryPointFilePath") is string}");
1577+
""");
1578+
1579+
// Run unpacked.
1580+
new DotnetCommand(Log, "run", "MyFileBasedTool.cs")
1581+
.WithWorkingDirectory(testInstance.Path)
1582+
.Execute()
1583+
.Should().Pass()
1584+
.And.HaveStdOut("Hello; EntryPointFilePath set? True");
1585+
1586+
var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
1587+
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
1588+
1589+
var outputDir = Path.Join(testInstance.Path, "artifacts");
1590+
if (Directory.Exists(outputDir)) Directory.Delete(outputDir, recursive: true);
1591+
1592+
// Pack.
1593+
new DotnetCommand(Log, "pack", "MyFileBasedTool.cs")
1594+
.WithWorkingDirectory(testInstance.Path)
1595+
.Execute()
1596+
.Should().Pass();
1597+
1598+
var packageDir = new DirectoryInfo(outputDir).Sub("MyFileBasedTool");
1599+
packageDir.File("MyFileBasedTool.1.0.0.nupkg").Should().Exist();
1600+
new DirectoryInfo(artifactsDir).Sub("package").Should().NotExist();
1601+
1602+
// Run the packed tool.
1603+
new DotnetCommand(Log, "tool", "exec", "MyFileBasedTool", "--yes", "--add-source", packageDir.FullName)
1604+
.WithWorkingDirectory(testInstance.Path)
1605+
.Execute()
1606+
.Should().Pass()
1607+
.And.HaveStdOutContaining("Hello; EntryPointFilePath set? False");
1608+
}
1609+
1610+
[Fact]
1611+
public void Pack_CustomPath()
1612+
{
1613+
var testInstance = _testAssetsManager.CreateTestDirectory();
1614+
var programFile = Path.Join(testInstance.Path, "MyFileBasedTool.cs");
1615+
File.WriteAllText(programFile, """
1616+
#:property PackAsTool=true
1617+
#:property PackageOutputPath=custom
1618+
Console.WriteLine($"Hello; EntryPointFilePath set? {AppContext.GetData("EntryPointFilePath") is string}");
1619+
""");
1620+
1621+
// Run unpacked.
1622+
new DotnetCommand(Log, "run", "MyFileBasedTool.cs")
1623+
.WithWorkingDirectory(testInstance.Path)
1624+
.Execute()
1625+
.Should().Pass()
1626+
.And.HaveStdOut("Hello; EntryPointFilePath set? True");
1627+
1628+
var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
1629+
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
1630+
1631+
var outputDir = Path.Join(testInstance.Path, "custom");
1632+
if (Directory.Exists(outputDir)) Directory.Delete(outputDir, recursive: true);
1633+
1634+
// Pack.
1635+
new DotnetCommand(Log, "pack", "MyFileBasedTool.cs")
1636+
.WithWorkingDirectory(testInstance.Path)
1637+
.Execute()
1638+
.Should().Pass();
1639+
1640+
new DirectoryInfo(outputDir).File("MyFileBasedTool.1.0.0.nupkg").Should().Exist();
1641+
new DirectoryInfo(artifactsDir).Sub("package").Should().NotExist();
1642+
1643+
// Run the packed tool.
1644+
new DotnetCommand(Log, "tool", "exec", "MyFileBasedTool", "--yes", "--add-source", outputDir)
1645+
.WithWorkingDirectory(testInstance.Path)
1646+
.Execute()
1647+
.Should().Pass()
1648+
.And.HaveStdOutContaining("Hello; EntryPointFilePath set? False");
1649+
}
1650+
15691651
[Fact]
15701652
public void Clean()
15711653
{
@@ -2756,6 +2838,7 @@ public void Api()
27562838
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
27572839
<ArtifactsPath>/artifacts</ArtifactsPath>
27582840
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
2841+
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
27592842
<FileBasedProgram>true</FileBasedProgram>
27602843
</PropertyGroup>
27612844
@@ -2824,6 +2907,7 @@ public void Api_Diagnostic_01()
28242907
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
28252908
<ArtifactsPath>/artifacts</ArtifactsPath>
28262909
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
2910+
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
28272911
<FileBasedProgram>true</FileBasedProgram>
28282912
</PropertyGroup>
28292913
@@ -2889,6 +2973,7 @@ public void Api_Diagnostic_02()
28892973
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
28902974
<ArtifactsPath>/artifacts</ArtifactsPath>
28912975
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
2976+
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
28922977
<FileBasedProgram>true</FileBasedProgram>
28932978
</PropertyGroup>
28942979

test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ _testhost() {
558558
'-r=[The target runtime to build for.]:RUNTIME_IDENTIFIER:->dotnet_dynamic_complete' \
559559
'--help[Show command line help.]' \
560560
'-h[Show command line help.]' \
561-
'*::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.: ' \
561+
'*::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.: ' \
562562
&& ret=0
563563
case $state in
564564
(dotnet_dynamic_complete)

0 commit comments

Comments
 (0)