Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
2effdf2
feat: add recursive install graph scaffold
ORESoftware Aug 3, 2026
727882a
feat: resolve recursive dependency graphs concurrently
ORESoftware Aug 3, 2026
fdcf9ae
feat: add atomic artifact fetches with process locks
ORESoftware Aug 3, 2026
febf856
test: cover recursive installs and process locking
ORESoftware Aug 3, 2026
e973658
refactor: route installs through recursive prefetch
ORESoftware Aug 3, 2026
52def3d
refactor: expose recursive installer facade
ORESoftware Aug 3, 2026
5fb1928
docs: explain recursive concurrent installs
ORESoftware Aug 3, 2026
1cbf780
refactor: use normal recursive install submodules
ORESoftware Aug 3, 2026
4d3514c
refactor: isolate recursive graph resolution
ORESoftware Aug 3, 2026
9c5d243
refactor: isolate atomic artifact workers
ORESoftware Aug 3, 2026
564e15e
test: prove concurrent processes share one download
ORESoftware Aug 3, 2026
98d4e32
fix: preserve deterministic dependency expansion
ORESoftware Aug 3, 2026
24038ef
fix: tag worker results with deterministic sequence
ORESoftware Aug 3, 2026
bbca238
fix: expand completed manifests in submission order
ORESoftware Aug 3, 2026
75658c8
style: apply rustfmt to installer facade
ORESoftware Aug 3, 2026
df48577
style: apply rustfmt to install graph
ORESoftware Aug 3, 2026
bc77b5b
style: apply rustfmt to artifact worker
ORESoftware Aug 3, 2026
2d3bce4
style: apply rustfmt to graph resolver
ORESoftware Aug 3, 2026
99ca21a
style: apply rustfmt to recursive installer tests
ORESoftware Aug 3, 2026
c8be179
test: cover warm stores frozen locks and conflicts
ORESoftware Aug 3, 2026
58d688b
style: format expanded recursive installer tests
ORESoftware Aug 3, 2026
df9a552
feat: prefetch in-memory add and remove manifests
ORESoftware Aug 3, 2026
0b1e11a
fix: route add and remove through recursive prefetch
ORESoftware Aug 3, 2026
f84c366
test: measure five concurrent recursive artifact transfers
ORESoftware Aug 3, 2026
bceed2a
style: format recursive concurrency integration test
ORESoftware Aug 3, 2026
59b86f9
Merge main into recursive installer branch
ORESoftware Aug 3, 2026
6cb56af
chore: align recursive branch with bootstrap cleanup
ORESoftware Aug 3, 2026
51346b7
chore: align recursive branch with bootstrap cleanup
ORESoftware Aug 3, 2026
2c15829
chore: align recursive branch with bootstrap cleanup
ORESoftware Aug 3, 2026
c1421f6
test: satisfy clippy in recursive install fixtures
ORESoftware Aug 3, 2026
08fd56f
chore: stage shared artifact acquisition patch
ORESoftware Aug 3, 2026
e464e03
ci: validate shared artifact acquisition patch
ORESoftware Aug 3, 2026
798468a
fix: unify artifact acquisition locks
github-actions[bot] Aug 3, 2026
60db3f7
docs: describe unified artifact acquisition lock
ORESoftware Aug 3, 2026
6aa8edc
Harden recursive worker failure handling (#63)
ORESoftware Aug 3, 2026
c2e7fa8
style: apply rustfmt to worker panic boundary
ORESoftware Aug 3, 2026
b559a32
chore: stage cross-platform process-lock test
ORESoftware Aug 3, 2026
5cd31d0
ci: add Windows recursive artifact locking contract
ORESoftware Aug 3, 2026
5e9b86d
ci: validate cross-platform artifact lock test
ORESoftware Aug 3, 2026
96f586c
ci: trigger cross-platform artifact lock validation
ORESoftware Aug 3, 2026
96e809e
ci: allow PR-triggered cross-platform lock validation
ORESoftware Aug 3, 2026
599cb18
test: exercise artifact lock across processes
github-actions[bot] Aug 3, 2026
fabf5c1
docs: describe cross-platform process lock validation
ORESoftware Aug 3, 2026
8c9d3fa
chore: stage kernel-blocking store lock patch
ORESoftware Aug 3, 2026
2aec1bc
ci: validate kernel-blocking store lock patch
ORESoftware Aug 3, 2026
b7893ae
ci: satisfy fail-closed workflow concurrency policy
ORESoftware Aug 3, 2026
0b7f524
ci: make blocking-lock bootstrap idempotent and code-only
ORESoftware Aug 3, 2026
4349127
ci: publish validated store code without workflow writes
ORESoftware Aug 3, 2026
a407898
fix: block store locks in the kernel
github-actions[bot] Aug 3, 2026
6e49f62
test: stage owner-exit process lock regression
ORESoftware Aug 3, 2026
b0d2a97
test: validate owner-exit lock release before publishing
ORESoftware Aug 3, 2026
e0b7480
test: run store lock blocking and owner-exit regressions on Windows
ORESoftware Aug 3, 2026
aed930e
fix: block store locks in the kernel
github-actions[bot] Aug 3, 2026
dfa2e19
ci: remove owner-exit lock test bootstrap
ORESoftware Aug 3, 2026
f84a754
ci: remove store-lock patch bootstrap
ORESoftware Aug 3, 2026
3a36b52
ci: keep permanent Linux blocking-lock regression gate
ORESoftware Aug 3, 2026
3e0f6e6
fix: validate dependency identities and embedded manifests
ORESoftware Aug 3, 2026
c51c501
fix: preserve lockfile order for frozen worker failures
ORESoftware Aug 3, 2026
dc6544e
test: harden recursive manifest, identity, slug, and ordering contracts
ORESoftware Aug 3, 2026
ddef3e4
test: register recursive resolver hardening suite
ORESoftware Aug 3, 2026
cea860b
style: apply Rustfmt to resolver hardening tests
ORESoftware Aug 3, 2026
e16fe93
ci: synchronize final cross-platform lock ancestry
ORESoftware Aug 3, 2026
8518e23
merge: synchronize cross-platform lock base
github-actions[bot] Aug 3, 2026
a1009fb
merge: align conflicted workflow with final cross-platform base
ORESoftware Aug 3, 2026
19c962e
ci: remove completed lock-stack synchronization workflow
ORESoftware Aug 3, 2026
1232fc1
test: restore expanded Windows store-lock regressions after base sync
ORESoftware Aug 3, 2026
67c01b8
merge: synchronize blocking-lock base into resolver hardening
ORESoftware Aug 3, 2026
09f2f6c
test: stage workspace adapter and frozen-install implementation
ORESoftware Aug 3, 2026
9de1d32
ci: validate and publish workspace install wiring
ORESoftware Aug 3, 2026
62954bc
ci: trigger workspace patch validation from the stacked PR
ORESoftware Aug 3, 2026
235ef28
fix: wire and restore workspace packages
github-actions[bot] Aug 3, 2026
93e26a6
ci: remove completed workspace install patch bootstrap
ORESoftware Aug 3, 2026
56e597b
ci: remove completed workspace install publishing workflow
ORESoftware Aug 3, 2026
3bbf82d
test: stage workspace-aware uninstall and wiring cleanup
ORESoftware Aug 3, 2026
c96abd0
ci: validate workspace uninstall and adapter cleanup
ORESoftware Aug 3, 2026
29df590
fix: uninstall workspace projections transactionally
github-actions[bot] Aug 3, 2026
678dbb5
ci: remove completed workspace uninstall patch helper
ORESoftware Aug 3, 2026
8cc6745
ci: remove completed workspace uninstall publishing workflow
ORESoftware Aug 3, 2026
8589587
ci(DEN-1505): map current-main recursive-stack conflicts
ORESoftware Aug 3, 2026
1bef5b8
ci(DEN-1505): expose merge diagnostics on the PR head
ORESoftware Aug 3, 2026
bed188e
ci(DEN-1505): commit bounded merge diagnostics from a disposable clone
ORESoftware Aug 3, 2026
313f454
merge(DEN-1505/DEN-1585): compose recursive installer stack with curr…
github-actions[bot] Aug 3, 2026
5685a58
docs(DEN-1505/DEN-1585): document strict frozen and Nix composition
ORESoftware Aug 3, 2026
24cc131
ci(DEN-1505): rename blocking-lock suite as permanent contract
ORESoftware Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/blocking-store-process-locks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: blocking store process locks

on:
pull_request:
paths:
- src/store.rs
- src/install_graph.rs
- src/install_graph/**
- Cargo.toml
- Cargo.lock
- .github/workflows/blocking-store-process-locks.yml
push:
branches: [main]
paths:
- src/store.rs
- src/install_graph.rs
- src/install_graph/**
- Cargo.toml
- Cargo.lock
- .github/workflows/blocking-store-process-locks.yml
workflow_dispatch:

permissions:
contents: read

concurrency:
group: blocking-store-process-locks-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: "1"

jobs:
linux-kernel-lock:
name: Linux process-lock and recursive-cache contracts
runs-on: ubuntu-24.04
timeout-minutes: 35
steps:
- name: Check out zed-cli
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
show-progress: false

- name: Install stable Rust with formatting and lint support
run: |
rustup toolchain install stable --profile minimal --component rustfmt,clippy
rustup default stable
rustc --version --verbose
cargo --version --verbose

- name: Check formatting
run: cargo fmt --all --check

- name: Prove a separate process blocks until orderly release
run: cargo test --locked --lib store::tests::install_process_lock_blocks_without_polling_until_owner_release -- --exact --nocapture

- name: Prove owner termination releases the descriptor lock
run: cargo test --locked --lib store::tests::install_process_lock_is_released_when_owner_process_is_terminated -- --exact --nocapture

- name: Run all store process-lock tests
run: cargo test --locked --lib store::tests

- name: Run recursive acquisition and cache-race tests
run: cargo test --locked --lib install_graph::tests

- name: Deny Rust warnings
run: cargo clippy --locked --all-targets -- -D warnings
87 changes: 87 additions & 0 deletions .github/workflows/recursive-install-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: recursive install windows locking

on:
pull_request:
paths:
- src/install_graph.rs
- src/install_graph/**
- src/store.rs
- Cargo.toml
- Cargo.lock
- .github/workflows/recursive-install-windows.yml
push:
branches: [main]
paths:
- src/install_graph.rs
- src/install_graph/**
- src/store.rs
- Cargo.toml
- Cargo.lock
- .github/workflows/recursive-install-windows.yml
workflow_dispatch:

permissions:
contents: read

concurrency:
group: recursive-install-windows-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: "1"

jobs:
windows-kernel-lock:
name: Windows process lock and shared-home races
runs-on: windows-latest
timeout-minutes: 35
defaults:
run:
shell: pwsh
steps:
- name: Check out zed-cli
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: zed-cli
fetch-depth: 1
persist-credentials: false
show-progress: false

- name: Check out pinned zed-interfaces sibling
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: zed-pkg/zed-interfaces
ref: dc0e0a0620b9462817950b552d3d334a184b1cb1
path: zed-interfaces
fetch-depth: 1
persist-credentials: false
show-progress: false

- name: Install stable Rust with formatting support
run: |
rustup toolchain install stable --profile minimal --component rustfmt
rustup default stable
rustc --version --verbose
cargo --version --verbose

- name: Check formatting
run: cargo fmt --all --check --manifest-path zed-cli/Cargo.toml

- name: Prove a separate process blocks on the same artifact lock
run: cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::tests::artifact_waiters_block_until_the_owner_releases_the_kernel_lock -- --exact --nocapture

- name: Prove concurrent prefetches deduplicate a shared artifact
run: cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::tests::concurrent_prefetches_share_one_artifact_download -- --exact --nocapture

- name: Prove recursive and transactional paths share one lock
run: cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::tests::legacy_and_recursive_acquisition_paths_share_one_download_lock -- --exact --nocapture

- name: Prove failed downloads leave no cache publication
run: cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::tests::failed_downloads_remove_staging_files_and_never_publish_cache_entries -- --exact --nocapture

- name: Prove the lower-level store lock blocks across processes
run: cargo test --locked --manifest-path zed-cli/Cargo.toml --lib store::tests::install_process_lock_blocks_without_polling_until_owner_release -- --exact --nocapture

- name: Prove owner termination releases the lower-level store lock
run: cargo test --locked --manifest-path zed-cli/Cargo.toml --lib store::tests::install_process_lock_is_released_when_owner_process_is_terminated -- --exact --nocapture
89 changes: 89 additions & 0 deletions docs/recursive-installs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Recursive and concurrent installs

`zed install` resolves dependencies from every installed package manifest, not
only the consumer's direct dependency table. The resolver keeps one selected
version per `org/name`, detects incompatible requirements, terminates cycles,
and deduplicates diamond-shaped graphs before the project tree is changed.

## Concurrency

Artifact download, integrity verification, and extraction run through a bounded
worker queue. The default is five concurrent artifact installs:

```text
ZED_PKG_INSTALL_CONCURRENCY=5 zed install
```

`ZED_PKG_INSTALL_CONCURRENCY` may be set from 1 through 32. Invalid values and
zero use the default. Registry metadata resolution stays deterministic on the
coordinator thread; only independent artifact work is concurrent.

## Cross-process locking

Each content hash owns a lock file under:

```text
$ZED_PKG_HOME/locks/artifact-<sha256>.lock
```

The lock uses the operating system's blocking file-lock primitive. A waiter
sleeps in the kernel until the owner exits or releases the descriptor; there is
no retry timer or filesystem polling loop. The lock covers download, SHA-256
verification, atomic publication into the download cache, and extraction into
the content-addressed store. Two terminals or CI jobs sharing a Zed home can
therefore install unrelated artifacts concurrently while downloading any one
artifact at most once.

Recursive prefetch and the transactional installer use the same artifact
acquisition function. Frozen installs, build preparation, `zed add`, and
`zed remove` therefore cannot bypass the per-hash lock through an older direct
cache-download path.

Downloads are written to a temporary file in the cache directory, verified,
and atomically renamed. A corrupt cache file left by an interrupted older
client is removed and fetched again while the artifact lock is held.

## Materialization

The concurrent phase only warms the global store. The existing project
transaction remains responsible for lockfile writes, adapter wiring, rollback,
and dependency materialization. Unix installations therefore continue to use
store-backed symlinks by default; explicit copy mode and the non-Unix fallback
remain self-contained copies.

## Frozen and workspace compatibility

Recursive acquisition is composed with the strict frozen-lock boundary. A
frozen install may verify artifacts, repair deleted links, reconstruct workspace
projections, and refresh generated adapter wiring, but it preserves the caller's
committed lockfile bytes exactly. It does not rewrite comments, provenance, or
fields introduced by a newer CLI.

Workspace packages still enter the ordinary adapter and transaction lifecycle
through the `ops_entry` dispatcher. This keeps direct and transitive workspace
links, copy mode, uninstall cleanup, and post-uninstall frozen restoration on
the same fail-closed path as registry packages.

The recursive install modules coexist with the resolver-only frozen fetch path,
Nix fixed-output bundle generation, canonical export planning, and standalone
pure flake rendering. Those export surfaces consume the same validated lock and
content identities; they do not bypass recursive integrity checks or per-hash
locking.

## Validation

The unit suite races the recursive and transactional acquisition paths against
the same absent content hash. The first download is held open while the second
path attempts acquisition; the test proves that the second path never enters
its registry download and that the aggregate download count remains one.

A separate parent/child-process regression holds an artifact lock in the parent,
proves that the child remains blocked after reaching acquisition, releases the
owner descriptor, and then requires the child to wake and acquire successfully.
The same contract runs on Windows together with the shared-home deduplication,
mixed recursive/transactional acquisition, and failed-download cleanup tests.

The companion end-to-end suite also runs multiple independent CLI processes
against one shared Zed home, verifies the complete transitive lock graph, and
checks that project packages are store-backed symlinks rather than copied
package trees.
70 changes: 69 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ struct ManifestOverride {

thread_local! {
static MANIFEST_OVERRIDE: RefCell<Option<ManifestOverride>> = const { RefCell::new(None) };
static INSTALL_PREFETCH_CONFIG: RefCell<Option<Config>> = const { RefCell::new(None) };
}

struct ManifestOverrideGuard;
Expand All @@ -168,10 +169,44 @@ impl Drop for ManifestOverrideGuard {
}
}

struct InstallPrefetchGuard;

impl Drop for InstallPrefetchGuard {
fn drop(&mut self) {
INSTALL_PREFETCH_CONFIG.with(|slot| {
slot.borrow_mut().take();
});
}
}

fn normalized_project(project: &Path) -> PathBuf {
fs::canonicalize(project).unwrap_or_else(|_| project.to_path_buf())
}

/// Mark one facade operation so an in-memory manifest proposed by `zed add`
/// or `zed remove` is recursively prefetched before the implementation-local
/// reinstall begins. The context is thread-local and panic-safe; ordinary
/// manifest overrides remain side-effect free.
pub(crate) fn with_install_prefetch<T>(
cfg: &Config,
operation: impl FnOnce() -> Result<T>,
) -> Result<T> {
INSTALL_PREFETCH_CONFIG.with(|slot| {
let mut slot = slot.borrow_mut();
if slot.is_some() {
return Err(anyhow!(
"an install prefetch context is already active on this thread"
));
}
*slot = Some(cfg.clone());
Ok(())
})?;
let guard = InstallPrefetchGuard;
let result = operation();
drop(guard);
result
}

/// Run one installer operation with an in-memory root manifest. Package
/// manifests read from the store still come from disk; only the exact consumer
/// directory is overridden. The guard is thread-local and panic-safe, so a
Expand All @@ -189,10 +224,17 @@ pub(crate) fn with_manifest_override<T>(
"a manifest override is already active on this thread"
));
}
*slot = Some(ManifestOverride { project, text });
*slot = Some(ManifestOverride {
project: project.clone(),
text,
});
Ok(())
})?;
let guard = ManifestOverrideGuard;
let prefetch_cfg = INSTALL_PREFETCH_CONFIG.with(|slot| slot.borrow().clone());
if let Some(cfg) = prefetch_cfg {
crate::install_graph::prefetch(&project, &cfg, false)?;
}
let result = operation();
drop(guard);
result
Expand Down Expand Up @@ -282,6 +324,32 @@ url = "https://localhost/manifestless/consumer"
assert!(error.to_string().contains("already active"));
}

#[test]
fn install_prefetch_context_is_scoped_and_nested_contexts_fail_closed() {
let home = tempfile::tempdir().unwrap();
let cfg = Config {
registry: "file:///unused".to_string(),
home: home.path().to_path_buf(),
token: None,
auth_url: "https://localhost/unused".to_string(),
supabase_url: None,
supabase_key: None,
interactive: false,
};

with_install_prefetch(&cfg, || {
let active = INSTALL_PREFETCH_CONFIG.with(|slot| slot.borrow().is_some());
assert!(active);
let nested = with_install_prefetch(&cfg, || Ok(())).unwrap_err();
assert!(nested.to_string().contains("already active"));
Ok(())
})
.unwrap();

let active = INSTALL_PREFETCH_CONFIG.with(|slot| slot.borrow().is_some());
assert!(!active);
}

#[test]
fn relative_home_is_resolved_from_the_invocation_directory() {
let globals = Globals {
Expand Down
Loading
Loading