[Bug]: Windows onefile extracts from older versions stay under Local AppData #167

Closed
opened 2026-08-20 05:36:40 +00:00 by Grok · 4 comments
Member

App version

Current packaged Windows builds (Nuitka onefile). Affects %LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer after upgrading through several versions / builds.

Platform

Windows

Steps to reproduce

  1. Run a packaged Windows .exe (it unpacks into %LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer\<version+build-id>).
  2. Install / run a newer .exe (different version or different git build id).
  3. Repeat across several upgrades (release and/or dev builds).
  4. Inspect %LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer.

Expected behavior

Opening a newer packaged build removes extract trees from older versions/builds. Only the currently running unpack remains under that product folder. The folder does not keep growing with every upgrade.

This is already the documented intent (DR-016, prune_stale_onefile_caches() on startup, TECHNICAL_OUTLINE / DESIGN: stale caches pruned on startup).

Actual behavior

AppData\Local\ValleyGeek\Bulk Image Organizer grows too large. Artifacts from past versions remain on disk after launching a newer application.

Additional context

core/onefile_cache.py already tries to prune sibling cache folders at startup, but it is conservative and can leave leftovers:

  • Only directories whose names contain + are treated as cache segments (_is_cache_segment_dir). Older or Nuitka-default {VERSION} folders (no +build-id) are never deleted.
  • Failed rmtree is logged as a warning and the tree is left in place (locks, AV, in-use files).
  • Prune is a no-op unless frozen Windows onefile detection succeeds (exe parent ≠ unpack root).
  • Config (%APPDATA%\bulk-image-organizer) and workspace caches (%LOCALAPPDATA%\bulk-image-organizer) are not this folder and must not be deleted.

Desired fix

On packaged Windows startup, after the current extract is in use:

  • Delete every other directory under %LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer except the active unpack (not only + name siblings).
  • Keep the current extract; never delete files in use by this process.
  • Best-effort on locked trees: retry or skip with a log; do not crash startup.
  • Honor BIO_SKIP_ONEFILE_CACHE_PRUNE.
  • Do not touch %APPDATA%\bulk-image-organizer (settings/logs) or %LOCALAPPDATA%\bulk-image-organizer (network workspace caches).

Acceptance criteria

  • Launching a newer Windows onefile build removes older version/build extract dirs under ValleyGeek\Bulk Image Organizer.
  • The running unpack is left intact.
  • Folders without + in the name (legacy {VERSION} extracts) are also removed.
  • Settings, logs, and workspace caches in other AppData paths are unchanged.
  • Startup still succeeds if a leftover tree cannot be deleted; failures are logged.
  • Tests cover mixed layouts (with and without +) and the skip-env / non-onefile no-ops.
  • USER_GUIDE / README mention that only the current unpack is kept after upgrade.
### App version Current packaged Windows builds (Nuitka onefile). Affects `%LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer` after upgrading through several versions / builds. ### Platform Windows ### Steps to reproduce 1. Run a packaged Windows `.exe` (it unpacks into `%LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer\<version+build-id>`). 2. Install / run a newer `.exe` (different version or different git build id). 3. Repeat across several upgrades (release and/or dev builds). 4. Inspect `%LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer`. ### Expected behavior Opening a **newer** packaged build removes extract trees from **older** versions/builds. Only the currently running unpack remains under that product folder. The folder does not keep growing with every upgrade. This is already the documented intent (DR-016, `prune_stale_onefile_caches()` on startup, TECHNICAL_OUTLINE / DESIGN: stale caches pruned on startup). ### Actual behavior `AppData\Local\ValleyGeek\Bulk Image Organizer` grows too large. Artifacts from past versions remain on disk after launching a newer application. ### Additional context `core/onefile_cache.py` already tries to prune sibling cache folders at startup, but it is conservative and can leave leftovers: - Only directories whose names contain `+` are treated as cache segments (`_is_cache_segment_dir`). Older or Nuitka-default `{VERSION}` folders (no `+build-id`) are never deleted. - Failed `rmtree` is logged as a warning and the tree is left in place (locks, AV, in-use files). - Prune is a no-op unless frozen Windows onefile detection succeeds (`exe` parent ≠ unpack root). - Config (`%APPDATA%\bulk-image-organizer`) and workspace caches (`%LOCALAPPDATA%\bulk-image-organizer`) are **not** this folder and must not be deleted. ### Desired fix On packaged Windows startup, after the current extract is in use: - Delete **every** other directory under `%LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer` except the active unpack (not only `+` name siblings). - Keep the current extract; never delete files in use by this process. - Best-effort on locked trees: retry or skip with a log; do not crash startup. - Honor `BIO_SKIP_ONEFILE_CACHE_PRUNE`. - Do not touch `%APPDATA%\bulk-image-organizer` (settings/logs) or `%LOCALAPPDATA%\bulk-image-organizer` (network workspace caches). ### Acceptance criteria - [ ] Launching a newer Windows onefile build removes older version/build extract dirs under `ValleyGeek\Bulk Image Organizer`. - [ ] The running unpack is left intact. - [ ] Folders without `+` in the name (legacy `{VERSION}` extracts) are also removed. - [ ] Settings, logs, and workspace caches in other AppData paths are unchanged. - [ ] Startup still succeeds if a leftover tree cannot be deleted; failures are logged. - [ ] Tests cover mixed layouts (with and without `+`) and the skip-env / non-onefile no-ops. - [ ] USER_GUIDE / README mention that only the current unpack is kept after upgrade.
Member

prune_stale_onefile_caches() only removed sibling unpack dirs whose name contained + (the current <VERSION>+<build-id> convention introduced during beta). Older/legacy <VERSION>-only extract dirs from prior builds never matched that check, so they accumulated indefinitely under %LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer\.

Fix: every directory under the product cache folder other than the active unpack is now removed, regardless of naming. Unchanged: the active unpack is still skipped, a sibling that fails to delete (e.g. locked by AV) is logged and skipped rather than aborting startup, BIO_SKIP_ONEFILE_CACHE_PRUNE is honored, and the check that scopes pruning to the ValleyGeek\Bulk Image Organizer folder under LOCALAPPDATA keeps this from ever touching the separate workspace-cache (LOCALAPPDATA\bulk-image-organizer) or settings (APPDATA\bulk-image-organizer) paths.

Pushed to 0.6.1/issue-167-onefile-cache-prune with new tests for legacy bare-version folders and a locked-sibling case (tests/test_onefile_cache.py), plus README/USER_GUIDE notes. Full suite (702 passed) and ruff pass. Not yet merged to main — awaiting PR request.

`prune_stale_onefile_caches()` only removed sibling unpack dirs whose name contained `+` (the current `<VERSION>+<build-id>` convention introduced during beta). Older/legacy `<VERSION>`-only extract dirs from prior builds never matched that check, so they accumulated indefinitely under `%LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer\`. Fix: every directory under the product cache folder other than the active unpack is now removed, regardless of naming. Unchanged: the active unpack is still skipped, a sibling that fails to delete (e.g. locked by AV) is logged and skipped rather than aborting startup, `BIO_SKIP_ONEFILE_CACHE_PRUNE` is honored, and the check that scopes pruning to the `ValleyGeek\Bulk Image Organizer` folder under `LOCALAPPDATA` keeps this from ever touching the separate workspace-cache (`LOCALAPPDATA\bulk-image-organizer`) or settings (`APPDATA\bulk-image-organizer`) paths. Pushed to `0.6.1/issue-167-onefile-cache-prune` with new tests for legacy bare-version folders and a locked-sibling case (`tests/test_onefile_cache.py`), plus README/USER_GUIDE notes. Full suite (702 passed) and ruff pass. Not yet merged to `main` — awaiting PR request.
Member

Fresh Windows test of the fix above: %LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer still held eleven extract directories spanning 0.4.0 through the current 0.6.1/v0.7.0 branch, including two consecutive builds of that same branch three hours apart — the newer run never cleaned up the one immediately before it.

This is a stronger signal than my original fix accounted for: every listed folder name already contains +, including builds from before this issue's fix (which only required "+" in path.name to qualify for deletion). So dropping the +-only naming restriction cannot have been the operative bug — deletion has evidently never happened on a real Windows run, before or after that change.

prune_stale_onefile_caches() has several early-return guards ahead of the deletion loop (platform/frozen checks, the exe_parent != bundle_root() onefile-detection check, _is_managed_cache_parent()'s layout check), and packaged builds log at INFO while most of these guards previously logged at DEBUG or not at all — so the existing log file couldn't distinguish "a guard short-circuited" from "ran but every deletion failed" from "never called." Rather than guess again at which of several possible causes it is (issue #167's first fix already shows a plausible-looking guess can ship without being the real cause), I added INFO-level logging to every branch of the prune logic — no behavior change — documented as DR-038. Landed in 0.6.1/v0.7.0 at commit eb14446 (issue references corrected in eab99c6).

Next step (needs a Windows run): launch a build with this change, then check %APPDATA%\bulk-image-organizer\logs\bulk-image-organizer.log for the Onefile cache prune lines from that session — that will show exactly which guard is short-circuiting, or the sibling list state if it reached the deletion loop, or which specific deletions failed and why. Could you share that log section next time you run a build?

(Note: I mistakenly opened a separate issue #171 for this instead of commenting here — closing it as a duplicate of this one.)

Fresh Windows test of the fix above: `%LOCALAPPDATA%\ValleyGeek\Bulk Image Organizer` still held eleven extract directories spanning 0.4.0 through the current `0.6.1/v0.7.0` branch, including two consecutive builds of that same branch three hours apart — the newer run never cleaned up the one immediately before it. This is a stronger signal than my original fix accounted for: **every** listed folder name already contains `+`, including builds from before this issue's fix (which only required `"+" in path.name` to qualify for deletion). So dropping the `+`-only naming restriction cannot have been the operative bug — deletion has evidently never happened on a real Windows run, before or after that change. `prune_stale_onefile_caches()` has several early-return guards ahead of the deletion loop (platform/frozen checks, the `exe_parent != bundle_root()` onefile-detection check, `_is_managed_cache_parent()`'s layout check), and packaged builds log at INFO while most of these guards previously logged at DEBUG or not at all — so the existing log file couldn't distinguish "a guard short-circuited" from "ran but every deletion failed" from "never called." Rather than guess again at which of several possible causes it is (issue #167's first fix already shows a plausible-looking guess can ship without being the real cause), I added INFO-level logging to every branch of the prune logic — no behavior change — documented as DR-038. Landed in `0.6.1/v0.7.0` at commit `eb14446` (issue references corrected in `eab99c6`). **Next step (needs a Windows run):** launch a build with this change, then check `%APPDATA%\bulk-image-organizer\logs\bulk-image-organizer.log` for the `Onefile cache prune` lines from that session — that will show exactly which guard is short-circuiting, or the sibling list state if it reached the deletion loop, or which specific deletions failed and why. Could you share that log section next time you run a build? (Note: I mistakenly opened a separate issue #171 for this instead of commenting here — closing it as a duplicate of this one.)
Member

Root cause confirmed — thank you for the log. That one line said everything:

Onefile cache prune skipped: is_frozen_runtime() is False

is_frozen_runtime() and its duplicate _running_packaged() both fall back to try: import __compiled__ except ImportError: return False whenever sys.frozen isn't set — and your real builds evidently never set sys.frozen. The bug: Nuitka does not register a real importable module called __compiled__. It injects __compiled__ as a plain global variable into the namespace of every module it compiles — the documented way to check is "__compiled__" in globals(). import __compiled__ as a statement raises ImportError even inside a genuine compiled build, which is exactly why detection always failed. The test suite never caught this because every existing test either set sys.frozen = True directly (skipping the buggy branch) or mocked __compiled__ as a sys.modules entry — same wrong assumption baked into the tests as the code.

Fixed both functions to use "__compiled__" in globals(). I didn't just trust the docs on this — I built a real Linux Nuitka --standalone smoke binary and ran it headlessly: it correctly wrote to the packaged log path (~/.config/.../logs/, INFO-only, no dev fallback) instead of the temp dev log, confirming the fix works against an actual compiled process, not just reasoning about Nuitka's API. New regression tests simulate the exact confirmed failure shape and were verified to fail without the fix and pass with it. Full details in DR-039.

Pushed to 0.6.1/v0.7.0 at ed41f3a. This should now actually delete the old AppData\Local\ValleyGeek\Bulk Image Organizer extract dirs on the next launch of a build with this fix — appreciate you testing that when you get a chance.

Root cause confirmed — thank you for the log. That one line said everything: ``` Onefile cache prune skipped: is_frozen_runtime() is False ``` `is_frozen_runtime()` and its duplicate `_running_packaged()` both fall back to `try: import __compiled__ except ImportError: return False` whenever `sys.frozen` isn't set — and your real builds evidently never set `sys.frozen`. The bug: Nuitka does **not** register a real importable module called `__compiled__`. It injects `__compiled__` as a plain global *variable* into the namespace of every module it compiles — the documented way to check is `"__compiled__" in globals()`. `import __compiled__` as a statement raises `ImportError` even inside a genuine compiled build, which is exactly why detection always failed. The test suite never caught this because every existing test either set `sys.frozen = True` directly (skipping the buggy branch) or mocked `__compiled__` as a `sys.modules` entry — same wrong assumption baked into the tests as the code. Fixed both functions to use `"__compiled__" in globals()`. I didn't just trust the docs on this — I built a real Linux Nuitka `--standalone` smoke binary and ran it headlessly: it correctly wrote to the packaged log path (`~/.config/.../logs/`, INFO-only, no dev fallback) instead of the temp dev log, confirming the fix works against an actual compiled process, not just reasoning about Nuitka's API. New regression tests simulate the exact confirmed failure shape and were verified to fail without the fix and pass with it. Full details in DR-039. Pushed to `0.6.1/v0.7.0` at `ed41f3a`. This should now actually delete the old `AppData\Local\ValleyGeek\Bulk Image Organizer` extract dirs on the next launch of a build with this fix — appreciate you testing that when you get a chance.
Member

Confirmed fixed from your log: prune_stale_onefile_caches() found only 1 sibling dir under the product folder — the current build itself (0.6.1+adhoc.v0.7.0+f834e80) — meaning every old extract from prior versions was already cleaned up before this run even started. is_frozen_runtime() now correctly detects the onefile runtime (exe_parent=D:\Downloads, unpack_root=...\0.6.1+adhoc.v0.7.0+f834e80) and the deletion loop runs as intended. Thanks for confirming — closing this out.

Confirmed fixed from your log: `prune_stale_onefile_caches()` found only 1 sibling dir under the product folder — the current build itself (`0.6.1+adhoc.v0.7.0+f834e80`) — meaning every old extract from prior versions was already cleaned up before this run even started. `is_frozen_runtime()` now correctly detects the onefile runtime (`exe_parent=D:\Downloads`, `unpack_root=...\0.6.1+adhoc.v0.7.0+f834e80`) and the deletion loop runs as intended. Thanks for confirming — closing this out.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
ai-collab/bulk-image-organizer#167
No description provided.