Repository navigation
WIP: feat(install): resolve dependencies from local disk (salvage feat/local-project-registry) - #457
Conversation
… registry `zed install` could only ever ask the configured registry where a package lives. That is the wrong answer while the registry itself is being built, on an offline or air-gapped machine, and whenever the dependency is a checkout sitting next to the project being edited. Workspace members solve this inside one tree; nothing solved it across trees. Add `zed local`, a shared index of on-disk projects under `ZED_PKG_HOME` that installs consult before the network. Entries are keyed by canonical filesystem path, not by package name. One package legitimately exists in several checkouts at once — a release clone, a feature worktree, a bisect tree — and a name-keyed registry would have to silently pick one. Several registrations of one package are therefore normal; `--priority` chooses between them, and a tie on both priority and version is a hard error naming both paths rather than a coin flip. Resolution is live: the recorded version is a registration-time snapshot for reporting and drift detection, while the manifest on disk is authoritative on every install. Selected projects are materialized through the same source-link path as workspace members, so symlink mode writes an absolute canonical directory symlink into the checkout (edits are visible to consumers with no reinstall) and copy mode produces a standalone tree via the existing guarded copier. Transitive dependencies of a linked project are read from that checkout's manifest, so a whole local graph links at once. `--local-registry` sets how much authority registrations have: `off`, `auto` (default; ordinary installs only), `prefer` (also `--frozen`), or `only` (fully offline — an unsatisfiable dependency errors instead of reaching the network). `--frozen` opts out by default because the index is machine-global ambient state and letting it override a pin would make "frozen" mean something different on every laptop. Requirements satisfied from live source are exempt from the frozen lockfile-pin check, exactly as workspace members already are. Hardening. Registration refuses a missing path, a non-directory, a missing or symlinked `.zpkg.toml`, a manifest that fails to validate, a non-UTF-8 path, and anything inside (or containing) the Zed home directory — the store holds extracted artifacts, and registering one would feed materialized output back in as source. The index must be a regular non-symlink file under 8 MiB with a known schema, at most 4096 entries, each slug-valid, versioned, absolute, and unique by path; a duplicate is reported rather than silently collapsed. Every read-modify-write runs under an exclusive zed-lock guard and is staged-then-renamed at 0600 under a 0700 directory, so concurrent `zed local register` runs cannot lose one another's entry. `zed local scan` is depth- and visit-bounded, follows no symlinks, and never descends into dependency, build, or VCS trees. At install time an entry whose path, manifest, or identity broke is reported by path and skipped; one that overlaps the consuming project in either direction is refused before it can put `zed_modules/<org>/<name>` inside its own link target; and a package already pinned to a registry artifact is never switched to a source link behind the version-conflict check. Tests cover the index unit-level, the CLI end to end against an empty `file://` registry (so any install that succeeds did so locally), and the adversarial surface: 16 concurrent registering processes, hand-corrupted and symlinked index files, oversized indexes, symlinked manifests, symlinked project spellings, ambiguous ties, and overlapping registrations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fvi78rjurJXbT9XYJv72by
…containers The local project registry assumed every registered checkout lives on the system disk and keeps the same absolute path forever. Neither holds where it is most useful: a checkout on an external SSD, a bind mount inside a container, or a BuildKit mount that exists for one `docker build` step. * Volume classification. Registration records where the checkout lives (`fixed`, `removable`, `network`, `container-mount`) from `/proc/self/mountinfo` where readable and from the conventional mount layout (`/Volumes/<label>`, `/run/media/<user>/<label>`, ...) everywhere else, which is what makes this work on macOS. * An `unavailable` health state. An unmounted volume is no longer reported as a directory that was deleted. `zed local prune` keeps such entries -- an external drive that happened not to be attached this afternoon is not a mistake to clean up -- while a genuinely missing directory is still dropped. Neither fails an install; resolution falls through to the registry with a reason, unless `--local-registry=only` forbids that. * Link policy. A checkout on removable, network, or container-mount storage is copied into `zed_modules/` even in symlink mode: a link into media that can vanish is a project that breaks when it does, far from the install that caused it. Linking stays allowed when the consuming project shares the volume -- nothing outlives it either way. `zed local register --link`, `--local-link-policy`, and `--local-ephemeral` override, most specific last. * Path mapping. `--local-path-map /host/codes=/work` (`ZED_PKG_LOCAL_REGISTRY_PATH_MAP`) translates registered paths across a bind mount, in both directions: entries are rewritten into the local view on load and back into host form on save, so one index shared through a volume stays meaningful on the host and inside every container it runs. Longest source prefix wins, and whichever side exists here is canonicalized so a rule written through a symlink (`/tmp` is `/private/tmp` on macOS) still matches. Without a map, host-shaped entries simply do not resolve and the install says which path it looked for. * `zed local doctor` prints that whole picture -- index location, container detection, mapping, policy, and each entry's volume and status -- which is the answer to "why did my install go to the network?". Also: `zed r2g` now installs its mock consumer with `--local-registry=off`. A roundtrip exists to prove the published artifact installs; letting a local registration of the same package substitute source would turn that guarantee into a tautology. Tests: 19 new unit tests in `src/local_registry.rs` covering classification, mount-table parsing, the unavailable/missing split, path mapping in both directions, and link-policy resolution; `tests/local_registry_portability.rs` drives removable media, unmounted volumes, bind-mount path maps, a mounted index, and image builds through the real binary; `tests/local_registry_vs_remote.rs` publishes the same package to a `file://` registry and registers a different copy locally, so precedence, fallback, requirement mismatch, `--local-registry off|only|prefer`, frozen replay, and transitive resolution are each decided by which bytes land in `zed_modules/`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ViG27Hx7QA9mYQ2ernM5n
…e tree compile Two commits landed on this branch from separate sessions, neither of which could be compiled where it was written (crates.io is unreachable from those sandboxes). Built and tested here, they do not hold up. This is that reconciliation, plus the piece the feature was missing. **Registrations are now resolved in the dependency solver.** `zed install` runs `install_graph::prepare` before the materializer, and that solver went straight to the registry for every key. A registered checkout therefore only worked when the package *also* existed remotely — precisely the case the local registry exists to avoid. The solver now consults the index at the same point it consults workspace members: a locally satisfied key is recorded from live source, never fetched and never prefetched. Because the solver is where the network would first be contacted, it is also where `--local-registry=only` is enforced and where the fall-through to the registry is reported; the materializer keeps the bail as a backstop for the paths that skip solving, and no longer duplicates the note. `select_with` lets the solver judge a candidate from its whole manifest, since it accumulates several constraints and interprets them under the package's own version scheme. **Compile fixes.** `Config::from_globals` initialized `local` twice; the `Globals` literal in its test predated three new fields; sixteen `register` call sites predated the explicit link-policy argument; two `validate_frozen_manifest_requirements` call sites predated its source-links parameter; and `LinkPolicy::resolve` had two identical arms, which `clippy -D warnings` rejects. The three new global flags were declared in `.cli-flags.toml` but not in `.dev-cli-flags.toml`, so `zed develop`'s contract no longer matched its typed parser. **Two semantic corrections**, both cases where the code contradicted its own documented rule. `volume_is_present` treated "this directory is not a mount point" as proof that a removable volume was detached. That is a sound heuristic only for Linux's leftover mount directory, which is empty — it must not outvote a checkout that is demonstrably readable underneath the mount point. It now does not, so a registration on media that was classified generously is used rather than reported unavailable, while a genuinely absent mount point is still absent. `link_decision` compared device numbers to decide whether the consuming project "shares the volume" with the checkout. Two bind mounts of one filesystem share a device id while having entirely different lifetimes, which is exactly what a container mount is, so device equality is the wrong question. It now compares recorded volume identity — mount point and kind — and falls back to device ids only when neither side has a mount point. Verified: `cargo clippy --all-targets -- -D warnings` clean, and 877 of 878 tests pass. The remaining failure is `validation::tests::checked_in_schemas_are_pinned_to_the_dependency_revision`, which asserts `Cargo.toml` names the pinned `zed-interfaces` revision; it fails only because building here required temporarily pointing the three git dependencies at local checkouts. `Cargo.toml` is committed unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fvi78rjurJXbT9XYJv72by
…m local disk Salvage of the unmerged branch origin/feat/local-project-registry (eeaf911, 8730f73, 2ebef8d) onto current main, part of #378. Conflicts resolved by keeping both sides' intent: - Config gains `local: LocalPortability` alongside main's `mirrors` and `fallback`; every test/literal Config sets all three. - Globals keeps main's no_mirrors/trust_mirror_metadata/r2_public_*/ source_fallback flags and adds the branch's local_path_map, local_link_policy, local_ephemeral; .cli-flags.toml carries both sets. - resolver::prepare keeps main's MirrorContext-based registry and pool and threads the branch's local index and LocalRegistryMode into solve_install. - lib.rs keeps main's lifecycle module plus local_cli/local_registry. - r2g's mock consumer inherits main's mirrors/fallback and keeps the branch's LocalRegistryMode::Off. Also drops the duplicate `use crate::registry::registry_for;` at the end of src/install_graph/hardening_tests.rs that broke `cargo test --lib` on main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The branch's config_for (local_registry.rs) and config_for_test (ops.rs) merged without textual conflict but predate main's `mirrors`/`fallback` Config fields, so `cargo test --lib` failed with E0063. Set them to the same inert values every other test Config on main uses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # src/lib.rs
…ignatures, signing_keys) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ORESoftware
left a comment
There was a problem hiding this comment.
Coordination note for DEN-3282 / merged zed-interfaces#138: local machine registrations must remain below explicit project-authored source authority. The resolver/materializer precedence should be documented and tested as:
- explicit
[overrides.path]for that dependency; - project-owned providers: ordinary
[workspace].membersand[interop.source-composition]entries withrole = "workspace"; - opt-in machine-local registry according to
--local-registrypolicy; - remote registry/fallback.
A machine registration must never silently replace a manifest-authoritative source-composition workspace provider or an explicit path override. Conversely, local-registry=only may suppress remote access but should not disable already-declared project-local providers. Frozen mode must also retain DEN-3282's rule that mutable ordinary source-composition checkouts are refused until an immutable workspace-source lock exists; the machine registry's prefer mode must not be used to bypass that source-lock boundary.
Please add cross-feature tests before this PR leaves WIP, especially the same package present in [overrides.path], source composition, and the local registry.
Part of #378 — the "look on local disk for deps/packages" half of the requirement (the github.com half merged in #379).
Salvages
feat/local-project-registry(3 commits from 2026-08-23, unmerged for ~3 weeks) onto currentmainwith a real merge, no rebase.What it adds
A machine-wide index of local project checkouts that
zed installcan resolve from, so a dependency that already exists as a directory on this filesystem needs no network at all:zed local register|list|unregister|scan|prune(src/local_cli.rs,src/local_registry.rs)<ZED_PKG_HOME>/local-registry/index.json, schemazed.local-registry.v1--local-registry/ZED_PKG_LOCAL_REGISTRY(auto/only/off)State: WIP — 2 known failures, wiring incomplete
mainrefactored install resolution while this branch sat (#456,local_overrides), so the salvaged gate is no longer consulted on every path:local_registry_off_ignores_registrations_entirely—--local-registry offstill resolves locally instead of consulting the remote registrylocal_registry_only_names_the_dependency_it_cannot_satisfy—--local-registry onlyfalls through to the file registry instead of failing with the local-only diagnosticEverything else passes: 653 lib tests, 16 adversarial, 15/17 CLI, plus portability and vs-remote suites compile and run. Conflicts resolved:
src/lib.rs(keeplocal_cli+local_registryalongside main'slocal_overrides), and the salvaged fixtures gained main's newmirrors/signatures/signing_keysmetadata fields.Relationship to #381
#381 (
DEN-3488-local-dev-repo-race) implements a different, automatic design: bounded discovery of checkouts under~/codes,~/code,~/src… for non-frozen symlink installs. It is currently unmergeable (dirty) with 66 failing checks. This PR is the opt-in, explicitly registered counterpart that also covers copy mode. They are complementary, but the two resolution paths should be reconciled before both land.Next
ZED_PKG_SOURCE_FALLBACK=0.🤖 Generated with Claude Code