Skip to content

Commit 4b9a99a

Browse files
committed
feat: support \go clean <remote-ref>\ (bundle + per-path publishes); remove --force
1 parent 1af466c commit 4b9a99a

9 files changed

Lines changed: 112 additions & 53 deletions

File tree

readme.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,18 @@ and participates in the normal up-to-date checks. Remote refs are always revalid
8888
by sending a conditional request (using ETag when available) to the source. A 304
8989
Not Modified response means the local copy is used as-is.
9090

91+
To force a fresh download for a remote ref, clean its bundle first:
92+
9193
```console
92-
# Force a fresh download (bypasses ETag conditional check)
93-
dnx go --force kzu/sandbox
94+
# Clean the downloaded bundle for a remote ref (forces full download on next run)
95+
dnx go clean kzu/sandbox
9496

95-
# Same using the --go- alias form
96-
dnx go --go-force kzu/sandbox
97+
# Works for refs with @ref or :path too (the bundle for the ref is deleted entirely)
98+
dnx go clean kzu/sandbox@main:program.cs
9799
```
98100

99101
The go-specific switches support both bare and `--go-` prefixed forms for consistency:
100102

101-
- `--force` / `--go-force`
102103
- `--debug` / `--go-debug`
103104
- `--r2r` / `--go-r2r`
104105

@@ -120,6 +121,9 @@ unchanged re-runs near-instant.
120121
# Delete the cached artifacts for a single app (next run rebuilds)
121122
dnx go clean app.cs
122123

