Skip to content

feat(paths): support the XDG Base Directory specification via the ?state path token - #36

Open
line0 wants to merge 7 commits into
mainfrom
xdg-base-dir-spec-support
Open

feat(paths): support the XDG Base Directory specification via the ?state path token#36
line0 wants to merge 7 commits into
mainfrom
xdg-base-dir-spec-support

Conversation

@line0

@line0 line0 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Companion to arch1t3cht/Aegisub#160, which adds a ?state path token and moves Aegisub's own logs, autosaves and recovered files there. This points DependencyControl's paths at the matching XDG locations, while degrading cleanly on every Aegisub that predates the token.

Where files go

setting before after rationale
paths.log ?user/log ?state/log follows Aegisub's own logs
paths.cache ?user/cache ?local/cache XDG cache home
paths.config ?user/config unchanged already XDG config home

Installed scripts stay under ?user/automation/…, since the PR leaves Path/Automation/* pointing there.

Only Windows and XDG-mode Linux see a real relocation — elsewhere ?state and ?local resolve to the same directory as ?user.

Backwards compatibility

An Aegisub without ?state returns the token verbatim from decode_path, which would leave a literal ?state/log directory next to the working directory. PathOps probes each token once and substitutes a fallback (?state?user) when the running build can't resolve it, so a path setting naming ?state works on every version and no config migration is needed.

Files an earlier release left in the old locations are swept on startup. Identification is by content, not location: log files matching the name Logger gives DepCtrl's own, and cache entries in a directory holding an index that decodes. Aegisub's own logs shared the old log directory and are untouched, as is any file the sweep can't identify. Directories are only removed when the sweep emptied them, and the sweep is self-extinguishing — one directory probe once the location is gone.

Along the way

  • New DependencyControl.PathOps — path composition, validation and token resolution, with no filesystem access. joinPath, pathSegments, getTempDir, validateFullPath and the path constants moved out of FileOps, which keeps deprecated re-exports. The split was forced by the dependency graph: FileOps requires Logger, so nothing at or below Logger could reach its path helpers — which is why Logger was hand-building log paths with a raw /.
  • getNamespacedPath moved to Domain, joining getAutomationDir and getTestDir as the third namespace-to-path mapping.
  • validateFullPathPathOps.resolveFullPath, now returning the absolute directory instead of a device plus a fragment relative to it. The old dir was unusable alone on both platforms, which is why its only consumer concatenated the pair straight back together.

Breaking

FileOps.getAttributes drops dev from its info table and its dir is now absolute. The type shipped in 0.7.0 nine days ago and nothing outside FileOps read it. FileOps.validateFullPath keeps its old four-value shape, deprecated.

Both packages bump to 0.9.0.

Testing

Full suite green (4 packages). New coverage: 8 tests for token fallback, 5 for cache-artifact recognition, 6 for the legacy sweep, plus assertions pinning the legacy shape the deprecated shim still has to produce. Feed validates against schema v0.4.0; types and docs generate with 0 warnings.

Deferred

UNC paths are rejected by resolveFullPath^[A-Za-z]:[/\] never matches \server\share, and the separator-collapsing gsub mangles the leading \ first. Internal-only fix, no API impact, so it's left for its own PR; worth deciding there whether feed-supplied install paths should be allowed to be UNC.

🤖 Generated with Claude Code

line0 and others added 5 commits August 2, 2026 14:52
…oken

Aegisub's XDG support (arch1t3cht/Aegisub#160) adds a ?state path token and
moves its own logs to ?state/log. Point DepCtrl's log directory there and its
cache at ?local/cache, the XDG cache home, leaving config on ?user.

The new path-ops module probes each token once and substitutes a fallback for
one the running build can't resolve, so ?state degrades to ?user on an Aegisub
that predates it and a path setting naming ?state works on every version.
Decoding for logs, caches, package configs and every FileOps path now routes
through it. The headless shim gains ?state (DEPCTRL_STATE_DIR), defaulting to
?user as Aegisub does on Windows and macOS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FileOps requires Logger, so nothing at or below Logger in the dependency graph
can reach its path helpers; Logger assembles log file paths by hand for that
reason. PathOps sits below Logger and now holds the handling that never touches
disk: joinPath, pathSegments, validateFullPath, getTempDir, __getPathRoot, the
long-path detection and the path limit constants.

FileOps re-exports each under its existing name, so external callers are
unaffected. getNamespacedPath stays behind, since domain.validateNamespace
reaches Logger through Enum and would close the cycle. The path tests move to
the PathOps suite, where the limit overrides have to target the fields the
path code actually reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ocations

Identification, not location, decides what goes: a log file matching the name
Logger gives DepCtrl's own, and a cache index whose contents decode as one plus
the snapshot it names. Aegisub's logs shared the old log directory before it
gained ?state, and a cache directory holding no decodable index was never ours
to begin with, so both survive untouched. A directory is removed only when the
sweep emptied it.

Both locations are skipped when they resolve to the configured one, which is
every platform where Aegisub maps ?state onto ?user, and covers a user who set
the old path deliberately. No migration flag is needed: the sweep removes its
own trigger, so it costs one directory probe once the location is gone, and a
file it cannot delete is retried on the next run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MS1 already lists postfix conditionals and loop modifiers among the idioms to
prefer, but says nothing about when a block is the wrong shape for a body that
is one statement. MS5 states the test — it has to fit the line and stay
readable — and shows the `when` filter collapsing a guarded accumulation that
would otherwise carry an inner `if`.

PathOps.isTokenSupported had the memo lookup's early return in a block; it
folds into a postfix `unless`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nd defaults

Resolving an index entry's snapshot — joining it onto the cache directory and
confirming the file is still there — was written out in both getFile and get,
so the rule for where a snapshot lives had two sites. A private
__snapshotPath holds it once; get keeps its own index read, so nothing pays
for a second decode.

The lock namespace spelled out l0.DependencyControl, which constants already
carries, and the lock holder name repeated the class name a third time. Both
derive now, so renaming the class can't leave a stale identity behind (ID1).

The FileCacheOptions annotations restated the 3600 and 50 that defaultMaxAge
and defaultMaxFiles define twelve lines below, with nothing keeping the two in
step; they point at the fields instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Test results — macos-latest ✅1084 · ubuntu-latest ✅1084 · windows-latest ✅1081

Summary

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Other ❓ Flaky 🍂 Duration ⏱️
3252 3249 0 3 0 0 1m 8s

🎉 No failed tests in this run.

Github Test Reporter by CTRF 💚

🔄 This comment has been updated

line0 and others added 2 commits August 2, 2026 15:50
…ve callers over

validateFullPath split a "device" off the front of the directory, leaving `dir`
relative to it: `/usr/local/bin` came back as `/usr` plus `/local`, and
`C:\Users\foo\bar.txt` as `C:\` plus `Users\foo`. Neither is a directory a
caller can use, which is why the one consumer of the pair, FileOps.mkdir,
concatenated them straight back together. It now reports the whole directory,
which frees the second return to be an error and nothing else — it previously
carried the device on success, and getAttributes already misread it as an error
under the name `dev`.

The name went with the shape. "Validate" described only the failure half, while
every call site wants the canonical absolute path it produces, so the PathOps
member is resolveFullPath. FileOps.validateFullPath keeps the pre-0.9 four
values for callers written against it, deprecated, splitting the root back off
through PathOps._getPathRoot; the same split backs the pre-0.7 `attributes`
shim. FileOpsAttributesInfo drops `dev` outright and its `dir` is absolute: it
shipped in 0.7.0 nine days ago and nothing outside FileOps read it.

Internal callers move off the deprecated re-exports at the same time, since
deprecating a function and then calling it from every module advertises the
opposite of what the deprecation says. Test doubles follow their subjects, and
their module-name constants gained the word boundary they were missing —
FILEOPS only passed the spell check because cspell's AWS dictionary carries it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The repo spell-checks against cspell's en-US dictionary, which rejected the two British forms in these comments.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant