diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index 4e0894793a1f..4f9012c41ab0 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -64,6 +64,14 @@ For example, the remaining command-line arguments after the first argument (the (except for the arguments recognized by `dotnet run` unless they are after the `--` separator) and working directory is not changed (e.g., `cd /x/ && dotnet run /y/file.cs` runs the program in directory `/x/`). +### Other commands + +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. +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. + ## Entry points If a file is given to `dotnet run`, it has to be an *entry-point file*, otherwise an error is reported. @@ -302,9 +310,8 @@ which is needed if one wants to use `/usr/bin/env` to find the `dotnet` executab We could also consider making `dotnet file.cs` work because `dotnet file.dll` also works today but that would require changes to the native dotnet host. -### Other commands +### Other possible commands -Commands `dotnet restore file.cs` and `dotnet build file.cs` are needed for IDE support and hence work for file-based programs. We can consider supporting other commands like `dotnet pack`, `dotnet watch`, however the primary scenario is `dotnet run` and we might never support additional commands. @@ -319,7 +326,6 @@ We could also add `dotnet compile` command that would be the equivalent of `dotn e.g., via `dotnet clean --file-based-program ` or `dotnet clean --all-file-based-programs`. - Adding package references via `dotnet package add` could be supported for file-based programs as well, i.e., the command would add a `#:package` directive to the top of a `.cs` file. diff --git a/src/Cli/dotnet/Commands/Build/BuildCommand.cs b/src/Cli/dotnet/Commands/Build/BuildCommand.cs index b2714d6090a9..dbceafe07006 100644 --- a/src/Cli/dotnet/Commands/Build/BuildCommand.cs +++ b/src/Cli/dotnet/Commands/Build/BuildCommand.cs @@ -51,7 +51,7 @@ public static CommandBase FromParseResult(ParseResult parseResult, string msbuil { NoRestore = noRestore, NoCache = true, - NoIncremental = noIncremental, + BuildTarget = noIncremental ? "Rebuild" : "Build", }; } else diff --git a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs index e830e1490456..a4c407980b2f 100644 --- a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs +++ b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs @@ -1,10 +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.Restore; +using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Extensions; namespace Microsoft.DotNet.Cli.Commands.Publish; @@ -14,49 +13,75 @@ public class PublishCommand : RestoringCommand private PublishCommand( IEnumerable msbuildArgs, bool noRestore, - string msbuildPath = null) + string? msbuildPath = null) : base(msbuildArgs, noRestore, msbuildPath) { } - public static PublishCommand FromArgs(string[] args, string msbuildPath = null) + public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { var parser = Parser.Instance; var parseResult = parser.ParseFrom("dotnet publish", args); return FromParseResult(parseResult); } - public static PublishCommand FromParseResult(ParseResult parseResult, string msbuildPath = null) + public static CommandBase FromParseResult(ParseResult parseResult, string? msbuildPath = null) { parseResult.HandleDebugSwitch(); parseResult.ShowHelpOrErrorIfAppropriate(); var msbuildArgs = new List() { - "-target:Publish", "--property:_IsPublishing=true" // This property will not hold true for MSBuild /t:Publish or in VS. }; - IEnumerable slnOrProjectArgs = parseResult.GetValue(PublishCommandParser.SlnOrProjectArgument); + string[] args = parseResult.GetValue(PublishCommandParser.SlnOrProjectOrFileArgument) ?? []; + + LoggerUtility.SeparateBinLogArguments(args, out var binLogArgs, out var nonBinLogArgs); CommonOptions.ValidateSelfContainedOptions(parseResult.HasOption(PublishCommandParser.SelfContainedOption), parseResult.HasOption(PublishCommandParser.NoSelfContainedOption)); msbuildArgs.AddRange(parseResult.OptionValuesToBeForwarded(PublishCommandParser.GetCommand())); + bool noBuild = parseResult.HasOption(PublishCommandParser.NoBuildOption); + + bool noRestore = noBuild || parseResult.HasOption(PublishCommandParser.NoRestoreOption); + + if (nonBinLogArgs is [{ } arg] && VirtualProjectBuildingCommand.IsValidEntryPointPath(arg)) + { + if (!parseResult.HasOption(PublishCommandParser.ConfigurationOption)) + { + msbuildArgs.Add("-p:Configuration=Release"); + } + + msbuildArgs.AddRange(binLogArgs); + + return new VirtualProjectBuildingCommand( + entryPointFileFullPath: Path.GetFullPath(arg), + msbuildArgs: msbuildArgs, + verbosity: parseResult.GetValue(CommonOptions.VerbosityOption), + interactive: parseResult.GetValue(CommonOptions.InteractiveMsBuildForwardOption)) + { + NoBuild = noBuild, + NoRestore = noRestore, + NoCache = true, + BuildTarget = "Publish", + }; + } + ReleasePropertyProjectLocator projectLocator = new(parseResult, MSBuildPropertyNames.PUBLISH_RELEASE, new ReleasePropertyProjectLocator.DependentCommandOptions( - parseResult.GetValue(PublishCommandParser.SlnOrProjectArgument), + nonBinLogArgs, parseResult.HasOption(PublishCommandParser.ConfigurationOption) ? parseResult.GetValue(PublishCommandParser.ConfigurationOption) : null, parseResult.HasOption(PublishCommandParser.FrameworkOption) ? parseResult.GetValue(PublishCommandParser.FrameworkOption) : null ) ); msbuildArgs.AddRange(projectLocator.GetCustomDefaultConfigurationValueIfSpecified()); - msbuildArgs.AddRange(slnOrProjectArgs ?? []); + msbuildArgs.AddRange(args ?? []); - bool noRestore = parseResult.HasOption(PublishCommandParser.NoRestoreOption) - || parseResult.HasOption(PublishCommandParser.NoBuildOption); + msbuildArgs.Insert(0, "-target:Publish"); return new PublishCommand( msbuildArgs, diff --git a/src/Cli/dotnet/Commands/Publish/PublishCommandParser.cs b/src/Cli/dotnet/Commands/Publish/PublishCommandParser.cs index 4fd18a1e3b8f..604d7de79b79 100644 --- a/src/Cli/dotnet/Commands/Publish/PublishCommandParser.cs +++ b/src/Cli/dotnet/Commands/Publish/PublishCommandParser.cs @@ -13,9 +13,9 @@ internal static class PublishCommandParser { public static readonly string DocsLink = "https://aka.ms/dotnet-publish"; - 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 }; @@ -67,7 +67,7 @@ private static Command ConstructCommand() { var command = new DocumentedCommand("publish", DocsLink, CliCommandStrings.PublishAppDescription); - command.Arguments.Add(SlnOrProjectArgument); + command.Arguments.Add(SlnOrProjectOrFileArgument); RestoreCommandParser.AddImplicitRestoreOptions(command, includeRuntimeOption: false, includeNoDependenciesOption: true); command.Options.Add(OutputOption); diff --git a/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs b/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs index bb1c58481016..c2cca7a8dc10 100644 --- a/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs +++ b/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs @@ -11,7 +11,7 @@ internal static class CommonRunHelpers /// /// Should have . /// - public static void AddUserPassedProperties(Dictionary globalProperties, string[] args) + public static void AddUserPassedProperties(Dictionary globalProperties, IReadOnlyList args) { Debug.Assert(globalProperties.Comparer == StringComparer.OrdinalIgnoreCase); diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index f0d58762dde9..e9e71bcdd86d 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -62,7 +62,7 @@ internal sealed class VirtualProjectBuildingCommand : CommandBase public VirtualProjectBuildingCommand( string entryPointFileFullPath, - string[] msbuildArgs, + IReadOnlyList msbuildArgs, VerbosityOptions? verbosity, bool interactive) { @@ -77,12 +77,12 @@ public VirtualProjectBuildingCommand( public string EntryPointFileFullPath { get; } public Dictionary GlobalProperties { get; } - public string[] BinaryLoggerArgs { get; } + public IReadOnlyList BinaryLoggerArgs { get; } public VerbosityOptions Verbosity { get; } public bool NoRestore { get; init; } public bool NoCache { get; init; } public bool NoBuild { get; init; } - public bool NoIncremental { get; init; } + public string BuildTarget { get; init; } = "Build"; public override int Execute() { @@ -164,7 +164,7 @@ public override int Execute() { var buildRequest = new BuildRequestData( CreateProjectInstance(projectCollection), - targetsToBuild: [NoIncremental ? "Rebuild" : "Build"]); + targetsToBuild: [BuildTarget]); var buildResult = BuildManager.DefaultBuildManager.BuildRequest(buildRequest); if (buildResult.OverallResult != BuildResultCode.Success) { @@ -195,10 +195,10 @@ public override int Execute() consoleLogger.Shutdown(); } - static ILogger? GetBinaryLogger(string[] args) + static ILogger? GetBinaryLogger(IReadOnlyList args) { // Like in MSBuild, only the last binary logger is used. - for (int i = args.Length - 1; i >= 0; i--) + for (int i = args.Count - 1; i >= 0; i--) { var arg = args[i]; if (LoggerUtility.IsBinLogArgument(arg)) @@ -534,6 +534,7 @@ public static void WriteProjectFile( net10.0 enable enable + true """); diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetPublishInvocation.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetPublishInvocation.cs index 410b3286914f..7f638e84d4b1 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetPublishInvocation.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetPublishInvocation.cs @@ -49,7 +49,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona .ToArray(); var msbuildPath = ""; - var command = PublishCommand.FromArgs(args, msbuildPath); + var command = (PublishCommand)PublishCommand.FromArgs(args, msbuildPath); command.SeparateRestoreCommand .Should() @@ -67,7 +67,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona public void MsbuildInvocationIsCorrectForSeparateRestore(string[] args, string[] expectedAdditionalArgs) { var msbuildPath = ""; - var command = PublishCommand.FromArgs(args, msbuildPath); + var command = (PublishCommand)PublishCommand.FromArgs(args, msbuildPath); var restoreTokens = command.SeparateRestoreCommand @@ -92,7 +92,7 @@ public void MsbuildInvocationIsCorrectForSeparateRestore(string[] args, string[] public void MsbuildInvocationIsCorrectForNoBuild() { var msbuildPath = ""; - var command = PublishCommand.FromArgs(new[] { "--no-build" }, msbuildPath); + var command = (PublishCommand)PublishCommand.FromArgs(new[] { "--no-build" }, msbuildPath); command.SeparateRestoreCommand .Should() @@ -107,7 +107,7 @@ public void MsbuildInvocationIsCorrectForNoBuild() public void CommandAcceptsMultipleCustomProperties() { var msbuildPath = ""; - var command = PublishCommand.FromArgs(new[] { "/p:Prop1=prop1", "/p:Prop2=prop2" }, msbuildPath); + var command = (PublishCommand)PublishCommand.FromArgs(new[] { "/p:Prop1=prop1", "/p:Prop2=prop2" }, msbuildPath); command.GetArgumentTokensToMSBuild() .Should() diff --git a/test/dotnet.Tests/CommandTests/MSBuild/MSBuildArgumentCommandLineParserTests.cs b/test/dotnet.Tests/CommandTests/MSBuild/MSBuildArgumentCommandLineParserTests.cs index 4ef72c0d5aa4..176ed182a469 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/MSBuildArgumentCommandLineParserTests.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/MSBuildArgumentCommandLineParserTests.cs @@ -33,8 +33,8 @@ public MSBuildArgumentCommandLineParserTests(ITestOutputHelper output) public void MSBuildArgumentsAreForwardedCorrectly(string[] arguments, bool buildCommand) { RestoringCommand command = buildCommand ? - ((RestoringCommand)BuildCommand.FromArgs(arguments)) : - PublishCommand.FromArgs(arguments); + (RestoringCommand)BuildCommand.FromArgs(arguments) : + (RestoringCommand)PublishCommand.FromArgs(arguments); var expectedArguments = arguments.Select(a => a.Replace("-property:", "--property:").Replace("-p:", "--property:")); var argString = command.MSBuildArguments; diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs index b9d70cba9b0b..fa67634696d9 100644 --- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs +++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs @@ -55,7 +55,12 @@ public void SameAsTemplate() var dotnetProjectConvertProjectText = File.ReadAllText(dotnetProjectConvertProject); var dotnetNewConsoleProjectText = File.ReadAllText(dotnetNewConsoleProject); - dotnetProjectConvertProjectText.Should().Be(dotnetNewConsoleProjectText) + + // There are some differences: we add PublishAot=true. + var patchedDotnetProjectConvertProjectText = dotnetProjectConvertProjectText + .Replace(" true" + Environment.NewLine, string.Empty); + + patchedDotnetProjectConvertProjectText.Should().Be(dotnetNewConsoleProjectText) .And.StartWith(""""""); } @@ -315,6 +320,7 @@ public void ProcessingSucceeds() {ToolsetInfo.CurrentTargetFramework} enable enable + true @@ -345,6 +351,7 @@ public void Directives() {ToolsetInfo.CurrentTargetFramework} enable enable + true @@ -380,6 +387,7 @@ public void Directives_Variable() {ToolsetInfo.CurrentTargetFramework} enable enable + true @@ -421,6 +429,7 @@ public void Directives_Separators() {ToolsetInfo.CurrentTargetFramework} enable enable + true @@ -515,6 +524,7 @@ public void Directives_Escaping() {ToolsetInfo.CurrentTargetFramework} enable enable + true @@ -551,6 +561,7 @@ public void Directives_Whitespace() {ToolsetInfo.CurrentTargetFramework} enable enable + true @@ -608,6 +619,7 @@ public void Directives_AfterToken() {ToolsetInfo.CurrentTargetFramework} enable enable + true @@ -656,6 +668,7 @@ public void Directives_AfterIf() {ToolsetInfo.CurrentTargetFramework} enable enable + true @@ -701,6 +714,7 @@ public void Directives_Comments() {ToolsetInfo.CurrentTargetFramework} enable enable + true diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index 70486f0f3537..eccf6197d059 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -868,6 +868,48 @@ public void NoBuild_02() .And.HaveStdOut("Changed"); } + [Fact] + public void Publish() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programFile, s_program); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "publish", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(artifactsDir).Sub("publish/release") + .Should().Exist() + .And.NotHaveFile("Program.deps.json"); // no deps.json file for AOT-published app + } + + [Fact] + public void Publish_Options() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programFile, s_program); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "publish", "Program.cs", "-c", "Debug", "-p:PublishAot=false", "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(artifactsDir).Sub("publish/debug") + .Should().Exist() + .And.HaveFile("Program.deps.json"); + + new DirectoryInfo(testInstance.Path).File("msbuild.binlog").Should().Exist(); + } + [PlatformSpecificFact(TestPlatforms.AnyUnix), UnsupportedOSPlatform("windows")] public void ArtifactsDirectory_Permissions() { @@ -1254,6 +1296,7 @@ public void Api() {ToolsetInfo.CurrentTargetFramework} enable enable + true @@ -1351,6 +1394,7 @@ public void Api_Diagnostic_01() {ToolsetInfo.CurrentTargetFramework} enable enable + true @@ -1442,6 +1486,7 @@ public void Api_Diagnostic_02() {ToolsetInfo.CurrentTargetFramework} enable enable + 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 a393378991a1..b69cf49b09b1 100644 --- a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh +++ b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh @@ -715,7 +715,7 @@ _testhost() { '--disable-build-servers[Force the command to ignore any persistent build servers.]' \ '--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)