Skip to content

Support dotnet clean file.cs #49511

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 4 additions & 3 deletions documentation/general/dotnet-run-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <path-to-entry-point>`
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.
Expand Down
37 changes: 29 additions & 8 deletions src/Cli/dotnet/Commands/Clean/CleanCommand.cs
Original file line number Diff line number Diff line change
@@ -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<string> msbuildArgs, string msbuildPath = null) : MSBuildForwardingApp(msbuildArgs, msbuildPath)
public class CleanCommand(IEnumerable<string> 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<string>
{
Expand All @@ -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);
}
Expand Down
6 changes: 3 additions & 3 deletions src/Cli/dotnet/Commands/Clean/CleanCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ internal static class CleanCommandParser
{
public static readonly string DocsLink = "https://aka.ms/dotnet-clean";

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

Expand Down Expand Up @@ -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);
Expand Down
23 changes: 23 additions & 0 deletions src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ public VirtualProjectBuildingCommand(
public bool NoBuild { get; init; }
public string BuildTarget { get; init; } = "Build";

/// <summary>
/// If <see langword="true"/>, no build markers are written
/// (like <see cref="BuildStartCacheFileName"/> and <see cref="BuildSuccessCacheFileName"/>).
/// </summary>
public bool NoBuildMarkers { get; init; }

public override int Execute()
{
Debug.Assert(!(NoRestore && NoBuild));
Expand Down Expand Up @@ -402,6 +408,11 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro

private void MarkBuildStart()
{
if (NoBuildMarkers)
{
return;
}

string directory = GetArtifactsPath();

if (OperatingSystem.IsWindows())
Expand All @@ -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);
Expand Down Expand Up @@ -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($"""
<Project>
Expand All @@ -538,6 +557,10 @@ public static void WriteProjectFile(
<ArtifactsPath>{EscapeValue(artifactsPath)}</ArtifactsPath>
</PropertyGroup>
<ItemGroup>
<Clean Include="{EscapeValue(artifactsPath)}/*" />
</ItemGroup>
<!-- We need to explicitly import Sdk props/targets so we can override the targets below. -->
<Import Project="Sdk.props" Sdk="{EscapeValue(sdkValue)}" />
""");
Expand Down
4 changes: 2 additions & 2 deletions test/dotnet.Tests/CliSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<System.String>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class GivenDotnetCleanInvocation : IClassFixture<NullCurrentSessionIdFixt
public void ItAddsProjectToMsbuildInvocation()
{
var msbuildPath = "<msbuildpath>";
CleanCommand.FromArgs(new string[] { "<project>" }, msbuildPath)
((CleanCommand)CleanCommand.FromArgs(new string[] { "<project>" }, msbuildPath))
.GetArgumentTokensToMSBuild()
.Should()
.BeEquivalentTo([.. ExpectedPrefix, "<project>"]);
Expand Down Expand Up @@ -56,7 +56,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona
.ToArray();

var msbuildPath = "<msbuildpath>";
CleanCommand.FromArgs(args, msbuildPath)
((CleanCommand)CleanCommand.FromArgs(args, msbuildPath))
.GetArgumentTokensToMSBuild()
.Should()
.BeEquivalentTo([.. ExpectedPrefix, .. expectedAdditionalArgs]);
Expand Down
42 changes: 42 additions & 0 deletions test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -1491,6 +1521,10 @@ public void Api()
<ArtifactsPath>/artifacts</ArtifactsPath>
</PropertyGroup>

<ItemGroup>
<Clean Include="/artifacts/*" />
</ItemGroup>

<!-- We need to explicitly import Sdk props/targets so we can override the targets below. -->
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
<Import Project="Sdk.props" Sdk="Aspire.Hosting.Sdk" Version="9.1.0" />
Expand Down Expand Up @@ -1565,6 +1599,10 @@ public void Api_Diagnostic_01()
<ArtifactsPath>/artifacts</ArtifactsPath>
</PropertyGroup>

<ItemGroup>
<Clean Include="/artifacts/*" />
</ItemGroup>

<!-- We need to explicitly import Sdk props/targets so we can override the targets below. -->
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />

Expand Down Expand Up @@ -1632,6 +1670,10 @@ public void Api_Diagnostic_02()
<ArtifactsPath>/artifacts</ArtifactsPath>
</PropertyGroup>

<ItemGroup>
<Clean Include="/artifacts/*" />
</ItemGroup>

<!-- We need to explicitly import Sdk props/targets so we can override the targets below. -->
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />

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