diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index a4baf3c4e21b..4a3e58c4901e 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -79,6 +79,8 @@ 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. +Command `dotnet clean file.cs` can be used to clean build artifacts of the file-based program. + ## Entry points If a file is given to `dotnet run`, it has to be an *entry-point file*, otherwise an error is reported. @@ -331,9 +333,8 @@ or as the first argument if it makes sense for them. We could also add `dotnet compile` command that would be the equivalent of `dotnet build` but for file-based programs (because "compiling" might make more sense for file-based programs than "building"). -`dotnet clean` could be extended to support cleaning [the output directory](#build-outputs), -e.g., via `dotnet clean --file-based-program ` -or `dotnet clean --all-file-based-programs`. +`dotnet clean` could be extended to support cleaning all file-based app outputs, +e.g., `dotnet clean --all-file-based-apps`. Adding references via `dotnet package add`/`dotnet reference add` could be supported for file-based programs as well, i.e., the command would add a `#:package`/`#:project` directive to the top of a `.cs` file. diff --git a/src/Cli/dotnet/Commands/Clean/CleanCommand.cs b/src/Cli/dotnet/Commands/Clean/CleanCommand.cs index 5804b7338764..fb86db062ca8 100644 --- a/src/Cli/dotnet/Commands/Clean/CleanCommand.cs +++ b/src/Cli/dotnet/Commands/Clean/CleanCommand.cs @@ -1,25 +1,23 @@ // 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.MSBuild; +using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Extensions; namespace Microsoft.DotNet.Cli.Commands.Clean; -public class CleanCommand(IEnumerable msbuildArgs, string msbuildPath = null) : MSBuildForwardingApp(msbuildArgs, msbuildPath) +public class CleanCommand(IEnumerable msbuildArgs, string? msbuildPath = null) : MSBuildForwardingApp(msbuildArgs, msbuildPath) { - public static CleanCommand FromArgs(string[] args, string msbuildPath = null) + public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { - var parser = Parser.Instance; var result = parser.ParseFrom("dotnet clean", args); return FromParseResult(result, msbuildPath); } - public static CleanCommand FromParseResult(ParseResult result, string msbuildPath = null) + public static CommandBase FromParseResult(ParseResult result, string? msbuildPath = null) { var msbuildArgs = new List { @@ -28,11 +26,34 @@ public static CleanCommand FromParseResult(ParseResult result, string msbuildPat result.ShowHelpOrErrorIfAppropriate(); - msbuildArgs.AddRange(result.GetValue(CleanCommandParser.SlnOrProjectArgument) ?? []); + var args = result.GetValue(CleanCommandParser.SlnOrProjectOrFileArgument) ?? []; + + LoggerUtility.SeparateBinLogArguments(args, out var binLogArgs, out var nonBinLogArgs); + + var forwardedArgs = result.OptionValuesToBeForwarded(CleanCommandParser.GetCommand()); + + if (nonBinLogArgs is [{ } arg] && VirtualProjectBuildingCommand.IsValidEntryPointPath(arg)) + { + msbuildArgs.AddRange(binLogArgs); + msbuildArgs.AddRange(forwardedArgs); + + return new VirtualProjectBuildingCommand( + entryPointFileFullPath: Path.GetFullPath(arg), + msbuildArgs: [.. msbuildArgs]) + { + NoBuild = false, + NoRestore = true, + NoCache = true, + BuildTarget = "Clean", + NoBuildMarkers = true, + }; + } + + msbuildArgs.AddRange(args); msbuildArgs.Add("-target:Clean"); - msbuildArgs.AddRange(result.OptionValuesToBeForwarded(CleanCommandParser.GetCommand())); + msbuildArgs.AddRange(forwardedArgs); return new CleanCommand(msbuildArgs, msbuildPath); } diff --git a/src/Cli/dotnet/Commands/Clean/CleanCommandParser.cs b/src/Cli/dotnet/Commands/Clean/CleanCommandParser.cs index fc73c81e8efd..e37969f03a4a 100644 --- a/src/Cli/dotnet/Commands/Clean/CleanCommandParser.cs +++ b/src/Cli/dotnet/Commands/Clean/CleanCommandParser.cs @@ -12,9 +12,9 @@ internal static class CleanCommandParser { public static readonly string DocsLink = "https://aka.ms/dotnet-clean"; - 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 }; @@ -45,7 +45,7 @@ private static Command ConstructCommand() { DocumentedCommand command = new("clean", DocsLink, CliCommandStrings.CleanAppFullName); - command.Arguments.Add(SlnOrProjectArgument); + command.Arguments.Add(SlnOrProjectOrFileArgument); command.Options.Add(FrameworkOption); command.Options.Add(CommonOptions.RuntimeOption(CliCommandStrings.CleanRuntimeOptionDescription)); command.Options.Add(ConfigurationOption); diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index 43c45917c2d4..833661693b18 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -131,6 +131,12 @@ public VirtualProjectBuildingCommand( public bool NoBuild { get; init; } public string BuildTarget { get; init; } = "Build"; + /// + /// If , no build markers are written + /// (like and ). + /// + public bool NoBuildMarkers { get; init; } + public override int Execute() { Debug.Assert(!(NoRestore && NoBuild)); @@ -402,6 +408,11 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro private void MarkBuildStart() { + if (NoBuildMarkers) + { + return; + } + string directory = GetArtifactsPath(); if (OperatingSystem.IsWindows()) @@ -421,6 +432,11 @@ private void MarkBuildStart() private void MarkBuildSuccess(RunFileBuildCacheEntry cacheEntry) { + if (NoBuildMarkers) + { + return; + } + string successCacheFile = Path.Join(GetArtifactsPath(), BuildSuccessCacheFileName); using var stream = File.Open(successCacheFile, FileMode.Create, FileAccess.Write, FileShare.None); JsonSerializer.Serialize(stream, cacheEntry, RunFileJsonSerializerContext.Default.RunFileBuildCacheEntry); @@ -530,6 +546,9 @@ public static void WriteProjectFile( { Debug.Assert(!string.IsNullOrWhiteSpace(artifactsPath)); + // Note that ArtifactsPath needs to be specified before Sdk.props + // (usually it's recommended to specify it in Directory.Build.props + // but importing Sdk.props manually afterwards also works). writer.WriteLine($""" @@ -538,6 +557,10 @@ public static void WriteProjectFile( {EscapeValue(artifactsPath)} + + + + """); diff --git a/test/dotnet.Tests/CliSchemaTests.cs b/test/dotnet.Tests/CliSchemaTests.cs index d0a50c640b4e..c538fc527c76 100644 --- a/test/dotnet.Tests/CliSchemaTests.cs +++ b/test/dotnet.Tests/CliSchemaTests.cs @@ -42,8 +42,8 @@ public CliSchemaTests(ITestOutputHelper log) : base(log) "description": ".NET Clean Command", "hidden": false, "arguments": { - "PROJECT | SOLUTION": { - "description": "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": { + "description": "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.", "order": 0, "hidden": false, "valueType": "System.Collections.Generic.IEnumerable", diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetCleanInvocation.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetCleanInvocation.cs index ce44e25588a2..7ca9ab3fef8b 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetCleanInvocation.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetCleanInvocation.cs @@ -19,7 +19,7 @@ public class GivenDotnetCleanInvocation : IClassFixture" }, msbuildPath) + ((CleanCommand)CleanCommand.FromArgs(new string[] { "" }, msbuildPath)) .GetArgumentTokensToMSBuild() .Should() .BeEquivalentTo([.. ExpectedPrefix, ""]); @@ -56,7 +56,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona .ToArray(); var msbuildPath = ""; - CleanCommand.FromArgs(args, msbuildPath) + ((CleanCommand)CleanCommand.FromArgs(args, msbuildPath)) .GetArgumentTokensToMSBuild() .Should() .BeEquivalentTo([.. ExpectedPrefix, .. expectedAdditionalArgs]); diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index b31da0a26864..4acbd83b6f9e 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -1017,6 +1017,36 @@ public void Publish_Options() new DirectoryInfo(testInstance.Path).File("msbuild.binlog").Should().Exist(); } + [Fact] + public void Clean() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programFile, s_program); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from Program"); + + var artifactsDir = new DirectoryInfo(VirtualProjectBuildingCommand.GetArtifactsPath(programFile)); + artifactsDir.Should().HaveFiles(["build-start.cache", "build-success.cache"]); + + var dllFile = artifactsDir.File("bin/debug/Program.dll"); + dllFile.Should().Exist(); + + new DotnetCommand(Log, "clean", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + artifactsDir.EnumerateFiles().Should().BeEmpty(); + + dllFile.Refresh(); + dllFile.Should().NotExist(); + } + [PlatformSpecificFact(TestPlatforms.AnyUnix), UnsupportedOSPlatform("windows")] public void ArtifactsDirectory_Permissions() { @@ -1491,6 +1521,10 @@ public void Api() /artifacts + + + + @@ -1565,6 +1599,10 @@ public void Api_Diagnostic_01() /artifacts + + + + @@ -1632,6 +1670,10 @@ public void Api_Diagnostic_02() /artifacts + + + + 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 368c7bfced60..9e8272ffe3b6 100644 --- a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh +++ b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh @@ -121,7 +121,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)