Skip to content

Commit a90c5e2

Browse files
jjonesczMiYanni
andauthored
Allow publishing file-based apps (#49310)
Co-authored-by: Michael Yanni <[email protected]>
1 parent 303bc52 commit a90c5e2

File tree

11 files changed

+124
-33
lines changed

11 files changed

+124
-33
lines changed

documentation/general/dotnet-run-file.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ For example, the remaining command-line arguments after the first argument (the
6565
(except for the arguments recognized by `dotnet run` unless they are after the `--` separator)
6666
and working directory is not changed (e.g., `cd /x/ && dotnet run /y/file.cs` runs the program in directory `/x/`).
6767

68+
### Other commands
69+
70+
Commands `dotnet restore file.cs` and `dotnet build file.cs` are needed for IDE support and hence work for file-based programs.
71+
72+
Command `dotnet publish file.cs` is also supported for file-based programs.
73+
Note that file-based apps have implicitly set `PublishAot=true`, so publishing uses Native AOT (and building reports AOT warnings).
74+
To opt out, use `#:property PublishAot=false` directive in your `.cs` file.
75+
6876
## Entry points
6977

7078
If a file is given to `dotnet run`, it has to be an *entry-point file*, otherwise an error is reported.
@@ -308,9 +316,8 @@ which is needed if one wants to use `/usr/bin/env` to find the `dotnet` executab
308316
We could also consider making `dotnet file.cs` work because `dotnet file.dll` also works today
309317
but that would require changes to the native dotnet host.
310318

311-
### Other commands
319+
### Other possible commands
312320

313-
Commands `dotnet restore file.cs` and `dotnet build file.cs` are needed for IDE support and hence work for file-based programs.
314321
We can consider supporting other commands like `dotnet pack`, `dotnet watch`,
315322
however the primary scenario is `dotnet run` and we might never support additional commands.
316323

@@ -325,7 +332,6 @@ We could also add `dotnet compile` command that would be the equivalent of `dotn
325332
e.g., via `dotnet clean --file-based-program <path-to-entry-point>`
326333
or `dotnet clean --all-file-based-programs`.
327334

328-
329335
Adding package references via `dotnet package add` could be supported for file-based programs as well,
330336
i.e., the command would add a `#:package` directive to the top of a `.cs` file.
331337

src/Cli/dotnet/Commands/Build/BuildCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public static CommandBase FromParseResult(ParseResult parseResult, string msbuil
5151
{
5252
NoRestore = noRestore,
5353
NoCache = true,
54-
NoIncremental = noIncremental,
54+
BuildTarget = noIncremental ? "Rebuild" : "Build",
5555
};
5656
}
5757
else

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

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
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-
#nullable disable
5-
64
using System.CommandLine;
75
using Microsoft.DotNet.Cli.Commands.Restore;
6+
using Microsoft.DotNet.Cli.Commands.Run;
87
using Microsoft.DotNet.Cli.Extensions;
98

109
namespace Microsoft.DotNet.Cli.Commands.Publish;
@@ -14,49 +13,75 @@ public class PublishCommand : RestoringCommand
1413
private PublishCommand(
1514
IEnumerable<string> msbuildArgs,
1615
bool noRestore,
17-
string msbuildPath = null)
16+
string? msbuildPath = null)
1817
: base(msbuildArgs, noRestore, msbuildPath)
1918
{
2019
}
2120

22-
public static PublishCommand FromArgs(string[] args, string msbuildPath = null)
21+
public static CommandBase FromArgs(string[] args, string? msbuildPath = null)
2322
{
2423
var parser = Parser.Instance;
2524
var parseResult = parser.ParseFrom("dotnet publish", args);
2625
return FromParseResult(parseResult);
2726
}
2827

29-
public static PublishCommand FromParseResult(ParseResult parseResult, string msbuildPath = null)
28+
public static CommandBase FromParseResult(ParseResult parseResult, string? msbuildPath = null)
3029
{
3130
parseResult.HandleDebugSwitch();
3231
parseResult.ShowHelpOrErrorIfAppropriate();
3332

3433
var msbuildArgs = new List<string>()
3534
{
36-
"-target:Publish",
3735
"--property:_IsPublishing=true" // This property will not hold true for MSBuild /t:Publish or in VS.
3836
};
3937

40-
IEnumerable<string> slnOrProjectArgs = parseResult.GetValue(PublishCommandParser.SlnOrProjectArgument);
38+
string[] args = parseResult.GetValue(PublishCommandParser.SlnOrProjectOrFileArgument) ?? [];
39+
40+
LoggerUtility.SeparateBinLogArguments(args, out var binLogArgs, out var nonBinLogArgs);
4141

4242
CommonOptions.ValidateSelfContainedOptions(parseResult.HasOption(PublishCommandParser.SelfContainedOption),
4343
parseResult.HasOption(PublishCommandParser.NoSelfContainedOption));
4444

4545
msbuildArgs.AddRange(parseResult.OptionValuesToBeForwarded(PublishCommandParser.GetCommand()));
4646

47+
bool noBuild = parseResult.HasOption(PublishCommandParser.NoBuildOption);
48+
49+
bool noRestore = noBuild || parseResult.HasOption(PublishCommandParser.NoRestoreOption);
50+
51+
if (nonBinLogArgs is [{ } arg] && VirtualProjectBuildingCommand.IsValidEntryPointPath(arg))
52+
{
53+
if (!parseResult.HasOption(PublishCommandParser.ConfigurationOption))
54+
{
55+
msbuildArgs.Add("-p:Configuration=Release");
56+
}
57+
58+
msbuildArgs.AddRange(binLogArgs);
59+
60+
return new VirtualProjectBuildingCommand(
61+
entryPointFileFullPath: Path.GetFullPath(arg),
62+
msbuildArgs: msbuildArgs,
63+
verbosity: parseResult.GetValue(CommonOptions.VerbosityOption),
64+
interactive: parseResult.GetValue(CommonOptions.InteractiveMsBuildForwardOption))
65+
{
66+
NoBuild = noBuild,
67+
NoRestore = noRestore,
68+
NoCache = true,
69+
BuildTarget = "Publish",
70+
};
71+
}
72+
4773
ReleasePropertyProjectLocator projectLocator = new(parseResult, MSBuildPropertyNames.PUBLISH_RELEASE,
4874
new ReleasePropertyProjectLocator.DependentCommandOptions(
49-
parseResult.GetValue(PublishCommandParser.SlnOrProjectArgument),
75+
nonBinLogArgs,
5076
parseResult.HasOption(PublishCommandParser.ConfigurationOption) ? parseResult.GetValue(PublishCommandParser.ConfigurationOption) : null,
5177
parseResult.HasOption(PublishCommandParser.FrameworkOption) ? parseResult.GetValue(PublishCommandParser.FrameworkOption) : null
5278
)
5379
);
5480
msbuildArgs.AddRange(projectLocator.GetCustomDefaultConfigurationValueIfSpecified());
5581

56-
msbuildArgs.AddRange(slnOrProjectArgs ?? []);
82+
msbuildArgs.AddRange(args ?? []);
5783

58-
bool noRestore = parseResult.HasOption(PublishCommandParser.NoRestoreOption)
59-
|| parseResult.HasOption(PublishCommandParser.NoBuildOption);
84+
msbuildArgs.Insert(0, "-target:Publish");
6085

6186
return new PublishCommand(
6287
msbuildArgs,

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ internal static class PublishCommandParser
1313
{
1414
public static readonly string DocsLink = "https://aka.ms/dotnet-publish";
1515

16-
public static readonly Argument<IEnumerable<string>> SlnOrProjectArgument = new(CliStrings.SolutionOrProjectArgumentName)
16+
public static readonly Argument<string[]> SlnOrProjectOrFileArgument = new(CliStrings.SolutionOrProjectOrFileArgumentName)
1717
{
18-
Description = CliStrings.SolutionOrProjectArgumentDescription,
18+
Description = CliStrings.SolutionOrProjectOrFileArgumentDescription,
1919
Arity = ArgumentArity.ZeroOrMore
2020
};
2121

@@ -67,7 +67,7 @@ private static Command ConstructCommand()
6767
{
6868
var command = new DocumentedCommand("publish", DocsLink, CliCommandStrings.PublishAppDescription);
6969

70-
command.Arguments.Add(SlnOrProjectArgument);
70+
command.Arguments.Add(SlnOrProjectOrFileArgument);
7171
RestoreCommandParser.AddImplicitRestoreOptions(command, includeRuntimeOption: false, includeNoDependenciesOption: true);
7272

7373
command.Options.Add(OutputOption);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ internal static class CommonRunHelpers
1111
/// <param name="globalProperties">
1212
/// Should have <see cref="StringComparer.OrdinalIgnoreCase"/>.
1313
/// </param>
14-
public static void AddUserPassedProperties(Dictionary<string, string> globalProperties, string[] args)
14+
public static void AddUserPassedProperties(Dictionary<string, string> globalProperties, IReadOnlyList<string> args)
1515
{
1616
Debug.Assert(globalProperties.Comparer == StringComparer.OrdinalIgnoreCase);
1717

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ internal sealed class VirtualProjectBuildingCommand : CommandBase
6262

6363
public VirtualProjectBuildingCommand(
6464
string entryPointFileFullPath,
65-
string[] msbuildArgs,
65+
IReadOnlyList<string> msbuildArgs,
6666
VerbosityOptions? verbosity,
6767
bool interactive)
6868
{
@@ -77,13 +77,13 @@ public VirtualProjectBuildingCommand(
7777

7878
public string EntryPointFileFullPath { get; }
7979
public Dictionary<string, string> GlobalProperties { get; }
80-
public string[] BinaryLoggerArgs { get; }
80+
public IReadOnlyList<string> BinaryLoggerArgs { get; }
8181
public VerbosityOptions Verbosity { get; }
8282
public string? CustomArtifactsPath { get; init; }
8383
public bool NoRestore { get; init; }
8484
public bool NoCache { get; init; }
8585
public bool NoBuild { get; init; }
86-
public bool NoIncremental { get; init; }
86+
public string BuildTarget { get; init; } = "Build";
8787

8888
public override int Execute()
8989
{
@@ -165,7 +165,7 @@ public override int Execute()
165165
{
166166
var buildRequest = new BuildRequestData(
167167
CreateProjectInstance(projectCollection),
168-
targetsToBuild: [NoIncremental ? "Rebuild" : "Build"]);
168+
targetsToBuild: [BuildTarget]);
169169
var buildResult = BuildManager.DefaultBuildManager.BuildRequest(buildRequest);
170170
if (buildResult.OverallResult != BuildResultCode.Success)
171171
{
@@ -196,10 +196,10 @@ public override int Execute()
196196
consoleLogger.Shutdown();
197197
}
198198

199-
static ILogger? GetBinaryLogger(string[] args)
199+
static ILogger? GetBinaryLogger(IReadOnlyList<string> args)
200200
{
201201
// Like in MSBuild, only the last binary logger is used.
202-
for (int i = args.Length - 1; i >= 0; i--)
202+
for (int i = args.Count - 1; i >= 0; i--)
203203
{
204204
var arg = args[i];
205205
if (LoggerUtility.IsBinLogArgument(arg))
@@ -535,6 +535,7 @@ public static void WriteProjectFile(
535535
<TargetFramework>net10.0</TargetFramework>
536536
<ImplicitUsings>enable</ImplicitUsings>
537537
<Nullable>enable</Nullable>
538+
<PublishAot>true</PublishAot>
538539
</PropertyGroup>
539540
""");
540541

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona
4949
.ToArray();
5050

5151
var msbuildPath = "<msbuildpath>";
52-
var command = PublishCommand.FromArgs(args, msbuildPath);
52+
var command = (PublishCommand)PublishCommand.FromArgs(args, msbuildPath);
5353

5454
command.SeparateRestoreCommand
5555
.Should()
@@ -67,7 +67,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona
6767
public void MsbuildInvocationIsCorrectForSeparateRestore(string[] args, string[] expectedAdditionalArgs)
6868
{
6969
var msbuildPath = "<msbuildpath>";
70-
var command = PublishCommand.FromArgs(args, msbuildPath);
70+
var command = (PublishCommand)PublishCommand.FromArgs(args, msbuildPath);
7171

7272
var restoreTokens =
7373
command.SeparateRestoreCommand
@@ -92,7 +92,7 @@ public void MsbuildInvocationIsCorrectForSeparateRestore(string[] args, string[]
9292
public void MsbuildInvocationIsCorrectForNoBuild()
9393
{
9494
var msbuildPath = "<msbuildpath>";
95-
var command = PublishCommand.FromArgs(new[] { "--no-build" }, msbuildPath);
95+
var command = (PublishCommand)PublishCommand.FromArgs(new[] { "--no-build" }, msbuildPath);
9696

9797
command.SeparateRestoreCommand
9898
.Should()
@@ -107,7 +107,7 @@ public void MsbuildInvocationIsCorrectForNoBuild()
107107
public void CommandAcceptsMultipleCustomProperties()
108108
{
109109
var msbuildPath = "<msbuildpath>";
110-
var command = PublishCommand.FromArgs(new[] { "/p:Prop1=prop1", "/p:Prop2=prop2" }, msbuildPath);
110+
var command = (PublishCommand)PublishCommand.FromArgs(new[] { "/p:Prop1=prop1", "/p:Prop2=prop2" }, msbuildPath);
111111

112112
command.GetArgumentTokensToMSBuild()
113113
.Should()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ public MSBuildArgumentCommandLineParserTests(ITestOutputHelper output)
3333
public void MSBuildArgumentsAreForwardedCorrectly(string[] arguments, bool buildCommand)
3434
{
3535
RestoringCommand command = buildCommand ?
36-
((RestoringCommand)BuildCommand.FromArgs(arguments)) :
37-
PublishCommand.FromArgs(arguments);
36+
(RestoringCommand)BuildCommand.FromArgs(arguments) :
37+
(RestoringCommand)PublishCommand.FromArgs(arguments);
3838
var expectedArguments = arguments.Select(a => a.Replace("-property:", "--property:").Replace("-p:", "--property:"));
3939
var argString = command.MSBuildArguments;
4040

test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ public void SameAsTemplate()
5555

5656
var dotnetProjectConvertProjectText = File.ReadAllText(dotnetProjectConvertProject);
5757
var dotnetNewConsoleProjectText = File.ReadAllText(dotnetNewConsoleProject);
58-
dotnetProjectConvertProjectText.Should().Be(dotnetNewConsoleProjectText)
58+
59+
// There are some differences: we add PublishAot=true.
60+
var patchedDotnetProjectConvertProjectText = dotnetProjectConvertProjectText
61+
.Replace(" <PublishAot>true</PublishAot>" + Environment.NewLine, string.Empty);
62+
63+
patchedDotnetProjectConvertProjectText.Should().Be(dotnetNewConsoleProjectText)
5964
.And.StartWith("""<Project Sdk="Microsoft.NET.Sdk">""");
6065
}
6166

@@ -315,6 +320,7 @@ public void ProcessingSucceeds()
315320
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
316321
<ImplicitUsings>enable</ImplicitUsings>
317322
<Nullable>enable</Nullable>
323+
<PublishAot>true</PublishAot>
318324
</PropertyGroup>
319325
320326
</Project>
@@ -345,6 +351,7 @@ public void Directives()
345351
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
346352
<ImplicitUsings>enable</ImplicitUsings>
347353
<Nullable>enable</Nullable>
354+
<PublishAot>true</PublishAot>
348355
</PropertyGroup>
349356
350357
<PropertyGroup>
@@ -380,6 +387,7 @@ public void Directives_Variable()
380387
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
381388
<ImplicitUsings>enable</ImplicitUsings>
382389
<Nullable>enable</Nullable>
390+
<PublishAot>true</PublishAot>
383391
</PropertyGroup>
384392
385393
<PropertyGroup>
@@ -421,6 +429,7 @@ public void Directives_Separators()
421429
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
422430
<ImplicitUsings>enable</ImplicitUsings>
423431
<Nullable>enable</Nullable>
432+
<PublishAot>true</PublishAot>
424433
</PropertyGroup>
425434
426435
<PropertyGroup>
@@ -532,6 +541,7 @@ public void Directives_Escaping()
532541
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
533542
<ImplicitUsings>enable</ImplicitUsings>
534543
<Nullable>enable</Nullable>
544+
<PublishAot>true</PublishAot>
535545
</PropertyGroup>
536546
537547
<PropertyGroup>
@@ -568,6 +578,7 @@ public void Directives_Whitespace()
568578
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
569579
<ImplicitUsings>enable</ImplicitUsings>
570580
<Nullable>enable</Nullable>
581+
<PublishAot>true</PublishAot>
571582
</PropertyGroup>
572583
573584
<PropertyGroup>
@@ -614,6 +625,7 @@ public void Directives_AfterToken()
614625
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
615626
<ImplicitUsings>enable</ImplicitUsings>
616627
<Nullable>enable</Nullable>
628+
<PublishAot>true</PublishAot>
617629
</PropertyGroup>
618630
619631
<PropertyGroup>
@@ -662,6 +674,7 @@ public void Directives_AfterIf()
662674
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
663675
<ImplicitUsings>enable</ImplicitUsings>
664676
<Nullable>enable</Nullable>
677+
<PublishAot>true</PublishAot>
665678
</PropertyGroup>
666679
667680
<PropertyGroup>
@@ -707,6 +720,7 @@ public void Directives_Comments()
707720
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
708721
<ImplicitUsings>enable</ImplicitUsings>
709722
<Nullable>enable</Nullable>
723+
<PublishAot>true</PublishAot>
710724
</PropertyGroup>
711725
712726
<PropertyGroup>

0 commit comments

Comments
 (0)