Skip to content

Commit eb9e3b1

Browse files
authored
Support dotnet clean file.cs (#49511)
1 parent a07baaa commit eb9e3b1

File tree

8 files changed

+106
-19
lines changed

8 files changed

+106
-19
lines changed

documentation/general/dotnet-run-file.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ Command `dotnet publish file.cs` is also supported for file-based programs.
9898
Note that file-based apps have implicitly set `PublishAot=true`, so publishing uses Native AOT (and building reports AOT warnings).
9999
To opt out, use `#:property PublishAot=false` directive in your `.cs` file.
100100

101+
Command `dotnet clean file.cs` can be used to clean build artifacts of the file-based program.
102+
101103
## Entry points
102104

103105
If a file is given to `dotnet run`, it has to be an *entry-point file*, otherwise an error is reported.
@@ -364,9 +366,8 @@ or as the first argument if it makes sense for them.
364366
We could also add `dotnet compile` command that would be the equivalent of `dotnet build` but for file-based programs
365367
(because "compiling" might make more sense for file-based programs than "building").
366368

367-
`dotnet clean` could be extended to support cleaning [the output directory](#build-outputs),
368-
e.g., via `dotnet clean --file-based-program <path-to-entry-point>`
369-
or `dotnet clean --all-file-based-programs`.
369+
`dotnet clean` could be extended to support cleaning all file-based app outputs,
370+
e.g., `dotnet clean --all-file-based-apps`.
370371

371372
Adding references via `dotnet package add`/`dotnet reference add` could be supported for file-based programs as well,
372373
i.e., the command would add a `#:package`/`#:project` directive to the top of a `.cs` file.

src/Cli/dotnet/Commands/Clean/CleanCommand.cs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
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.MSBuild;
6+
using Microsoft.DotNet.Cli.Commands.Run;
87
using Microsoft.DotNet.Cli.Extensions;
98

109
namespace Microsoft.DotNet.Cli.Commands.Clean;
1110

12-
public class CleanCommand(IEnumerable<string> msbuildArgs, string msbuildPath = null) : MSBuildForwardingApp(msbuildArgs, msbuildPath)
11+
public class CleanCommand(IEnumerable<string> msbuildArgs, string? msbuildPath = null) : MSBuildForwardingApp(msbuildArgs, msbuildPath)
1312
{
14-
public static CleanCommand FromArgs(string[] args, string msbuildPath = null)
13+
public static CommandBase FromArgs(string[] args, string? msbuildPath = null)
1514
{
16-
1715
var parser = Parser.Instance;
1816
var result = parser.ParseFrom("dotnet clean", args);
1917
return FromParseResult(result, msbuildPath);
2018
}
2119

22-
public static CleanCommand FromParseResult(ParseResult result, string msbuildPath = null)
20+
public static CommandBase FromParseResult(ParseResult result, string? msbuildPath = null)
2321
{
2422
var msbuildArgs = new List<string>
2523
{
@@ -28,11 +26,34 @@ public static CleanCommand FromParseResult(ParseResult result, string msbuildPat
2826

2927
result.ShowHelpOrErrorIfAppropriate();
3028

31-
msbuildArgs.AddRange(result.GetValue(CleanCommandParser.SlnOrProjectArgument) ?? []);
29+
var args = result.GetValue(CleanCommandParser.SlnOrProjectOrFileArgument) ?? [];
30+
31+
LoggerUtility.SeparateBinLogArguments(args, out var binLogArgs, out var nonBinLogArgs);
32+
33+
var forwardedArgs = result.OptionValuesToBeForwarded(CleanCommandParser.GetCommand());
34+
35+
if (nonBinLogArgs is [{ } arg] && VirtualProjectBuildingCommand.IsValidEntryPointPath(arg))
36+
{
37+
msbuildArgs.AddRange(binLogArgs);
38+
msbuildArgs.AddRange(forwardedArgs);
39+
40+
return new VirtualProjectBuildingCommand(
41+
entryPointFileFullPath: Path.GetFullPath(arg),
42+
msbuildArgs: [.. msbuildArgs])
43+
{
44+
NoBuild = false,
45+
NoRestore = true,
46+
NoCache = true,
47+
BuildTarget = "Clean",
48+
NoBuildMarkers = true,
49+
};
50+
}
51+
52+
msbuildArgs.AddRange(args);
3253

3354
msbuildArgs.Add("-target:Clean");
3455

35-
msbuildArgs.AddRange(result.OptionValuesToBeForwarded(CleanCommandParser.GetCommand()));
56+
msbuildArgs.AddRange(forwardedArgs);
3657

3758
return new CleanCommand(msbuildArgs, msbuildPath);
3859
}

src/Cli/dotnet/Commands/Clean/CleanCommandParser.cs

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

15-
public static readonly Argument<IEnumerable<string>> SlnOrProjectArgument = new(CliStrings.SolutionOrProjectArgumentName)
15+
public static readonly Argument<IEnumerable<string>> SlnOrProjectOrFileArgument = new(CliStrings.SolutionOrProjectOrFileArgumentName)
1616
{
17-
Description = CliStrings.SolutionOrProjectArgumentDescription,
17+
Description = CliStrings.SolutionOrProjectOrFileArgumentDescription,
1818
Arity = ArgumentArity.ZeroOrMore
1919
};
2020

@@ -45,7 +45,7 @@ private static Command ConstructCommand()
4545
{
4646
DocumentedCommand command = new("clean", DocsLink, CliCommandStrings.CleanAppFullName);
4747

48-
command.Arguments.Add(SlnOrProjectArgument);
48+
command.Arguments.Add(SlnOrProjectOrFileArgument);
4949
command.Options.Add(FrameworkOption);
5050
command.Options.Add(CommonOptions.RuntimeOption(CliCommandStrings.CleanRuntimeOptionDescription));
5151
command.Options.Add(ConfigurationOption);

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ public VirtualProjectBuildingCommand(
131131
public bool NoBuild { get; init; }
132132
public string BuildTarget { get; init; } = "Build";
133133

134+
/// <summary>
135+
/// If <see langword="true"/>, no build markers are written
136+
/// (like <see cref="BuildStartCacheFileName"/> and <see cref="BuildSuccessCacheFileName"/>).
137+
/// </summary>
138+
public bool NoBuildMarkers { get; init; }
139+
134140
public override int Execute()
135141
{
136142
Debug.Assert(!(NoRestore && NoBuild));
@@ -412,6 +418,11 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro
412418

413419
private void MarkBuildStart()
414420
{
421+
if (NoBuildMarkers)
422+
{
423+
return;
424+
}
425+
415426
string directory = GetArtifactsPath();
416427

417428
if (OperatingSystem.IsWindows())
@@ -431,6 +442,11 @@ private void MarkBuildStart()
431442

432443
private void MarkBuildSuccess(RunFileBuildCacheEntry cacheEntry)
433444
{
445+
if (NoBuildMarkers)
446+
{
447+
return;
448+
}
449+
434450
string successCacheFile = Path.Join(GetArtifactsPath(), BuildSuccessCacheFileName);
435451
using var stream = File.Open(successCacheFile, FileMode.Create, FileAccess.Write, FileShare.None);
436452
JsonSerializer.Serialize(stream, cacheEntry, RunFileJsonSerializerContext.Default.RunFileBuildCacheEntry);
@@ -541,6 +557,9 @@ public static void WriteProjectFile(
541557
{
542558
Debug.Assert(!string.IsNullOrWhiteSpace(artifactsPath));
543559

560+
// Note that ArtifactsPath needs to be specified before Sdk.props
561+
// (usually it's recommended to specify it in Directory.Build.props
562+
// but importing Sdk.props manually afterwards also works).
544563
writer.WriteLine($"""
545564
<Project>
546565
@@ -550,6 +569,10 @@ public static void WriteProjectFile(
550569
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
551570
</PropertyGroup>
552571
572+
<ItemGroup>
573+
<Clean Include="{EscapeValue(artifactsPath)}/*" />
574+
</ItemGroup>
575+
553576
<!-- We need to explicitly import Sdk props/targets so we can override the targets below. -->
554577
<Import Project="Sdk.props" Sdk="{EscapeValue(sdkValue)}" />
555578
""");

test/dotnet.Tests/CliSchemaTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public CliSchemaTests(ITestOutputHelper log) : base(log)
4242
"description": ".NET Clean Command",
4343
"hidden": false,
4444
"arguments": {
45-
"PROJECT | SOLUTION": {
46-
"description": "The project or solution file to operate on. If a file is not specified, the command will search the current directory for one.",
45+
"PROJECT | SOLUTION | FILE": {
46+
"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.",
4747
"order": 0,
4848
"hidden": false,
4949
"valueType": "System.Collections.Generic.IEnumerable<System.String>",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class GivenDotnetCleanInvocation : IClassFixture<NullCurrentSessionIdFixt
1919
public void ItAddsProjectToMsbuildInvocation()
2020
{
2121
var msbuildPath = "<msbuildpath>";
22-
CleanCommand.FromArgs(new string[] { "<project>" }, msbuildPath)
22+
((CleanCommand)CleanCommand.FromArgs(new string[] { "<project>" }, msbuildPath))
2323
.GetArgumentTokensToMSBuild()
2424
.Should()
2525
.BeEquivalentTo([.. ExpectedPrefix, "<project>"]);
@@ -56,7 +56,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona
5656
.ToArray();
5757

5858
var msbuildPath = "<msbuildpath>";
59-
CleanCommand.FromArgs(args, msbuildPath)
59+
((CleanCommand)CleanCommand.FromArgs(args, msbuildPath))
6060
.GetArgumentTokensToMSBuild()
6161
.Should()
6262
.BeEquivalentTo([.. ExpectedPrefix, .. expectedAdditionalArgs]);

test/dotnet.Tests/CommandTests/Run/RunFileTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,36 @@ public void Publish_In_SubDir()
12521252
.And.NotHaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly); // no deps.json file for AOT-published app
12531253
}
12541254

1255+
[Fact]
1256+
public void Clean()
1257+
{
1258+
var testInstance = _testAssetsManager.CreateTestDirectory();
1259+
var programFile = Path.Join(testInstance.Path, "Program.cs");
1260+
File.WriteAllText(programFile, s_program);
1261+
1262+
new DotnetCommand(Log, "run", "Program.cs")
1263+
.WithWorkingDirectory(testInstance.Path)
1264+
.Execute()
1265+
.Should().Pass()
1266+
.And.HaveStdOut("Hello from Program");
1267+
1268+
var artifactsDir = new DirectoryInfo(VirtualProjectBuildingCommand.GetArtifactsPath(programFile));
1269+
artifactsDir.Should().HaveFiles(["build-start.cache", "build-success.cache"]);
1270+
1271+
var dllFile = artifactsDir.File("bin/debug/Program.dll");
1272+
dllFile.Should().Exist();
1273+
1274+
new DotnetCommand(Log, "clean", "Program.cs")
1275+
.WithWorkingDirectory(testInstance.Path)
1276+
.Execute()
1277+
.Should().Pass();
1278+
1279+
artifactsDir.EnumerateFiles().Should().BeEmpty();
1280+
1281+
dllFile.Refresh();
1282+
dllFile.Should().NotExist();
1283+
}
1284+
12551285
[PlatformSpecificFact(TestPlatforms.AnyUnix), UnsupportedOSPlatform("windows")]
12561286
public void ArtifactsDirectory_Permissions()
12571287
{
@@ -1727,6 +1757,10 @@ public void Api()
17271757
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
17281758
</PropertyGroup>
17291759
1760+
<ItemGroup>
1761+
<Clean Include="/artifacts/*" />
1762+
</ItemGroup>
1763+
17301764
<!-- We need to explicitly import Sdk props/targets so we can override the targets below. -->
17311765
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
17321766
<Import Project="Sdk.props" Sdk="Aspire.Hosting.Sdk" Version="9.1.0" />
@@ -1802,6 +1836,10 @@ public void Api_Diagnostic_01()
18021836
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
18031837
</PropertyGroup>
18041838
1839+
<ItemGroup>
1840+
<Clean Include="/artifacts/*" />
1841+
</ItemGroup>
1842+
18051843
<!-- We need to explicitly import Sdk props/targets so we can override the targets below. -->
18061844
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
18071845
@@ -1870,6 +1908,10 @@ public void Api_Diagnostic_02()
18701908
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
18711909
</PropertyGroup>
18721910
1911+
<ItemGroup>
1912+
<Clean Include="/artifacts/*" />
1913+
</ItemGroup>
1914+
18731915
<!-- We need to explicitly import Sdk props/targets so we can override the targets below. -->
18741916
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
18751917

test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ _testhost() {
121121
'--disable-build-servers[Force the command to ignore any persistent build servers.]' \
122122
'--help[Show command line help.]' \
123123
'-h[Show command line help.]' \
124-
'*::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.: ' \
124+
'*::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.: ' \
125125
&& ret=0
126126
case $state in
127127
(dotnet_dynamic_complete)

0 commit comments

Comments
 (0)