From 14b37be8cffb170ee24fae9a2ec5702282984118 Mon Sep 17 00:00:00 2001 From: revtex Date: Sat, 29 Aug 2026 10:58:07 -0400 Subject: [PATCH] Clean by deleting bin and obj, not by asking dotnet to `dotnet clean` removes what the configuration and target framework you name would have produced. Everything else it leaves, and there was a great deal of it: 485 MB of `net10.0-windows` output stranded when the app retargeted to Windows 11 a fortnight ago, and 183 MB of `obj\scratch*` trees from builds that had redirected their output path to get around a locked DLL. Two thirds of a gigabyte that no build would ever look at again, surviving every clean anyone ran. None of it appeared in `git status`, which is why it lasted. `[Oo]bj/` and `[Bb]in/` are unanchored, so they match at any depth and the residue was ignored from the moment it was written. That is the right rule - the variant it does not cover, an output directory at the project root, shows up as untracked and gets noticed the same day - but it does mean nothing was ever going to point at this. So `-Clean` deletes both folders beside every project in the solution and says what it freed. It ignores `-Configuration` deliberately: the whole value is in taking the configurations you are not building, since those are the ones nothing else will touch. It walks the `.csproj` files rather than sweeping the tree recursively, so it cannot wander into `.git`, `.vs`, or a vendored folder that happens to contain a `bin`. Failing to delete a folder now names the likely cause. A running Offstream holds its own binaries open, and Windows reports that as a file it cannot access - true, unhelpful, and the same message you get for half a dozen unrelated problems. Worth spending a line on, because this is also where the `obj\scratch*` trees came from: redirecting the output path gets past the lock, and quietly leaves the copy behind. The plan gets the finding beside the one about `build.ps1` holding a second copy of the TFM, since they are the same lesson from opposite ends - changing the TFM invalidates paths that no longer appear anywhere in the build, and the ones already on disk have nothing left that knows their name. The README's test count said 1,089 and the suite is 1,128. Corrected while in the file rather than left to drift further. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 11 +++++++++++ README.md | 4 ++-- build.ps1 | 35 +++++++++++++++++++++++++++++++---- docs/MODERNIZATION-PLAN.md | 1 + 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31dbeab..8250c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,17 @@ phase plan these entries follow. said the same thing every time whether or not anyone needed it. Hovering the row shows it instead. The text is unchanged and still translated, and each control carries it as accessible help text as well, because a tooltip is the one thing on the page a screen reader cannot see. +- **`build.ps1 -Clean` deletes `bin\` and `obj\` outright, because `dotnet clean` could never + reach what was actually stale.** It only removes what the configuration and target framework you + name would have produced, so everything orphaned survived every clean anyone ran: 485 MB of + `net10.0-windows` output stranded when the app retargeted to Windows 11, and 183 MB of + `obj\scratch*` trees left by builds that had redirected their output path to dodge a file lock. + All of it is git-ignored, so none of it ever appeared in `git status` — the repository quietly + carried two thirds of a gigabyte that no build would ever look at again. `-Clean` now removes + both folders beside every project and reports what it freed, and ignores `-Configuration`, + since the point is to take the configurations you are *not* building too. Deleting a folder the + running app holds open now says so and names the app, rather than passing on Windows' own + account of a file it cannot open. ### Removed diff --git a/README.md b/README.md index 57a376a..08fbc27 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ original assumptions and records what replaced it; then Status — phases 0–7 complete, phase 8 (packaging) next ```powershell -.\build.ps1 -Clean -Test # 1,089 green; add -IncludeDesktop for the FlaUI suite +.\build.ps1 -Clean -Test # 1,128 green; add -IncludeDesktop for the FlaUI suite dotnet run --project src/Offstream.App ``` @@ -384,7 +384,7 @@ fix rather than a compiler error. .\build.ps1 -Configuration Release .\build.ps1 -Test # build, then run the whole suite .\build.ps1 -Test -Filter FileNameTemplate -.\build.ps1 -Clean -Test # rebuild from scratch, then test +.\build.ps1 -Clean -Test # delete every bin\ and obj\, rebuild, then test .\build.ps1 -Format # apply .editorconfig .\build.ps1 -VerifyFormat # what CI enforces .\build.ps1 -Publish # self-contained win-x64 publish diff --git a/build.ps1 b/build.ps1 index 2061e18..07e28aa 100644 --- a/build.ps1 +++ b/build.ps1 @@ -18,7 +18,7 @@ .\build.ps1 -Test # build, then run the suite (desktop tests excluded) .\build.ps1 -Test -IncludeDesktop # also run the FlaUI tests, needs a real session .\build.ps1 -Test -Filter FileNameTemplate - .\build.ps1 -Clean -Test # rebuild from scratch, then test + .\build.ps1 -Clean -Test # delete every bin\ and obj\, rebuild, then test .\build.ps1 -VerifyFormat # what CI enforces .\build.ps1 -Publish # self-contained win-x64 publish .\build.ps1 -Publish -BundleFfmpeg # ...with the ffmpeg a release ships (108 MB) @@ -84,10 +84,37 @@ if ($Test -and -not $hasFfmpeg) { # --- Clean ------------------------------------------------------------------ +# `dotnet clean` only removes what the *current* configuration and target framework would +# have produced, so anything orphaned survives it: output stranded by a retarget (the +# pre-Win11 `net10.0-windows` folders sat there for weeks), and the `obj\scratch*` trees a +# build with a redirected BaseOutputPath leaves behind. Both are git-ignored, so they never +# show up in `git status` and accumulate unnoticed - 645 MB of them, once. Deleting bin\ and +# obj\ outright is the only clean that catches them, and it ignores -Configuration because +# the whole point is to take the configurations you are *not* building too. if ($Clean) { - Step "Cleaning ($Configuration)..." - & dotnet clean $sln -c $Configuration --nologo -v minimal - Assert-ExitCode 'Clean' + Step 'Cleaning (every configuration and target framework)...' + + $stale = @( + Get-ChildItem -Path $PSScriptRoot -Recurse -Filter '*.csproj' -File | + Where-Object { $_.FullName -notmatch '\\(bin|obj)\\' } | + ForEach-Object { (Join-Path $_.DirectoryName 'bin'), (Join-Path $_.DirectoryName 'obj') } | + Where-Object { Test-Path $_ } + ) + + $freed = 0 + foreach ($dir in $stale) { + $freed += (Get-ChildItem $dir -Recurse -File -ErrorAction SilentlyContinue | + Measure-Object -Property Length -Sum).Sum + try { + Remove-Item $dir -Recurse -Force -ErrorAction Stop + } + catch { + # Much the likeliest cause, and the message Windows gives for it is unhelpful. + throw "Could not delete $dir - $($_.Exception.Message)`nIf Offstream is running it holds its own binaries open; quit it (the tray icon too) and re-run." + } + } + + Write-Host (" removed {0} folder(s), {1:N0} MB" -f $stale.Count, ($freed / 1MB)) -ForegroundColor DarkGray } # --- Format ----------------------------------------------------------------- diff --git a/docs/MODERNIZATION-PLAN.md b/docs/MODERNIZATION-PLAN.md index 65c8507..83c7c2a 100644 --- a/docs/MODERNIZATION-PLAN.md +++ b/docs/MODERNIZATION-PLAN.md @@ -784,6 +784,7 @@ error paths and the scope list. - **The TFM raise was mechanical, and the routing warning was unfounded — verified rather than assumed.** `net10.0-windows10.0.22621.0`, one property. The plan flagged that CsWinRT projections change what the compiler generates around WinRT types and that `IAudioPolicyConfig` should be re-checked. It is unaffected *by construction*: the routing code names `Windows.Media.Internal.AudioPolicyConfig` only as a runtime-class **string** and otherwise uses `ComImport` and raw P/Invoke, so there is no WinRT type in the type system for the projections to claim. Confirmed at runtime anyway — still binds the 21H2 IID and completes a COM round-trip on build 26200. - **22621, not the 22000 floor.** It is the oldest build still in support, so targeting lower gains nothing, and Windows 11 only (decided 2026-08-11) means it costs no supported user. - **`build.ps1` held a second copy of the TFM** in its publish path and pointed at a directory that stopped existing the moment this changed. It reads the value from `Directory.Build.props` now. Worth remembering that the TFM appears in more places than the props file. +- **The TFM raise also stranded every build output under the old one, and nothing cleaned it up (found 2026-08-29).** `dotnet clean` only removes what the configuration and target framework you name would have produced, so the `net10.0-windows` folders left behind by the raise above were unreachable by every clean anyone ran - 485 MB of them, still there a fortnight later. Another 183 MB was `obj\scratch*` trees from builds that had redirected their output path to get around a locked DLL, which is what happens when the app is running while the solution rebuilds; quitting it is the fix, and redirecting the output just moves the lock and leaves the copy behind. All of it is git-ignored, so none of it ever showed up in `git status` and the working tree carried two thirds of a gigabyte nobody could see. `build.ps1 -Clean` deletes `bin\` and `obj\` outright now rather than delegating to `dotnet clean`. The general point is the one the finding above makes from the other direction: **changing the TFM invalidates paths that no longer appear anywhere in the build**, and the ones already written to disk have nothing left that knows their name. - **SMTC is the primary source and the window title is the fallback, not the reverse.** The title is only readable while Spotify has a window — minimised to the tray it has none, which is the gap this closes. It is also *better* information: SMTC hands over separate artist, title and album fields, where the title is one string that has to be split on a separator that can legitimately appear inside either half. Confirmed live: the media session reported an album (`… (Expanded Edition)`) that no window title carries. - **"Prefers" means "answers", nothing cleverer.** The preferred source wins whenever it returns a track at all — not by being newer or more detailed. An *idle* answer is still an answer and is not second-guessed; only a null, meaning "I cannot see Spotify", reaches the fallback. Anything more sophisticated means two detectors disagreeing mid-track and a recording whose tags change halfway through. - **A failing SMTC is treated as a silent one.** It is a system service Offstream does not control, so a fault costs the better metadata and never the recording — logged once per transition rather than per poll, since this runs several times a second. Cancellation is exempt: that is the session stopping, and it propagates.