Repository navigation
Install devlaunch's tools into a pixi home of its own - #277
Merged
Merged
Conversation
Reviewer's GuideThis PR makes devlaunch install its pixi-based tools (gh, claude, zellij) into a dedicated pixi home at $HOME/.devlaunch/pixi instead of the container’s shared ~/.pixi, updates PATH/profile handling accordingly, and adds tests/docs to pin the new behavior and invariants around PIXI_HOME and cache sharing. Sequence diagram for devlaunch tool provisioning with dedicated PIXI_HOMEsequenceDiagram
actor User
participant provision_script
participant Shell
participant pixi
User->>provision_script: provision_script(tools)
provision_script->>Shell: _all_present(tools)
alt tools missing
provision_script->>Shell: _pixi_home_export()
Shell->>Shell: export PIXI_HOME="$HOME/.devlaunch/pixi"
provision_script->>Shell: _pixi_bootstrap()
Shell->>pixi: curl | bash (pixi installer)
pixi-->>Shell: pixi in PIXI_HOME_TARGET/bin
provision_script->>Shell: _install_line(tool) via pixi global install
Shell->>pixi: pixi global install gh/claude/zellij
provision_script->>Shell: _profile_prepend(PATH using PIXI_HOME_TARGET)
Shell-->>User: tools installed in $HOME/.devlaunch/pixi
else all_present
provision_script-->>User: exit 0 (no pixi or profile changes)
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
`pixi global install` is not only an install: it is an edit to
`$PIXI_HOME/manifests/pixi-global.toml`, a *declarative* file that in a container
already has an owner -- an image's, or a dotfiles repo's. Writing there made
devlaunch a second author of a file with one owner, which cost something in both
directions:
1. `pixi global sync` removes every env the manifest does not list, so a
dotfiles apply that rewrites the manifest and syncs *uninstalls* the zellij
the setup pass just installed -- on a schedule nothing here can see, and
the next launch reinstalls it, forever.
2. The manifest is not always a file. kinisi_ros's devcontainer symlinks
`~/.pixi/manifests/pixi-global.toml` onto a tracked file inside the
checkout (its `.devcontainer/on_create.sh`), so the append landed in the
user's work tree and every `git status` in the workspace came up dirty.
Nothing about that is one repo's quirk: dl launches arbitrary repos, and an
install that dirties the tree it was pointed at is the launch damaging the
work it exists to serve.
So PIXI_HOME_TARGET (`$HOME/.devlaunch/pixi`), exported by `_pixi_home_export()`
at the top of both scripts that run pixi, which makes both unrepresentable
rather than handled: nothing syncs that manifest, and every path under it is one
devlaunch created, so no repo state can be beneath it.
Three details are load-bearing, and each is pinned by a test:
- The export comes *before* the bootstrap, not after. pixi's own installer
honours PIXI_HOME (`${PIXI_HOME:-$HOME/.pixi}`), so the other order leaves the
binary in one home and every env it installs in another -- the one shape of
this that fails silently, since both halves work alone.
- Not `$HOME/.local/share/devlaunch/pixi`, the conventional answer. Containers
bind-mount `~/.cache`, `~/.config` and `~/.local/share` straight from the host,
so a prefix tree under one would be shared by every container on the machine
and written into the host's own home. Prefixes are baked with absolute paths
and two syncs sharing one tree is prefix-dev/pixi#5476 -- exactly the hazard
PIXI_CACHE_TARGET refuses to let PIXI_HOME near.
- Never exported into the login profile. A user's own `pixi global install` in a
workspace still goes to their `~/.pixi`, which is the separation this exists to
keep; only the bin directories go on PATH.
What it costs is a duplicate extracted prefix in the case where a tool is
installed but unreachable from a login shell -- disk only, since PIXI_HOME does
not move the download cache and `PIXI_CACHE_TARGET` still shares it -- and that
is the case where the old behaviour reinstalled on every launch anyway.
The devcontainer feature's installer still names `~/.pixi`, deliberately: it is
baked into an image at build time, where that path is the image's own and there
is no checkout to dirty. Its row in the profile-guard table says so.
Verified against real pixi rather than only the fakes: the rendered
`zellij_script()`, run against a home whose manifest is symlinked onto a tracked
file, installs zellij 0.45.0 into `~/.devlaunch/pixi`, leaves the file untouched,
and writes the profile line that keeps the next launch's `command -v` from
reinstalling. The same script before this change appends through the link.
blooop
force-pushed
the
fix/tools-own-pixi-home
branch
from
August 20, 2026 17:20
42fc196 to
c9a992b
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #277 +/- ##
=======================================
Coverage 93.26% 93.26%
=======================================
Files 25 25
Lines 3771 3774 +3
=======================================
+ Hits 3517 3520 +3
Misses 254 254
🚀 New features to boost your workflow:
|
The previous commit fixed devlaunch/tools.py, and `dl` on a user's machine is not that: 0.1.0 onwards is the Rust rewrite, the released binary is an ELF, and the Python tree is the reference implementation beside it. So the fix reached nobody until this. `rust/devlaunch-core/src/flows/provision.rs` gets PIXI_HOME_EXPORT ahead of the bootstrap in both scripts, and PIXI_BOOTSTRAP / PIXI_BIN_LINE / CLAUDE_SHIM_BIN_LINE move to `$HOME/.devlaunch/pixi`. The goldens in its test module -- "what devlaunch/tools.py renders, byte for byte" -- were regenerated from the Python module itself rather than hand-edited, which is the only way the claim in their header stays true; the marks in them are content hashes, so retyping one is a duplicate PATH entry in every workspace forever. The interesting half is what did *not* move. `.devcontainer/claude-code/install.sh` still names `~/.pixi`: it runs at image build time, where that path is the image's own, there is no checkout to dirty and no other owner to race -- and editing it would invalidate the devcontainer prebuild for no gain. So the build-time writer and the two runtime writers now append different lines under different marks, and both land. That is the dedupe working rather than failing: two pixi homes are two directories a login shell genuinely needs, and `_profile_prepend`'s mark being a hash of the line is exactly what keeps one writer from reading the other's work as its own. Three tests had encoded the old agreement and now state the new one: - `Writer::prepended` had a `_ => ".pixi/bin"` catch-all, which is how the build-time and runtime writers came to share an answer they no longer share. Every arm is spelled out; a `_` here would let a fourth writer inherit whichever answer happened to be the default. - `a_marks_digest_is_twelve_hex_characters_of_the_lines_sha256` pins the two new digests, and now also the feature installer's own two -- those are pasted into a shell script that cannot derive them, so nothing but a test holds them. - `the_feature_installer_writes_the_very_fragments_devlaunch_renders` compares against the feature's own lines instead of the runtime consts, which is what the Python test beside it already did. That difference is why the Python suite stayed green on this while the Rust one went red -- worth knowing, because the Rust form is the stronger of the two and it is the one that caught this. cargo test -p devlaunch-core: 1140 passed. clippy --all-targets -D warnings and cargo fmt --check clean.
Found reviewing the two commits before it, and it is the hole they walked through. The goldens in the Rust test module are headed "what devlaunch/tools.py renders, byte for byte" -- and that was a claim in a comment. They are hardcoded strings compared against the *Rust* renderer, so nothing ever compared them to the Python one they are named for. Moving the tools into their own pixi home had to be carried into both trees by hand. The Rust side's internal tests caught a divergence the Python side's did not (the feature installer's PATH line, which deliberately did not move). Had the edit gone the other way -- Python only -- nothing at all would have failed: the goldens would have kept agreeing with the Rust renderer while both drifted from the module they are named after, and the drift would have surfaced as a container whose PATH line no later writer recognises. `rust/tools/parity_cases.txt` does not cover it; it compares dl's observable behaviour, and these scripts are payload strings inside a `devpod ssh --command`. So the comparison is made in the tree that owns the definition: extract every `PYTHON_*` const out of provision.rs and assert it equals the call in devlaunch.tools that is supposed to produce it. No cargo and no Rust toolchain -- it is a pytest, which is what puts the failure in front of whoever edits either side. Two tests rather than one, because the first alone would only check the goldens it happens to have a mapping for: the second fails on any `PYTHON_*` const with no rendering named for it, so a new golden cannot be added outside the check. The mapping is written out rather than discovered, for the reason the module already gives about tests that derive their expectations from the thing under test. Both failure modes were verified by breaking them on purpose: one character of a content-hash mark in a golden fails with the name of the golden, and an unmapped new golden fails with its name.
`Auto-publish` fires on every push to `main` and releases only when `[workspace.package] version` in rust/Cargo.toml differs from its parent commit's, so the bump is what turns this merge into a release; it then reads the release body out of the `## [0.2.1]` section and fails the run if that section is missing. `rust/Cargo.lock` moves with it -- the workspace members inherit the version, and every build in CI passes `--locked`, so a lock left at 0.2.0 is a red build rather than a stale file. `devlaunch-test-support`'s `versioned_with_the_workspace` pins the version as a string and had to be edited too. Worth knowing that no CI step runs it: the rust-parity job tests `devlaunch-runner`, `devlaunch-core`, `aid` and `dl` by name and never that crate, so the one test standing guard over this was found by grep rather than by the gate it exists to be part of.
blooop
enabled auto-merge
August 20, 2026 18:02
# Conflicts: # README.md
0.2.1 went out from main while this branch was open (#272, the --help and README reordering): tag v0.2.1 is on main's tip and the GitHub release exists. So the version bump here had to move, and it is not a cosmetic renumber -- Auto-publish skips a version it finds already tagged, so merging this branch claiming 0.2.1 would have published nothing at all and said so only in a log nobody reads. Cargo.toml, Cargo.lock and devlaunch-test-support's version pin go to 0.2.2, and the changelog keeps both sections: their 0.2.1 as shipped, this branch's notes under 0.2.2 above it. The README conflict is main's reordering rather than a disagreement. That commit moved the whole reference half of the document, including `## The shared pixi package cache`, and left a short intro where it used to be -- so this takes main's text at that position outright and re-places `### Where the tools themselves land` where its neighbours went, still between the shared-cache section and the recreate note. Same for the earlier conflict in this branch: main's shared-cache uid paragraph and this branch's subsection were both insertions at one point, and both are kept.
blooop
added a commit
that referenced
this pull request
Aug 20, 2026
Main moved 43 commits and four releases (0.2.0 through 0.3.0) while this branch was being written, and two of those commits touch the same ground. Reconciled rather than re-applied, since main thought about one of them more carefully than this branch had. **The Rust toolchain in the pixi environment (#284) was already done, better.** This branch had added `rust` as a plain dependency; main added it as a *feature* carried only by `default`, so the py31x solves and their CI caches do not each drag in a 1.5GB toolchain, with the 521MB→2044MB disk trade written down. Main's design is kept and the plain dependency dropped; the collapse to one environment in this branch then removes the reason the feature had to be scoped at all, and its comment says so. One correction on top of it: the pin was `rust = "1.97.*"`, while AGENTS.md told a reader the environment is "pinned to the 1.97.1 that rust-toolchain.toml names". Those disagree the day 1.97.2 is published — silently, and only inside the container, which is the worst place for a toolchain difference to surface. Tightened to `1.97.1.*`, which is what the lockfile already held, so it costs no re-solve and makes main's own sentence true. `test_agents_doc.py` now reads the feature and holds the two to each other exactly. **Nothing was lost to the deletion.** The two Python modules main edited in that window were checked against their Rust counterparts before being removed: `workspace_id.py`'s `TARGET_LENGTH = 47` (#283) is `TARGET_LENGTH: usize = 47` in `domain/workspace_id.rs`, and `tools.py`'s `PIXI_HOME_TARGET = "$HOME/.devlaunch/pixi"` (#277) is `PIXI_HOME_EXPORT`/`PIXI_BIN_LINE`/`CLAUDE_SHIM_BIN_LINE` in `flows/provision.rs`, with byte-goldens pinning the rendered scripts. Both sides moved together, which is what the parity discipline was for. The rest was mechanical: main's `pixi.lock` is lock-file version 7, which needed a newer pixi than the container had, and the manifest's dependency changes here required a fresh solve (932 lines shorter, four environments gone). README keeps main's new paragraph about cargo needing no toolchain of your own — that one is about the crates, not about the Python build this branch ends. `test/unit/test_devcontainer_manifest.py` gained 333 lines on main and imported `tomli`, which this branch removes; it reads `tomllib` now, like its two siblings. Also, while re-reading the goldens that survive: the `PYTHON_*` constants in `flows/provision.rs` are named for where they were transcribed from, and that module is now gone. Kept under their names — what they pin did not change when their author left, and the name is the one thing still recording that nobody read them off the implementation they judge — with a note saying exactly that, and saying that there is nothing left to re-derive them from.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Launching
kinisi-robotics/kinisi_roswithdlleft the workspace'sgit statusdirty, with two envs appended to a tracked file:That repo's devcontainer symlinks the pixi global manifest onto a file in the checkout:
(
.devcontainer/on_create.sh:10, and.docker/dev/entrypoints/entrypoint_dev.sh:123, which re-makes the link on every container start.) So anypixi global installin that container writes into the work tree.[envs.zellij]is ours — the zellij setup stage.[envs.chezmoi]is the dotfiles' own bootstrap, fixed separately in blooop/dotfiles.Why a separate home, rather than a special case for symlinks
The dirty work tree is a symptom. The defect is that
pixi global installedits a declarative file devlaunch does not own, and that costs something in both directions:pixi global syncremoves every env the manifest does not list. A dotfiles apply that rewrites the manifest and syncs therefore uninstalls the zellij we just installed — invisibly, and the next launch reinstalls it, forever.dllaunches arbitrary repos; an install that dirties the tree it was pointed at is the launch damaging the work it exists to serve.$HOME/.devlaunch/piximakes both unrepresentable rather than handled. Nothing syncs that manifest, and every path under it is one devlaunch created.Two commits, because the Python fix reached nobody
0.1.0onwards is the Rust rewrite and the releaseddlis an ELF binary, so the first commit fixes the reference implementation and the second fixes the build that actually ships. Both, or neither is worth having.Three load-bearing details, each pinned by a test
PIXI_HOME(${PIXI_HOME:-$HOME/.pixi}), so the other order leaves the binary in one home and its envs in another — the one shape of this that fails silently, since both halves work alone.~/.local/share/devlaunch/pixi, the conventional path. Containers bind-mount~/.cache,~/.configand~/.local/sharestraight from the host, so a prefix tree under one would be shared by every container on the machine and written into the host's home. Prefixes are baked with absolute paths; two syncs sharing one tree isError: failed to collect prefix recordsduring concurrent execution on HPC (SLURM + Lustre) prefix-dev/pixi#5476 — the exact hazardPIXI_CACHE_TARGETrefuses to letPIXI_HOMEnear.pixi global installin a workspace still goes to their~/.pixi. Only the bin directories go onPATH.What did not move, and why that is the interesting half
.devcontainer/claude-code/install.shstill names~/.pixi. It runs at image build time, where that path is the image's own, there is no checkout to dirty and no other owner to race — and editing it would invalidate the devcontainer prebuild for no gain.So the build-time writer and the two runtime writers now append different lines under different marks, and both land. That is the dedupe working rather than failing: two pixi homes are two directories a login shell genuinely needs, and
_profile_prepend's mark being a hash of the line is what keeps one writer from reading the other's work as its own.Writer::prependedin the Rust tests had a_ => ".pixi/bin"catch-all — which is how the build-time and runtime writers came to share an answer they no longer share. Every arm is now spelled out.Worth noting for the record: the Python test for this compares against the feature's own literals, the Rust test compared against the shared consts. That difference is why the Python suite stayed green here while the Rust one went red — the Rust form is the stronger of the two, and it is the one that caught it.
What it costs
A duplicate extracted prefix in the one case where a tool is installed but unreachable from a login shell. Disk only:
PIXI_HOMEdoes not move the download cache, soPIXI_CACHE_TARGETstill shares every package. And that is the case where the old behaviour reinstalled on every launch anyway.Verification
Against real pixi, not only the fakes. The rendered
zellij_script(), run against a home whose manifest is symlinked onto a tracked file:The same script before this change appends
[envs.zellij]straight through the link. The profile line matters on its own: without it the next launch'scommand -v zellijmisses and reinstalls forever.The Rust goldens ("what
devlaunch/tools.pyrenders, byte for byte") were regenerated from the Python module rather than hand-edited — the marks in them are content hashes, so retyping one is a duplicate PATH entry in every workspace forever.pytest(full suite less e2e): 1884 passed.ruff format --check,ruff check,pylint(10.00/10) clean.cargo test -p devlaunch-core: 1140 passed.clippy --all-targets -D warningsandcargo fmt --checkclean.Summary by Sourcery
Install devlaunch's runtime tools in an isolated pixi home while preserving user and build-time pixi environments.
Bug Fixes:
~/.devlaunch/pixihome.~/.pixi.Enhancements:
~/.pixisetup.Build:
Documentation:
Tests:
Chores: