Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions documentation/general/dotnet-run-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -97,7 +100,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.

Expand Down Expand Up @@ -369,7 +372,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`,
Expand Down
64 changes: 43 additions & 21 deletions src/Cli/dotnet/Commands/Pack/PackCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.CommandLine;
using Microsoft.DotNet.Cli.Commands.Restore;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;

Expand All @@ -14,35 +15,56 @@ 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)
{
parseResult.ShowHelpOrErrorIfAppropriate();

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);
}

Expand Down
6 changes: 3 additions & 3 deletions src/Cli/dotnet/Commands/Pack/PackCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ internal static class PackCommandParser
{
public static readonly string DocsLink = "https://aka.ms/dotnet-pack";

public static readonly Argument<IEnumerable<string>> SlnOrProjectArgument = new(CliStrings.SolutionOrProjectArgumentName)
public static readonly Argument<string[]> SlnOrProjectOrFileArgument = new(CliStrings.SolutionOrProjectOrFileArgumentName)
{
Description = CliStrings.SolutionOrProjectArgumentDescription,
Description = CliStrings.SolutionOrProjectOrFileArgumentDescription,
Arity = ArgumentArity.ZeroOrMore
};

Expand Down Expand Up @@ -72,7 +72,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);
Expand Down
2 changes: 0 additions & 2 deletions src/Cli/dotnet/Commands/Publish/PublishCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,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);
Expand Down Expand Up @@ -807,6 +807,7 @@ public static void WriteProjectFile(
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
<ArtifactsPath>{EscapeValue(artifactsPath)}</ArtifactsPath>
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona
.ToArray();

var msbuildPath = "<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();
Expand Down
44 changes: 44 additions & 0 deletions test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,47 @@ 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 Clean()
{
Expand Down Expand Up @@ -2648,6 +2689,7 @@ public void Api()
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
<ArtifactsPath>/artifacts</ArtifactsPath>
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
</PropertyGroup>

Expand Down Expand Up @@ -2715,6 +2757,7 @@ public void Api_Diagnostic_01()
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
<ArtifactsPath>/artifacts</ArtifactsPath>
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
</PropertyGroup>

Expand Down Expand Up @@ -2779,6 +2822,7 @@ public void Api_Diagnostic_02()
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
<ArtifactsPath>/artifacts</ArtifactsPath>
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,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)
Expand Down