124+
# Delete the downloaded bundle for a remote ref (next run re-downloads; :path ignored)
125+
dnx go clean owner/repo[@ref][:path]
126+
123127
# Delete the cached artifacts for all apps
124128
dnx go clean --all
125129
```

src/Tests/EndToEndTests.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,6 @@ public void Remote_ref_always_revalidates_via_etag_root_dir_touched_source_files
298298

299299
Assert.True((mtimeAfterImm - mtimeBeforeImm).TotalSeconds < 2.0, "immediate must not change source file mtime");
300300
Assert.True(rootAfterImm > rootBeforeImm || rootAfterImm >= rootBeforeImm, "root dir touched on immediate use too");
301-
302-
// --force should succeed (forces full fetch, bypassing any conditional).
303-
var (exitForce, outForce) = RunGo("--force", remoteRef, "--", "-v:q");
304-
File.AppendAllText(logPath, "FORCE:\n" + outForce);
305-
Assert.Equal(0, exitForce);
306-
Assert.Contains("run.cs", outForce);
307301
}
308302
finally
309303
{

src/Tests/GoArgsTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,25 @@ public void ApplyPublishMode_leaves_args_unchanged_when_disabled()
5959
[Fact]
6060
public void Normalize_maps_go_prefixed_switches_to_bare_forms()
6161
{
62-
var normalized = GoArgs.Normalize(["--go-force", "--go-debug", "--go-r2r", "owner/repo", "--", "apparg"]);
62+
var normalized = GoArgs.Normalize(["--go-debug", "--go-r2r", "owner/repo", "--", "apparg"]);
6363

64-
Assert.Equal(["--force", "--debug", "--r2r", "owner/repo", "--", "apparg"], normalized);
64+
Assert.Equal(["--debug", "--r2r", "owner/repo", "--", "apparg"], normalized);
6565
}
6666

6767
[Fact]
6868
public void Normalize_leaves_bare_and_unknown_untouched()
6969
{
70-
var normalized = GoArgs.Normalize(["--force", "--debug", "--r2r", "--other", "input.cs"]);
70+
var normalized = GoArgs.Normalize(["--debug", "--r2r", "--other", "input.cs"]);
7171

72-
Assert.Equal(["--force", "--debug", "--r2r", "--other", "input.cs"], normalized);
72+
Assert.Equal(["--debug", "--r2r", "--other", "input.cs"], normalized);
7373
}
7474

7575
[Fact]
7676
public void Normalize_handles_mixed_case_and_all_forms()
7777
{
78-
var normalized = GoArgs.Normalize(["--Go-Force", "--DEBUG", "ref"]);
78+
var normalized = GoArgs.Normalize(["--Go-Debug", "--R2R", "ref"]);
7979

80-
Assert.Equal(["--force", "--debug", "ref"], normalized);
80+
Assert.Equal(["--debug", "--r2r", "ref"], normalized);
8181
}
8282

8383
[Fact]

src/Tests/GoBuildCacheTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public async Task GetEffectiveSourceAsync_returns_local_existing_without_net()
200200
{
201201
var dir = CreateTempDir();
202202
var f = WriteFile(dir, "local.cs", "Console.WriteLine(\"hi\");");
203-
var eff = await Devlooped.RemoteSourceResolver.GetEffectiveSourceAsync(f, force: false);
203+
var eff = await Devlooped.RemoteSourceResolver.GetEffectiveSourceAsync(f);
204204
Assert.Equal(Path.GetFullPath(f), eff);
205205
}
206206
}

src/go/GoArgs.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ public static class GoArgs
44
{
55
public static readonly string[] ReadyToRunPublishArgs = ["/p:PublishAot=false", "/p:PublishReadyToRun=true"];
66

7-
static readonly string[] GoSwitchNames = ["force", "debug", "r2r"];
7+
static readonly string[] GoSwitchNames = ["debug", "r2r"];
88

99
/// <summary>
10-
/// Normalizes go-specific switches so both prefix-less (--force) and --go- forms (--go-force)
10+
/// Normalizes go-specific switches so both prefix-less and --go- forms
1111
/// are accepted; maps --go-* aliases to the bare form the command methods declare.
1212
/// </summary>
1313
public static string[] Normalize(string[] args)

src/go/Program.cs

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@
1515
/// </summary>
1616
/// <param name="input">Path to an existing .cs file or remote ref (owner/repo[@ref][:path]).</param>
1717
/// <param name="r2r">Publish with ReadyToRun instead of native AOT; supports more dynamic .NET features while keeping most publish optimizations. Alias: --go-r2r.</param>
18-
/// <param name="force">Force re-download of remote ref if applicable. Alias: --go-force.</param>
1918
/// <param name="debug">Launch debugger before executing. Alias: --go-debug.</param>
2019
/// <param name="extraArgs">Arguments before '--' are passed to 'dotnet publish'; arguments after '--' are forwarded to the published app. Without '--', all extra arguments are forwarded to the published app.
2120
/// </param>
22-
static async Task<int> RunAsync([Argument] string input, bool r2r = false, bool force = false, bool debug = false, [Argument] params string[] extraArgs)
21+
static async Task<int> RunAsync([Argument] string input, bool r2r = false, bool debug = false, [Argument] params string[] extraArgs)
2322
{
2423
if (debug)
2524
System.Diagnostics.Debugger.Launch();
2625

27-
var source = await GetEffectiveSourceAsync(input, force);
26+
var source = await GetEffectiveSourceAsync(input);
2827
if (source is null)
2928
return 1;
3029

@@ -55,9 +54,9 @@ state.App is not null &&
5554
}
5655

5756
/// <summary>
58-
/// Deletes cached publish artifacts for a file-based .NET app.
57+
/// Deletes cached publish artifacts for a file-based .NET app, or for a remote ref: deletes the bundle and also its associated publish artifacts for every previously-used path recorded in the bundle's ETags/Entry (:path on the ref is ignored).
5958
/// </summary>
60-
/// <param name="input">Path to an existing .cs file.</param>
59+
/// <param name="input">Path to an existing .cs file or remote ref (owner/repo[@ref][:path]).</param>
6160
/// <param name="all">Delete cached artifacts for all apps instead.</param>
6261
static int CleanAsync([Argument] string? input = null, bool all = false)
6362
{
@@ -74,31 +73,88 @@ static int CleanAsync([Argument] string? input = null, bool all = false)
7473

7574
if (input is null)
7675
{
77-
ConsoleApp.LogError("Specify a .cs file to clean, or --all to clean all cached apps.");
76+
ConsoleApp.LogError("Specify a .cs file or remote ref to clean, or --all to clean all cached apps.");
7877
return 1;
7978
}
8079

81-
if (!TryResolveEntryPoint(input, out _, out var publishDir, out var stamp))
80+
var full = Path.GetFullPath(input);
81+
if (File.Exists(full))
82+
{
83+
if (!TryResolveEntryPoint(input, out _, out var publishDir, out var stamp))
84+
return 1;
85+
86+
return AppCleaner.Clean(publishDir, stamp);
87+
}
88+
89+
if (RemoteRef.TryParse(input, out var remote))
90+
{
91+
var bundleDir = remote.TempPath;
92+
if (Directory.Exists(bundleDir))
93+
{
94+
try
95+
{
96+
// A bundle can contain multiple ref paths in use (e.g. different :path or discovered entries).
97+
// ETags keys (and the persisted Entry) record the relative file paths previously used to run.
98+
// Clean the published artifacts for each by treating them as local file paths (before we nuke the bundle).
99+
var settings = RemoteSettingsStore.Load(bundleDir);
100+
var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
101+
if (!string.IsNullOrEmpty(settings.Entry))
102+
keys.Add(settings.Entry.Replace('\\', '/'));
103+
if (settings.ETags != null)
104+
{
105+
foreach (var k in settings.ETags.Keys)
106+
keys.Add(k.Replace('\\', '/'));
107+
}
108+
109+
foreach (var key in keys)
110+
{
111+
var filePath = Path.Combine(bundleDir, key.Replace('/', Path.DirectorySeparatorChar));
112+
if (File.Exists(filePath))
113+
{
114+
if (TryResolveEntryPoint(filePath, out _, out var publishDir, out var stamp))
115+
AppCleaner.Clean(publishDir, stamp);
116+
}
117+
}
118+
}
119+
catch
120+
{
121+
// Best-effort: continue to delete the bundle even if some publish cleans failed.
122+
}
123+
124+
try
125+
{
126+
Directory.Delete(bundleDir, recursive: true);
127+
return 0;
128+
}
129+
catch
130+
{
131+
return 1;
132+
}
133+
}
134+
135+
return 0;
136+
}
137+
138+
if (!TryResolveEntryPoint(input, out _, out var pdir, out var stmp))
82139
return 1;
83140

84-
return AppCleaner.Clean(publishDir, stamp);
141+
return AppCleaner.Clean(pdir, stmp);
85142
}
86143

87144
/// <summary>
88145
/// Runs a file-based .NET app from a .cs entrypoint using dotnet run for fast iteration.
89146
/// </summary>
90147
/// <param name="input">Path to an existing .cs file or remote ref (owner/repo[@ref][:path]).</param>
91148
/// <param name="r2r">Accepted for consistency (ignored for dev which uses dotnet run). Alias: --go-r2r.</param>
92-
/// <param name="force">Force re-download of remote ref if applicable. Alias: --go-force.</param>
93149
/// <param name="debug">Launch debugger before executing. Alias: --go-debug.</param>
94150
/// <param name="extraArgs">Arguments before '--' are passed to 'dotnet run'; arguments after '--' are forwarded to the app. Without '--', all extra arguments are forwarded to the app.
95151
/// </param>
96-
static async Task<int> DevAsync([Argument] string input, bool r2r = false, bool force = false, bool debug = false, [Argument] params string[] extraArgs)
152+
static async Task<int> DevAsync([Argument] string input, bool r2r = false, bool debug = false, [Argument] params string[] extraArgs)
97153
{
98154
if (debug)
99155
System.Diagnostics.Debugger.Launch();
100156

101-
var source = await GetEffectiveSourceAsync(input, force);
157+
var source = await GetEffectiveSourceAsync(input);
102158
if (source is null)
103159
return 1;
104160

@@ -150,5 +206,5 @@ static bool TryResolveEntryPoint(string input, out string effectiveCs, out strin
150206
return (dotnet, cs, publishDir, stamp, targets, mode, dotnetArgs, appArgs);
151207
}
152208

153-
static async Task<string?> GetEffectiveSourceAsync(string input, bool force)
154-
=> await RemoteSourceResolver.GetEffectiveSourceAsync(input, force);
209+
static async Task<string?> GetEffectiveSourceAsync(string input)
210+
=> await RemoteSourceResolver.GetEffectiveSourceAsync(input);

src/go/RemoteSourceResolver.cs

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public static bool IsRemoteDownloadStale(string sourcePath)
8282
return (DateTime.UtcNow - mtime) > TimeSpan.FromDays(14);
8383
}
8484

85-
public static async Task<string?> GetEffectiveSourceAsync(string input, bool force)
85+
public static async Task<string?> GetEffectiveSourceAsync(string input)
8686
{
8787
var full = Path.GetFullPath(input);
8888
if (File.Exists(full))
@@ -94,22 +94,17 @@ public static bool IsRemoteDownloadStale(string sourcePath)
9494
return null;
9595
}
9696

97-
return await DownloadIfNeededAsync(remote, force);
97+
return await DownloadIfNeededAsync(remote);
9898
}
9999

100-
internal static async Task<string> DownloadIfNeededAsync(RemoteRef remote, bool force)
100+
internal static async Task<string> DownloadIfNeededAsync(RemoteRef remote)
101101
{
102-
if (force && Directory.Exists(remote.TempPath))
103-
{
104-
try { Directory.Delete(remote.TempPath, recursive: true); } catch { }
105-
}
106-
107102
var candidate = GetRemoteEntryPointPath(remote);
108103

109104
// Always revalidate remote refs via ETag (conditional request to source).
110105
// 304 => keep local, 200 => (re)extract. No local mtime-based freshness gate.
111106
string? etag = null;
112-
if (!force && Directory.Exists(remote.TempPath))
107+
if (Directory.Exists(remote.TempPath))
113108
{
114109
var rs = RemoteSettingsStore.Load(remote.TempPath);
115110
var key = GetStartupKey(remote, candidate);
@@ -120,6 +115,7 @@ internal static async Task<string> DownloadIfNeededAsync(RemoteRef remote, bool
120115
var requestRef = remote with { ETag = etag };
121116
HttpResponseMessage? contents = null;
122117
bool didExtract = false;
118+
string? newEtag = null;
123119
try
124120
{
125121
contents = await provider.GetAsync(requestRef);
@@ -140,15 +136,9 @@ internal static async Task<string> DownloadIfNeededAsync(RemoteRef remote, bool
140136
await contents.ExtractToAsync(remote);
141137
didExtract = true;
142138

143-
// Persist ETag for future conditional requests (if server provided one).
144-
var newEtag = contents.Headers.ETag?.ToString();
145-
if (!string.IsNullOrWhiteSpace(newEtag))
146-
{
147-
var rs = RemoteSettingsStore.Load(remote.TempPath);
148-
var key = GetStartupKey(remote, candidate);
149-
RemoteSettingsStore.SetETag(rs, key, newEtag);
150-
RemoteSettingsStore.Save(rs, remote.TempPath);
151-
}
139+
// Capture ETag; persist below using the *final* candidate (important for
140+
// no-:path refs where default "program.cs" candidate may be overridden by discovery).
141+
newEtag = contents.Headers.ETag?.ToString();
152142

153143
// Mark the ref root (unzipped bundle) dir for cleanup tracking.
154144
Directory.Touch(remote.TempPath);
@@ -184,6 +174,19 @@ internal static async Task<string> DownloadIfNeededAsync(RemoteRef remote, bool
184174
}
185175
}
186176

177+
// Persist ETag for future conditional requests (if server provided one), using final candidate key.
178+
if (didExtract && !string.IsNullOrWhiteSpace(newEtag))
179+
{
180+
try
181+
{
182+
var rs = RemoteSettingsStore.Load(remote.TempPath);
183+
var key = GetStartupKey(remote, candidate);
184+
RemoteSettingsStore.SetETag(rs, key, newEtag);
185+
RemoteSettingsStore.Save(rs, remote.TempPath);
186+
}
187+
catch { }
188+
}
189+
187190
// If candidate missing but dir now exists (e.g. 304 path or prior state), recompute.
188191
if (!File.Exists(candidate) && Directory.Exists(remote.TempPath))
189192
{

src/go/help.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ Arguments:
99

1010
Options:
1111
--r2r Publish with ReadyToRun instead of native AOT; supports more dynamic .NET features while keeping most publish optimizations. Alias: --go-r2r.
12-
--force Force re-download of remote ref if applicable. Alias: --go-force.
1312
--debug Launch debugger before executing. Alias: --go-debug.
1413

1514
Commands:
16-
clean Deletes cached publish artifacts for a file-based .NET app.
15+
clean Deletes cached publish artifacts for a file-based .NET app, or for a remote ref: deletes the bundle and also its associated publish artifacts for every previously-used path recorded in the bundle's ETags/Entry (:path on the ref is ignored).
1716
dev Runs a file-based .NET app from a .cs entrypoint using dotnet run for fast iteration.
1817
```

src/go/readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ unchanged re-runs near-instant.
7171
# Delete the cached artifacts for a single app (next run rebuilds)
7272
dnx go clean app.cs
7373

74+
# Delete the downloaded bundle for a remote ref (next run re-downloads; :path ignored)
75+
dnx go clean owner/repo[@ref][:path]
76+
7477
# Delete the cached artifacts for all apps
7578
dnx go clean --all
7679
```

0 commit comments

Comments
 (0)