Skip to content

feat: XDG-compliant directory layout via unified Dirs resolution - #2346

Draft
forehalo wants to merge 5 commits into
voidzero-dev:mainfrom
forehalo:feat/dirs-path-resolution
Draft

feat: XDG-compliant directory layout via unified Dirs resolution#2346
forehalo wants to merge 5 commits into
voidzero-dev:mainfrom
forehalo:feat/dirs-path-resolution

Conversation

@forehalo

@forehalo forehalo commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Close #827

Summary

Makes vite-plus follow the XDG Base Directory specification for fresh installs while keeping every existing install working untouched. Two commits:

  1. vp_shared::Dirs — a single owner of all on-disk placement decisions; all priority chains are internal details.
  2. Installer cutover — fresh installs default to the split XDG layout; VP_HOME is deprecated but still honored.

Layout resolution (first match wins)

  1. VP_HOME set (deprecated) → legacy monolithic layout. Still honored so older env scripts and custom-location installs keep working; also an explicit escape hatch for layouts the heuristics can't disambiguate.

  2. Executable self-locationcurrent_exe matches <X>/current/bin/vp: <X>/bin/vp exists → legacy root; otherwise X is a split install's data dir (data category pinned to the running binary's payload). Covers IDE/trampoline launches without PATH context.

  3. PATH inference — legacy sibling check, then canonicalize vp entries through the same shape check.

  4. ~/.vite-plus exists → legacy layout, grandfathered; nothing is moved.

  5. Split XDG layout (fresh installs):

    Category Chain
    bin VP_BIN_DIRXDG_BIN_HOMEXDG_DATA_HOME/../bin~/.local/bin (uv's chain)
    config XDG_CONFIG_HOME/vite-plus~/.config/vite-plus
    data (runtimes, package managers, versions, bins/*.json) VP_DATA_DIRXDG_DATA_HOME/vite-plus~/.local/share/vite-plus
    state XDG_STATE_HOME/vite-plus~/.local/state/vite-plus
    cache VP_CACHE_DIRXDG_CACHE_HOME/vite-plus~/.cache/vite-plus

    Only bin/data/cache get dedicated VP_*_DIR overrides (mirroring uv's UV_TOOL_BIN_DIR / UV_PYTHON_INSTALL_DIR / UV_CACHE_DIR); config and state rely on the standard XDG vars. Relative values are ignored per the spec. Windows: %LOCALAPPDATA%\vite-plus\{bin,data,state,cache}, config under %APPDATA%\vite-plus.

What changed

  • crates/vp_shared/src/dirs.rs (new, absorbs home.rs): Dirs + DirsInner (Home/Custom), pure injectable resolution core (no env mutation or serial_test), 5 category accessors, 12 named helpers, is_legacy_layout(). Test helpers live in a #[cfg(test)] impl block; cross-crate tests sandbox via EnvConfig::for_test_with_home.
  • Env plumbing: VP_BIN_DIR/VP_DATA_DIR/VP_CACHE_DIR, DEPRECATED_VP_HOME, and the XDG_*_HOME names are constants in env_vars.rs; EnvConfig carries only vp_* path overrides — Dirs reads the XDG vars itself.
  • Call-site migration: all get_vp_home() uses and hand-rolled $VP_HOME/... constructions across the workspace now go through Dirs; per-crate wrapper helpers deleted.
  • Installers: install.sh/install.ps1/Dockerfile default to the split layout; explicit VP_HOME/--install-dir or an existing ~/.vite-plus keeps legacy. vp_installer resolves an InstallLayout from Dirs. The Windows trampoline and vp-use.cmd support both layouts (and only export VP_HOME for legacy).
  • vp implode removes either layout: split = data/config/state/cache dirs plus only vp-owned shim names from the bin dir (a shared ~/.local/bin is never removed); profile stripping handles both.
  • TS parity: the global CLI injects resolved VP_BIN_DIR/VP_DATA_DIR/VP_CACHE_DIR into JS child processes under the split layout only; org-tarball.ts prefers VP_CACHE_DIR; git hook scripts fall back through VP_BIN_DIRVP_HOME/bin~/.vite-plus/bin~/.local/bin.
  • Docs: layout resolution documented in installer-env-vars.md; env.md/install.md/implode.md/CONTRIBUTING.md/AGENTS.md updated; supersede note in rfcs/env-command.md.

Migration impact

  • Existing ~/.vite-plus installs: zero impact (grandfathered; re-running the installer upgrades in place).
  • Custom VP_HOME installs: keep working — the variable is still honored (deprecated), and the binary also self-locates without it.
  • Fresh installs: split layout; ~/.local/bin is the only PATH entry needed.

Validation

  • cargo check/clippy --workspace --all-targets: zero warnings; cargo fmt clean
  • cargo test on all touched crates: ~1500 tests green
  • PTY snapshot suite: 622/622 cases pass, including two fixtures rewritten for the post-VP_HOME isolation model
  • TS: 200 unit tests pass, including the shell end-to-end test for the modified git hook script

@netlify

netlify Bot commented Aug 5, 2026

Copy link
Copy Markdown

Deploy Preview for viteplus-preview ready!

Name Link
🔨 Latest commit 08b7589
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-preview/deploys/6a743dbbc2a00d000833d939
😎 Deploy Preview https://deploy-preview-2346--viteplus-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@forehalo
forehalo force-pushed the feat/dirs-path-resolution branch 4 times, most recently from b8069a5 to 2f8be03 Compare August 5, 2026 17:13
@forehalo forehalo changed the title feat: introduce Dirs for unified XDG-aware path resolution feat: XDG-compliant directory layout via unified Dirs resolution Aug 6, 2026
Add vp_shared::Dirs as the single owner of on-disk placement decisions.
An internal DirsInner enum selects the layout once per resolution (first
match wins):

- Home (legacy monolithic root): VP_HOME is set, the vp binary
  self-locates at <root>/current/bin/vp, a legacy layout is found on
  PATH, or ~/.vite-plus exists on disk. Existing installs keep working
  untouched, byte-identical paths.
- Custom (split XDG layout, fresh installs): each category resolves
  through its own VP_*_DIR override -> XDG_* -> platform-default chain
  (bin: VP_BIN_DIR -> XDG_BIN_HOME -> XDG_DATA_HOME/../bin -> ~/.local/bin,
  mirroring uv). Only VP_BIN_DIR/VP_DATA_DIR/VP_CACHE_DIR exist as
  dedicated overrides; config and state rely on XDG_CONFIG_HOME /
  XDG_STATE_HOME. Relative values are ignored per the spec.

XDG_* names are defined alongside vp's own variables in env_vars.rs, but
only vp_* path overrides live in EnvConfig; Dirs reads XDG vars itself
behind an injectable, parallel-safe resolution core (no env mutation or
serial_test anywhere in Dirs tests; test helpers live in a #[cfg(test)]
impl block, and cross-crate tests sandbox through
EnvConfig::for_test_with_home).

Migrate every Rust call site to Dirs category accessors and named
helpers, delete get_vp_home() and the home.rs module (folded into
dirs.rs), and drop per-crate wrapper helpers so Dirs is the sole path
source. bins/*.json metadata moves to the data category.

On the TS side, the global CLI injects the resolved VP_BIN_DIR /
VP_DATA_DIR / VP_CACHE_DIR into JS child processes under the split layout
only (never overriding user-set vars); org-tarball.ts prefers
VP_CACHE_DIR; generated git hook scripts fall back through VP_BIN_DIR,
VP_HOME/bin, ~/.vite-plus/bin, ~/.local/bin.

VP_HOME, the installers, and the generated env* shell scripts are
unchanged in behavior: fresh installs still land in ~/.vite-plus, so the
split layout is opt-in via VP_*_DIR/XDG until the follow-up stack removes
the legacy variables and switches installer defaults.

Groundwork for voidzero-dev#827.
Fresh installs now land in the split XDG layout: versions and the
current symlink under the data dir (~/.local/share/vite-plus,
%LOCALAPPDATA%\vite-plus\data), the vp wrapper and shims in the bin dir
(~/.local/bin, %LOCALAPPDATA%\vite-plus\bin), and env scripts in the
config dir (~/.config/vite-plus, %APPDATA%\vite-plus). install.sh,
install.ps1, and the Dockerfile switch their defaults accordingly;
explicit VP_HOME/--install-dir or an existing ~/.vite-plus keeps the
legacy monolithic layout, so re-running the installer upgrades existing
installs in place.

VP_HOME becomes DEPRECATED_VP_HOME: still honored by the CLI as the
highest-priority layout rule (legacy monolithic root) so older env
scripts and custom-location installs keep working, and kept by the
installers as an install-dir override — but the installers and the
generated env* scripts no longer set it, and the Windows trampoline and
vp_installer only export it for legacy-layout installs. This also gives
an explicit escape hatch for layouts the detection heuristics cannot
disambiguate (e.g. VP_BIN_DIR pointed inside the data dir).

Layout detection learns to distinguish split installs: a
<X>/current/bin/vp shape classifies as legacy when <X>/bin/vp exists,
otherwise X is the split data dir and the data category is pinned to the
running binary's payload. PATH inference canonicalizes vp entries
through the same shape check.

vp implode removes the split layout (data/config/state/cache dirs plus
only the vp-owned shim names from the bin dir; a shared ~/.local/bin is
never removed) and strips profile lines for both layouts. vp-use.cmd and
the trampoline gain the split fallback path.

Snapshot fixtures move off per-step VP_HOME: the runner provisions the
complete legacy shape, and isolated-install cases use real on-disk
layouts driven through their own binaries (with VP_HOME cleared so
self-location governs). Docs describe the split layout as the
fresh-install default and VP_HOME as deprecated.
…tallers

Self-location never fired in production: canonicalizing current_exe
resolves the current symlink, yielding <X>/<version>/bin/vp rather than
the <X>/current/bin/vp shape the check required. Detection now accepts
both shapes (install_parent_candidate) and validates the install on disk
(classify_exe): <X>/bin/vp plus <X>/current/bin/vp means a legacy root,
only the latter means a split data dir, neither means not an install.
Verified end-to-end with a sandboxed split install (env scripts land in
~/.config/vite-plus, doctor pins the split data dir).

CI compatibility: install.sh/install.ps1 are fetched independently of
the CLI release line, so the released (pre-split) CLI must not be
installed by the new split-only scripts. The pre-XDG scripts are
preserved as frozen legacy_install.sh/legacy_install.ps1 for exactly
that combination, and test-standalone-install.yml now runs them (job
names unchanged for branch protection). The new scripts stay free of
compatibility logic; split-layout e2e for them runs against the
registry-bridge preview build in publish-preview.yml (same-repo PRs,
like the bridge registration). The Docker image carries both candidate
bin dirs on PATH and cleans both runtime locations so it works across
the transition, and the docs site serves the legacy installers.

Also gate PlatformDefaults::unix to non-Windows/test builds to fix the
Windows cross-compile (-D warnings) dead-code error, and format docs
with vp check --fix.
The test-install-split job landed between publish-docker-preview's env
block and its steps, producing a duplicate steps key that failed the
zizmor security analysis. Move it after the complete docker job.
test_uninstall_removes_shims_from_metadata and
test_restore_previous_install_state_removes_partial_new_bins managed
shims in <temp>/bin while the code under test removes them from
Dirs::bin_dir(). Under the reworked for_test_with_home (user_home
override) a fresh temp home resolves the split layout
(<temp>/.local/bin), so the two never met. Unix masked the mismatch:
the shim symlinks dangle, and dangling-symlink exists() is false, so
the "removed" assertions passed vacuously. On Windows the trampoline
.exe shims are real files, so Test (Windows) failed.

Create <temp>/.vite-plus in both tests so resolution selects the legacy
layout and bin_dir() equals the directory the tests manage.
@forehalo
forehalo force-pushed the feat/dirs-path-resolution branch from 8fa20f7 to 08b7589 Compare August 6, 2026 07:54
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.

use xdg config

1 participant