From 2effdf2ce3eca8afbd3b340f4b55e63499802d6b Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:43:20 -0500 Subject: [PATCH 01/80] feat: add recursive install graph scaffold --- src/install_graph.rs | 298 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 src/install_graph.rs diff --git a/src/install_graph.rs b/src/install_graph.rs new file mode 100644 index 00000000..97f3a46f --- /dev/null +++ b/src/install_graph.rs @@ -0,0 +1,298 @@ +//! Recursive, bounded-concurrency artifact prefetch for `zed install`. +//! +//! The normal installer remains the authority for resolution, lockfile writes, +//! project transactions, adapters, and symlink/copy materialization. This module +//! runs immediately before it: it walks package manifests recursively, installs +//! artifacts into the content-addressed store with a five-worker queue, and lets +//! the normal install phase reuse the warm store. +//! +//! Cross-process coordination is deliberately per artifact. Waiters block in +//! the operating system's file-lock primitive instead of polling, so unrelated +//! dependencies and unrelated projects can keep making progress. + +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard, mpsc}; +use std::thread::{self, JoinHandle}; + +use anyhow::{Context, Result, bail}; +use fs2::FileExt; +use globset::Glob; +use zed_interfaces::lockfile::Lockfile; +use zed_interfaces::manifest::{Manifest, is_slug}; +use zed_interfaces::paths::{LOCKFILE_FILE, MANIFEST_FILE}; +use zed_interfaces::registry::VersionMetadata; +use zed_interfaces::version::{self, Requirement}; + +use crate::config::{Config, read_manifest}; +use crate::pack::sha256_file; +use crate::registry::{Registry, registry_for}; +use crate::store::{Store, require_sha256}; + +/// Default number of artifact installs allowed to make progress at once. +pub const DEFAULT_INSTALL_CONCURRENCY: usize = 5; +const MAX_INSTALL_CONCURRENCY: usize = 32; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct PrefetchReport { + /// Unique registry packages resolved (workspace links are not counted). + pub resolved: usize, + /// Artifacts that were absent locally and downloaded by this process. + pub downloaded: usize, +} + +#[derive(Debug, Clone)] +struct DependencyRequest { + key: String, + requirement: String, +} + +#[derive(Debug, Clone)] +struct FetchTask { + key: String, + version: VersionMetadata, +} + +#[derive(Debug)] +struct FetchResult { + dependencies: BTreeMap, + downloaded: bool, +} + +#[derive(Debug, Default)] +struct QueueState { + tasks: VecDeque, + closed: bool, +} + +#[derive(Debug, Default)] +struct TaskQueue { + state: Mutex, + ready: Condvar, +} + +impl TaskQueue { + fn state(&self) -> MutexGuard<'_, QueueState> { + self.state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + } + + fn push(&self, task: FetchTask) -> bool { + let mut state = self.state(); + if state.closed { + return false; + } + state.tasks.push_back(task); + self.ready.notify_one(); + true + } + + fn pop(&self) -> Option { + let mut state = self.state(); + loop { + if let Some(task) = state.tasks.pop_front() { + return Some(task); + } + if state.closed { + return None; + } + state = self + .ready + .wait(state) + .unwrap_or_else(|poisoned| poisoned.into_inner()); + } + } + + fn close(&self) { + let mut state = self.state(); + state.closed = true; + self.ready.notify_all(); + } + + fn cancel(&self) { + let mut state = self.state(); + state.tasks.clear(); + state.closed = true; + self.ready.notify_all(); + } +} + +struct FetchPool { + queue: Arc, + results: mpsc::Receiver>, + workers: Vec>, +} + +impl FetchPool { + fn new(concurrency: usize, registry_url: &str, home: &Path) -> Result { + let queue = Arc::new(TaskQueue::default()); + let (result_tx, results) = mpsc::channel(); + let mut workers = Vec::with_capacity(concurrency); + + for index in 0..concurrency { + let worker_queue = Arc::clone(&queue); + let worker_tx = result_tx.clone(); + let worker_registry = registry_url.to_string(); + let worker_home = home.to_path_buf(); + let spawn = thread::Builder::new() + .name(format!("zed-install-{index}")) + .spawn(move || { + worker_loop( + worker_queue, + worker_tx, + worker_registry, + worker_home, + ) + }); + match spawn { + Ok(worker) => workers.push(worker), + Err(error) => { + queue.cancel(); + for worker in workers { + let _ = worker.join(); + } + return Err(error).context("starting recursive install worker"); + } + } + } + drop(result_tx); + Ok(Self { + queue, + results, + workers, + }) + } + + fn submit(&self, task: FetchTask) -> Result<()> { + if !self.queue.push(task) { + bail!("recursive install worker queue closed unexpectedly"); + } + Ok(()) + } + + fn receive(&self) -> Result { + self.results + .recv() + .context("all recursive install workers stopped unexpectedly")? + } + + fn shutdown(self, cancel: bool) -> Result<()> { + if cancel { + self.queue.cancel(); + } else { + self.queue.close(); + } + let mut panicked = false; + for worker in self.workers { + panicked |= worker.join().is_err(); + } + if panicked { + bail!("a recursive install worker panicked"); + } + Ok(()) + } +} + +/// A process lock for one content hash. `lock_exclusive` blocks in the kernel; +/// it does not wake up periodically to retry. The file descriptor owns the +/// lock, so process exit or panic releases it automatically. +struct ArtifactProcessLock { + file: fs::File, +} + +impl ArtifactProcessLock { + fn acquire(home: &Path, sha256: &str) -> Result { + require_sha256(sha256)?; + let path = home + .join("locks") + .join(format!("artifact-{sha256}.lock")); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let file = fs::OpenOptions::new() + .create(true) + .read(true) + .write(true) + .truncate(false) + .open(&path) + .with_context(|| format!("opening artifact lock {}", path.display()))?; + FileExt::lock_exclusive(&file) + .with_context(|| format!("waiting for artifact lock {}", path.display()))?; + Ok(Self { file }) + } +} + +impl Drop for ArtifactProcessLock { + fn drop(&mut self) { + let _ = FileExt::unlock(&self.file); + } +} + +#[derive(Debug, Default)] +struct WorkspaceGraph { + dependencies: BTreeMap>, +} + +impl WorkspaceGraph { + fn discover(project: &Path) -> Self { + let mut current = Some(project); + while let Some(directory) = current { + if directory.join(MANIFEST_FILE).is_file() + && let Ok(manifest) = read_manifest(directory) + && let Some(workspace) = manifest.workspace.as_ref() + { + return Self::collect(directory, &workspace.members); + } + current = directory.parent(); + } + Self::default() + } + + fn collect(root: &Path, patterns: &[String]) -> Self { + let mut graph = Self::default(); + for pattern in patterns { + let mut candidates = vec![root.to_path_buf()]; + for segment in pattern.split('/') { + let mut next = Vec::new(); + for base in &candidates { + if segment.contains('*') { + let Ok(glob) = Glob::new(segment) else { + continue; + }; + let matcher = glob.compile_matcher(); + if let Ok(entries) = fs::read_dir(base) { + for entry in entries.flatten() { + let name = entry.file_name(); + if entry.path().is_dir() + && matcher.is_match(Path::new(&name)) + && !name.to_string_lossy().starts_with('.') + { + next.push(entry.path()); + } + } + } + } else { + let candidate = base.join(segment); + if candidate.is_dir() { + next.push(candidate); + } + } + } + candidates = next; + } + for member_dir in candidates { + if let Ok(member) = read_manifest(&member_dir) { + let key = member.full_name(); + graph.dependencies.insert(key, member.dependencies); + } + } + } + graph + } +} + +include!("install_graph/resolver.rs"); +include!("install_graph/artifact.rs"); +include!("install_graph/tests.rs"); From 727882a1c10d5295b42cf3eeddd9be66e29a6551 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:43:53 -0500 Subject: [PATCH 02/80] feat: resolve recursive dependency graphs concurrently --- src/install_graph/resolver.rs | 197 ++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 src/install_graph/resolver.rs diff --git a/src/install_graph/resolver.rs b/src/install_graph/resolver.rs new file mode 100644 index 00000000..b507944a --- /dev/null +++ b/src/install_graph/resolver.rs @@ -0,0 +1,197 @@ +/// Warm the content-addressed store for an install. The normal installer runs +/// immediately afterward and remains responsible for the project transaction. +pub fn prefetch(project: &Path, cfg: &Config, frozen: bool) -> Result { + let concurrency = install_concurrency(); + let registry = registry_for(&cfg.registry)?; + let report = if frozen { + prefetch_locked(project, cfg, registry.as_ref(), concurrency)? + } else { + let manifest = read_manifest(project)?; + prefetch_recursive(project, cfg, registry.as_ref(), &manifest, concurrency)? + }; + + if report.resolved > 0 { + eprintln!( + "recursive install prefetch: {} package(s), up to {} concurrent, {} downloaded", + report.resolved, concurrency, report.downloaded + ); + } + Ok(report) +} + +fn install_concurrency() -> usize { + normalize_concurrency( + std::env::var("ZED_PKG_INSTALL_CONCURRENCY") + .ok() + .as_deref(), + ) +} + +fn normalize_concurrency(raw: Option<&str>) -> usize { + raw.and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_INSTALL_CONCURRENCY) + .min(MAX_INSTALL_CONCURRENCY) +} + +fn run_with_pool( + cfg: &Config, + concurrency: usize, + operation: impl FnOnce(&FetchPool) -> Result, +) -> Result { + let pool = FetchPool::new(concurrency, &cfg.registry, &cfg.home)?; + match operation(&pool) { + Ok(value) => { + pool.shutdown(false)?; + Ok(value) + } + Err(error) => { + let _ = pool.shutdown(true); + Err(error) + } + } +} + +fn prefetch_locked( + project: &Path, + cfg: &Config, + registry: &dyn Registry, + concurrency: usize, +) -> Result { + let lock_path = project.join(LOCKFILE_FILE); + let text = fs::read_to_string(&lock_path) + .with_context(|| format!("--frozen requires {}", lock_path.display()))?; + let lock = Lockfile::parse(&text)?; + let mut tasks = Vec::with_capacity(lock.packages.len()); + let mut seen = BTreeSet::new(); + + for locked in &lock.packages { + if !is_slug(&locked.org) || !is_slug(&locked.name) { + bail!( + "lockfile entry `{}/{}` has an invalid identity; refusing", + locked.org, + locked.name + ); + } + require_sha256(&locked.sha256)?; + let key = locked.full_name(); + if !seen.insert(key.clone()) { + bail!("duplicate package `{key}` in {LOCKFILE_FILE}"); + } + let version = registry.get_version(&locked.org, &locked.name, &locked.version)?; + validate_version_identity(&version, &locked.org, &locked.name, &locked.version)?; + if version.sha256 != locked.sha256 { + bail!( + "registry artifact for {}@{} changed (lock {} vs registry {}); refusing", + key, + locked.version, + locked.sha256, + version.sha256 + ); + } + tasks.push(FetchTask { key, version }); + } + + if tasks.is_empty() { + return Ok(PrefetchReport::default()); + } + + run_with_pool(cfg, concurrency, |pool| { + let total = tasks.len(); + for task in tasks { + pool.submit(task)?; + } + let mut downloaded = 0usize; + for _ in 0..total { + let result = pool.receive()?; + downloaded += usize::from(result.downloaded); + } + Ok(PrefetchReport { + resolved: total, + downloaded, + }) + }) +} + +fn prefetch_recursive( + project: &Path, + cfg: &Config, + registry: &dyn Registry, + manifest: &Manifest, + concurrency: usize, +) -> Result { + if manifest.dependencies.is_empty() { + return Ok(PrefetchReport::default()); + } + let workspace = WorkspaceGraph::discover(project); + + run_with_pool(cfg, concurrency, |pool| { + let mut pending: VecDeque = manifest + .dependencies + .iter() + .map(|(key, requirement)| DependencyRequest { + key: key.clone(), + requirement: requirement.clone(), + }) + .collect(); + let mut resolved: BTreeMap = BTreeMap::new(); + let mut expanded_workspace = BTreeSet::new(); + let mut in_flight = 0usize; + let mut downloaded = 0usize; + + loop { + while let Some(request) = pending.pop_front() { + if let Some(member_dependencies) = workspace.dependencies.get(&request.key) { + if expanded_workspace.insert(request.key.clone()) { + pending.extend(member_dependencies.iter().map( + |(key, requirement)| DependencyRequest { + key: key.clone(), + requirement: requirement.clone(), + }, + )); + } + continue; + } + + let (org, name) = split_key(&request.key)?; + let requirement = Requirement::parse(&request.requirement); + if let Some(existing) = resolved.get(&request.key) { + if requirement.matches(&existing.version) { + continue; + } + bail!( + "version conflict for {}: resolved {} but another dependency requires `{}` (zed installs one version per package)", + request.key, + existing.version, + request.requirement + ); + } + + let version = resolve_version(registry, &org, &name, &request.requirement)?; + resolved.insert(request.key.clone(), version.clone()); + pool.submit(FetchTask { + key: request.key, + version, + })?; + in_flight += 1; + } + + if in_flight == 0 { + break; + } + + let result = pool.receive()?; + in_flight -= 1; + downloaded += usize::from(result.downloaded); + pending.extend(result.dependencies.into_iter().map( + |(key, requirement)| DependencyRequest { key, requirement }, + )); + } + + Ok(PrefetchReport { + resolved: resolved.len(), + downloaded, + }) + }) +} + From fdcf9ae3baa10cf3e061f919634432880b37c354 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:44:26 -0500 Subject: [PATCH 03/80] feat: add atomic artifact fetches with process locks --- src/install_graph/artifact.rs | 209 ++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/install_graph/artifact.rs diff --git a/src/install_graph/artifact.rs b/src/install_graph/artifact.rs new file mode 100644 index 00000000..b41810ea --- /dev/null +++ b/src/install_graph/artifact.rs @@ -0,0 +1,209 @@ +fn worker_loop( + queue: Arc, + results: mpsc::Sender>, + registry_url: String, + home: PathBuf, +) { + let store = Store::new(&home); + let mut registry: Option> = None; + + while let Some(task) = queue.pop() { + let result = (|| -> Result { + if registry.is_none() { + registry = Some(registry_for(®istry_url)?); + } + let registry = registry + .as_deref() + .context("recursive install worker has no registry")?; + prefetch_one(registry, &store, &home, task) + })(); + if results.send(result).is_err() { + return; + } + } +} + +fn prefetch_one( + registry: &dyn Registry, + store: &Store, + home: &Path, + task: FetchTask, +) -> Result { + let (package_dir, downloaded) = ensure_artifact(registry, store, home, &task.version) + .with_context(|| { + format!( + "prefetching {}@{}", + task.key, task.version.version + ) + })?; + let dependencies = read_manifest(&package_dir) + .map(|manifest| manifest.dependencies) + .unwrap_or_default(); + Ok(FetchResult { + dependencies, + downloaded, + }) +} + +fn ensure_artifact( + registry: &dyn Registry, + store: &Store, + home: &Path, + version: &VersionMetadata, +) -> Result<(PathBuf, bool)> { + validate_version_identity(version, &version.org, &version.name, &version.version)?; + if store.has(&version.sha256) { + return Ok((store.pkg_dir(&version.sha256), false)); + } + + let _artifact_lock = ArtifactProcessLock::acquire(home, &version.sha256)?; + if store.has(&version.sha256) { + return Ok((store.pkg_dir(&version.sha256), false)); + } + + let cached = store.cached_artifact(&version.sha256); + let mut downloaded = false; + if !cached.is_file() { + download_atomic(registry, version, &cached)?; + downloaded = true; + } + + match store.add_artifact(&cached, &version.sha256) { + Ok(package_dir) => Ok((package_dir, downloaded)), + Err(first_error) if !downloaded => { + // A killed legacy client may have left a partial cache file. The + // per-artifact lock makes removal and replacement safe. + let _ = fs::remove_file(&cached); + download_atomic(registry, version, &cached)?; + store + .add_artifact(&cached, &version.sha256) + .with_context(|| { + format!( + "cached artifact was invalid ({first_error:#}); redownload also failed" + ) + }) + .map(|package_dir| (package_dir, true)) + } + Err(error) => Err(error), + } +} + +fn download_atomic( + registry: &dyn Registry, + version: &VersionMetadata, + cached: &Path, +) -> Result<()> { + let cache_dir = cached.parent().context("cached artifact has a parent")?; + fs::create_dir_all(cache_dir)?; + let staging_dir = tempfile::tempdir_in(cache_dir)?; + let staged = staging_dir.path().join("artifact.download"); + registry.download(version, &staged)?; + + let (actual_sha256, _) = sha256_file(&staged)?; + if actual_sha256 != version.sha256 { + bail!( + "artifact hash mismatch: expected {}, got {}", + version.sha256, + actual_sha256 + ); + } + + if cached.exists() { + let (cached_sha256, _) = sha256_file(cached)?; + if cached_sha256 == version.sha256 { + return Ok(()); + } + fs::remove_file(cached)?; + } + match fs::rename(&staged, cached) { + Ok(()) => Ok(()), + Err(error) if cached.is_file() => { + let (cached_sha256, _) = sha256_file(cached)?; + if cached_sha256 == version.sha256 { + Ok(()) + } else { + Err(error).with_context(|| { + format!("publishing downloaded artifact to {}", cached.display()) + }) + } + } + Err(error) => Err(error) + .with_context(|| format!("publishing downloaded artifact to {}", cached.display())), + } +} + +fn split_key(key: &str) -> Result<(String, String)> { + let mut parts = key.splitn(2, '/'); + match (parts.next(), parts.next()) { + (Some(org), Some(name)) if !org.is_empty() && !name.is_empty() => { + Ok((org.to_string(), name.to_string())) + } + _ => bail!("invalid package spec `{key}` (expected org/name)"), + } +} + +fn validate_version_identity( + version: &VersionMetadata, + expected_org: &str, + expected_name: &str, + expected_version: &str, +) -> Result<()> { + if !is_slug(&version.org) || !is_slug(&version.name) { + bail!( + "registry returned invalid package identity `{}/{}`; refusing", + version.org, + version.name + ); + } + require_sha256(&version.sha256)?; + if version.org != expected_org + || version.name != expected_name + || version.version != expected_version + { + bail!( + "registry returned `{}/{}@{}` while resolving `{expected_org}/{expected_name}@{expected_version}`; refusing", + version.org, + version.name, + version.version + ); + } + Ok(()) +} + +fn resolve_version( + registry: &dyn Registry, + org: &str, + name: &str, + requirement_text: &str, +) -> Result { + let key = format!("{org}/{name}"); + let requirement = Requirement::parse(requirement_text); + let package = registry.get_package(org, name)?; + let mut candidates = package.versions.clone(); + let mut skipped_yanked = Vec::new(); + + loop { + let selected = version::resolve(&requirement, &candidates).map(str::to_string); + let Some(selected) = selected else { + if !skipped_yanked.is_empty() { + bail!( + "the only version(s) of {key} satisfying `{requirement_text}` are yanked ({}); existing lockfiles keep working via `zed install --frozen`", + skipped_yanked.join(", ") + ); + } + bail!( + "no version of {key} satisfies `{requirement_text}` (available: {})", + package.versions.join(", ") + ); + }; + let version = registry.get_version(org, name, &selected)?; + validate_version_identity(&version, org, name, &selected)?; + if version.yanked { + candidates.retain(|candidate| *candidate != selected); + skipped_yanked.push(selected); + continue; + } + return Ok(version); + } +} + From febf85624af53c62c790a3630c203b3d56403e80 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:45:14 -0500 Subject: [PATCH 04/80] test: cover recursive installs and process locking --- src/install_graph/tests.rs | 258 +++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 src/install_graph/tests.rs diff --git a/src/install_graph/tests.rs b/src/install_graph/tests.rs new file mode 100644 index 00000000..36f54c6d --- /dev/null +++ b/src/install_graph/tests.rs @@ -0,0 +1,258 @@ +#[cfg(test)] +mod tests { + use std::sync::mpsc as test_mpsc; + use std::time::Duration; + + use zed_interfaces::registry::{PackageMetadata, VersionMetadata}; + use zed_interfaces::vcs::Vcs; + + use super::*; + use crate::pack::pack; + + fn test_config(registry: &Path, home: &Path) -> Config { + Config { + registry: format!("file://{}", registry.display()), + home: home.to_path_buf(), + token: None, + auth_url: "http://127.0.0.1/unused".to_string(), + supabase_url: None, + supabase_key: None, + interactive: false, + } + } + + fn manifest_text( + org: &str, + name: &str, + version: &str, + dependencies: &[(&str, &str)], + ) -> String { + let mut text = format!( + r#"[package] +org = "{org}" +name = "{name}" +version = "{version}" + +[package.repository] +vcs = "git" +url = "https://example.invalid/{org}/{name}" +"#, + ); + if !dependencies.is_empty() { + text.push_str("\n[dependencies]\n"); + for (key, requirement) in dependencies { + text.push_str(&format!("\"{key}\" = \"{requirement}\"\n")); + } + } + text + } + + fn publish_fixture( + registry_root: &Path, + scratch: &Path, + org: &str, + name: &str, + version: &str, + dependencies: &[(&str, &str)], + ) -> String { + let source = scratch.join(format!("source-{name}")); + fs::create_dir_all(&source).unwrap(); + let manifest_text = manifest_text(org, name, version, dependencies); + fs::write(source.join(MANIFEST_FILE), &manifest_text).unwrap(); + fs::write(source.join("payload.txt"), format!("{org}/{name}@{version}\n")).unwrap(); + let manifest = Manifest::parse(&manifest_text).unwrap(); + let packed = pack( + &source, + &manifest, + Some(&scratch.join(format!("packed-{name}"))), + ) + .unwrap(); + + let artifacts = registry_root.join("artifacts"); + fs::create_dir_all(&artifacts).unwrap(); + fs::copy( + &packed.path, + artifacts.join(format!("{}.tar.gz", packed.sha256)), + ) + .unwrap(); + + let package_dir = registry_root.join("packages").join(org).join(name); + fs::create_dir_all(package_dir.join("versions")).unwrap(); + let version_metadata = VersionMetadata { + org: org.to_string(), + name: name.to_string(), + version: version.to_string(), + sha256: packed.sha256.clone(), + size: packed.size, + format: packed.format.clone(), + vcs_tag: format!("v{version}"), + vcs_commit: Some("0123456789abcdef0123456789abcdef01234567".to_string()), + download_url: format!("file://{}/{}.tar.gz", artifacts.display(), packed.sha256), + published_at: "1970-01-01T00:00:00Z".to_string(), + yanked: false, + }; + fs::write( + package_dir.join("versions").join(format!("{version}.json")), + serde_json::to_string_pretty(&version_metadata).unwrap(), + ) + .unwrap(); + let package_metadata = PackageMetadata { + org: org.to_string(), + name: name.to_string(), + description: Some(format!("fixture {name}")), + vcs: Vcs::Git, + repo_url: format!("https://example.invalid/{org}/{name}"), + version_scheme: manifest.package.version_scheme, + latest: Some(version.to_string()), + tags: Vec::new(), + versions: vec![version.to_string()], + }; + fs::write( + package_dir.join("package.json"), + serde_json::to_string_pretty(&package_metadata).unwrap(), + ) + .unwrap(); + packed.sha256 + } + + #[test] + fn defaults_to_five_workers_and_bounds_overrides() { + assert_eq!(normalize_concurrency(None), 5); + assert_eq!(normalize_concurrency(Some("not-a-number")), 5); + assert_eq!(normalize_concurrency(Some("0")), 5); + assert_eq!(normalize_concurrency(Some("1")), 1); + assert_eq!(normalize_concurrency(Some("999")), MAX_INSTALL_CONCURRENCY); + } + + #[test] + fn recursively_prefetches_packages_of_packages_and_deduplicates_diamonds() { + let temp = tempfile::tempdir().unwrap(); + let registry = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); + + let leaf = publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); + let left = publish_fixture( + ®istry, + &scratch, + "test", + "left", + "1.0.0", + &[("test/leaf", "^1")], + ); + let right = publish_fixture( + ®istry, + &scratch, + "test", + "right", + "1.0.0", + &[("test/leaf", "^1")], + ); + let root = publish_fixture( + ®istry, + &scratch, + "test", + "root", + "1.0.0", + &[("test/left", "^1"), ("test/right", "^1")], + ); + fs::write( + project.join(MANIFEST_FILE), + manifest_text("consumer", "app", "0.1.0", &[("test/root", "^1")]), + ) + .unwrap(); + + let report = prefetch(&project, &test_config(®istry, &home), false).unwrap(); + assert_eq!(report.resolved, 4); + assert_eq!(report.downloaded, 4); + let store = Store::new(&home); + for sha in [leaf, left, right, root] { + assert!(store.has(&sha), "missing prefetched artifact {sha}"); + } + } + + #[test] + fn dependency_cycles_terminate_after_each_package_is_resolved_once() { + let temp = tempfile::tempdir().unwrap(); + let registry = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); + + publish_fixture( + ®istry, + &scratch, + "test", + "a", + "1.0.0", + &[("test/b", "^1")], + ); + publish_fixture( + ®istry, + &scratch, + "test", + "b", + "1.0.0", + &[("test/a", "^1")], + ); + fs::write( + project.join(MANIFEST_FILE), + manifest_text("consumer", "app", "0.1.0", &[("test/a", "^1")]), + ) + .unwrap(); + + let report = prefetch(&project, &test_config(®istry, &home), false).unwrap(); + assert_eq!(report.resolved, 2); + assert_eq!(report.downloaded, 2); + } + + #[test] + fn a_corrupt_partial_cache_is_replaced_under_the_artifact_lock() { + let temp = tempfile::tempdir().unwrap(); + let registry = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); + + let sha = publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); + fs::write( + project.join(MANIFEST_FILE), + manifest_text("consumer", "app", "0.1.0", &[("test/leaf", "^1")]), + ) + .unwrap(); + let store = Store::new(&home); + let cached = store.cached_artifact(&sha); + fs::create_dir_all(cached.parent().unwrap()).unwrap(); + fs::write(&cached, b"partial download").unwrap(); + + let report = prefetch(&project, &test_config(®istry, &home), false).unwrap(); + assert_eq!(report.resolved, 1); + assert_eq!(report.downloaded, 1); + assert!(store.has(&sha)); + assert_eq!(sha256_file(&cached).unwrap().0, sha); + } + + #[cfg(unix)] + #[test] + fn artifact_waiters_block_until_the_owner_releases_the_kernel_lock() { + let temp = tempfile::tempdir().unwrap(); + let sha = "a".repeat(64); + let owner = ArtifactProcessLock::acquire(temp.path(), &sha).unwrap(); + let second_home = temp.path().to_path_buf(); + let second_sha = sha.clone(); + let (acquired_tx, acquired_rx) = test_mpsc::channel(); + let waiter = thread::spawn(move || { + let _waiter = ArtifactProcessLock::acquire(&second_home, &second_sha).unwrap(); + acquired_tx.send(()).unwrap(); + }); + + assert!(acquired_rx.recv_timeout(Duration::from_millis(100)).is_err()); + drop(owner); + acquired_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + waiter.join().unwrap(); + } +} From e97365847a1406fbad7716a76faab578d3fc52ac Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:45:29 -0500 Subject: [PATCH 05/80] refactor: route installs through recursive prefetch --- src/ops_entry.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/ops_entry.rs diff --git a/src/ops_entry.rs b/src/ops_entry.rs new file mode 100644 index 00000000..6edb4871 --- /dev/null +++ b/src/ops_entry.rs @@ -0,0 +1,72 @@ +//! Public installer facade. +//! +//! The existing implementation continues to own dependency resolution, +//! project transactions, lockfile writes, adapter wiring, and materialization. +//! This facade warms the content-addressed store with the recursive, +//! bounded-concurrency installer before entering that transactional phase. + +use std::path::Path; + +use anyhow::Result; + +use crate::cli::{Adapter, InstallMode}; +use crate::config::Config; + +#[path = "ops.rs"] +mod implementation; + +pub use implementation::{ + InstallOutcome, WorkspaceInfo, add, build_cmd, build_publish_meta, cache_clean, find, gc, + init, login, org_audit, org_claim, pack_cmd, publish, remove, run, split_key, store_prune, + store_status, uninstall, yank, +}; + +pub(crate) use implementation::{ + detect_adapter, detect_native_manifest_target, detect_structure_target, detect_target, +}; + +#[allow(clippy::too_many_arguments)] +pub fn install( + project: &Path, + cfg: &Config, + frozen: bool, + mode: InstallMode, + adapter: Adapter, + allow_build: bool, + target: Option<&str>, + allow_ecosystem_mismatch: bool, +) -> Result { + crate::install_graph::prefetch(project, cfg, frozen)?; + implementation::install( + project, + cfg, + frozen, + mode, + adapter, + allow_build, + target, + allow_ecosystem_mismatch, + ) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn install_frozen_lock_only( + project: &Path, + cfg: &Config, + mode: InstallMode, + adapter: Adapter, + allow_build: bool, + target: Option<&str>, + allow_ecosystem_mismatch: bool, +) -> Result { + crate::install_graph::prefetch(project, cfg, true)?; + implementation::install_frozen_lock_only( + project, + cfg, + mode, + adapter, + allow_build, + target, + allow_ecosystem_mismatch, + ) +} From 52def3dd3352ac03eb94194a2ce5a6b7b1401071 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:45:43 -0500 Subject: [PATCH 06/80] refactor: expose recursive installer facade --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 49076839..a4a79627 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,10 @@ pub mod completion; pub mod config; pub mod dev; pub mod flags; +pub mod install_graph; pub mod interactive; pub mod manifestless; +#[path = "ops_entry.rs"] pub mod ops; pub mod pack; pub mod preflight; From 5fb1928edaae0ca3e5dbc0063c2bb69844b071a7 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:45:58 -0500 Subject: [PATCH 07/80] docs: explain recursive concurrent installs --- docs/recursive-installs.md | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/recursive-installs.md diff --git a/docs/recursive-installs.md b/docs/recursive-installs.md new file mode 100644 index 00000000..b320d867 --- /dev/null +++ b/docs/recursive-installs.md @@ -0,0 +1,47 @@ +# 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-.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. + +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. From 1cbf780ea7cc4305c7f52840fe2984157567a2d5 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:50:53 -0500 Subject: [PATCH 08/80] refactor: use normal recursive install submodules --- src/install_graph.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/install_graph.rs b/src/install_graph.rs index 97f3a46f..6c6f36db 100644 --- a/src/install_graph.rs +++ b/src/install_graph.rs @@ -293,6 +293,10 @@ impl WorkspaceGraph { } } -include!("install_graph/resolver.rs"); -include!("install_graph/artifact.rs"); -include!("install_graph/tests.rs"); +mod artifact; +mod resolver; +#[cfg(test)] +mod tests; + +use artifact::worker_loop; +pub use resolver::prefetch; From 4d3514c16fa6dbb6023a5f54876290e4d55e4900 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:51:36 -0500 Subject: [PATCH 09/80] refactor: isolate recursive graph resolution --- src/install_graph/resolver.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/install_graph/resolver.rs b/src/install_graph/resolver.rs index b507944a..48ab829f 100644 --- a/src/install_graph/resolver.rs +++ b/src/install_graph/resolver.rs @@ -1,3 +1,6 @@ +use super::*; +use super::artifact::{resolve_version, split_key, validate_version_identity}; + /// Warm the content-addressed store for an install. The normal installer runs /// immediately afterward and remains responsible for the project transaction. pub fn prefetch(project: &Path, cfg: &Config, frozen: bool) -> Result { @@ -27,7 +30,7 @@ fn install_concurrency() -> usize { ) } -fn normalize_concurrency(raw: Option<&str>) -> usize { +pub(super) fn normalize_concurrency(raw: Option<&str>) -> usize { raw.and_then(|value| value.parse::().ok()) .filter(|value| *value > 0) .unwrap_or(DEFAULT_INSTALL_CONCURRENCY) @@ -194,4 +197,3 @@ fn prefetch_recursive( }) }) } - From 9c5d24328484608fe18f8a8b51ad1fcda62fbfb9 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:52:12 -0500 Subject: [PATCH 10/80] refactor: isolate atomic artifact workers --- src/install_graph/artifact.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/install_graph/artifact.rs b/src/install_graph/artifact.rs index b41810ea..c6ea610d 100644 --- a/src/install_graph/artifact.rs +++ b/src/install_graph/artifact.rs @@ -1,4 +1,6 @@ -fn worker_loop( +use super::*; + +pub(super) fn worker_loop( queue: Arc, results: mpsc::Sender>, registry_url: String, @@ -132,7 +134,7 @@ fn download_atomic( } } -fn split_key(key: &str) -> Result<(String, String)> { +pub(super) fn split_key(key: &str) -> Result<(String, String)> { let mut parts = key.splitn(2, '/'); match (parts.next(), parts.next()) { (Some(org), Some(name)) if !org.is_empty() && !name.is_empty() => { @@ -142,7 +144,7 @@ fn split_key(key: &str) -> Result<(String, String)> { } } -fn validate_version_identity( +pub(super) fn validate_version_identity( version: &VersionMetadata, expected_org: &str, expected_name: &str, @@ -170,7 +172,7 @@ fn validate_version_identity( Ok(()) } -fn resolve_version( +pub(super) fn resolve_version( registry: &dyn Registry, org: &str, name: &str, @@ -206,4 +208,3 @@ fn resolve_version( return Ok(version); } } - From 564e15ed504a5974a2b61c02b1a0b6d61e016402 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:52:55 -0500 Subject: [PATCH 11/80] test: prove concurrent processes share one download --- src/install_graph/tests.rs | 493 ++++++++++++++++++++----------------- 1 file changed, 267 insertions(+), 226 deletions(-) diff --git a/src/install_graph/tests.rs b/src/install_graph/tests.rs index 36f54c6d..c9ecc6c0 100644 --- a/src/install_graph/tests.rs +++ b/src/install_graph/tests.rs @@ -1,34 +1,34 @@ -#[cfg(test)] -mod tests { - use std::sync::mpsc as test_mpsc; - use std::time::Duration; +use std::sync::{Arc, Barrier, mpsc as test_mpsc}; +use std::thread; +use std::time::Duration; - use zed_interfaces::registry::{PackageMetadata, VersionMetadata}; - use zed_interfaces::vcs::Vcs; +use zed_interfaces::registry::{PackageMetadata, VersionMetadata}; +use zed_interfaces::vcs::Vcs; - use super::*; - use crate::pack::pack; +use super::resolver::normalize_concurrency; +use super::*; +use crate::pack::pack; - fn test_config(registry: &Path, home: &Path) -> Config { - Config { - registry: format!("file://{}", registry.display()), - home: home.to_path_buf(), - token: None, - auth_url: "http://127.0.0.1/unused".to_string(), - supabase_url: None, - supabase_key: None, - interactive: false, - } +fn test_config(registry: &Path, home: &Path) -> Config { + Config { + registry: format!("file://{}", registry.display()), + home: home.to_path_buf(), + token: None, + auth_url: "http://127.0.0.1/unused".to_string(), + supabase_url: None, + supabase_key: None, + interactive: false, } +} - fn manifest_text( - org: &str, - name: &str, - version: &str, - dependencies: &[(&str, &str)], - ) -> String { - let mut text = format!( - r#"[package] +fn manifest_text( + org: &str, + name: &str, + version: &str, + dependencies: &[(&str, &str)], +) -> String { + let mut text = format!( + r#"[package] org = "{org}" name = "{name}" version = "{version}" @@ -37,222 +37,263 @@ version = "{version}" vcs = "git" url = "https://example.invalid/{org}/{name}" "#, - ); - if !dependencies.is_empty() { - text.push_str("\n[dependencies]\n"); - for (key, requirement) in dependencies { - text.push_str(&format!("\"{key}\" = \"{requirement}\"\n")); - } + ); + if !dependencies.is_empty() { + text.push_str("\n[dependencies]\n"); + for (key, requirement) in dependencies { + text.push_str(&format!("\"{key}\" = \"{requirement}\"\n")); } - text } + text +} - fn publish_fixture( - registry_root: &Path, - scratch: &Path, - org: &str, - name: &str, - version: &str, - dependencies: &[(&str, &str)], - ) -> String { - let source = scratch.join(format!("source-{name}")); - fs::create_dir_all(&source).unwrap(); - let manifest_text = manifest_text(org, name, version, dependencies); - fs::write(source.join(MANIFEST_FILE), &manifest_text).unwrap(); - fs::write(source.join("payload.txt"), format!("{org}/{name}@{version}\n")).unwrap(); - let manifest = Manifest::parse(&manifest_text).unwrap(); - let packed = pack( - &source, - &manifest, - Some(&scratch.join(format!("packed-{name}"))), - ) - .unwrap(); +fn publish_fixture( + registry_root: &Path, + scratch: &Path, + org: &str, + name: &str, + version: &str, + dependencies: &[(&str, &str)], +) -> String { + let source = scratch.join(format!("source-{name}")); + fs::create_dir_all(&source).unwrap(); + let manifest_text = manifest_text(org, name, version, dependencies); + fs::write(source.join(MANIFEST_FILE), &manifest_text).unwrap(); + fs::write(source.join("payload.txt"), format!("{org}/{name}@{version}\n")).unwrap(); + let manifest = Manifest::parse(&manifest_text).unwrap(); + let packed = pack( + &source, + &manifest, + Some(&scratch.join(format!("packed-{name}"))), + ) + .unwrap(); - let artifacts = registry_root.join("artifacts"); - fs::create_dir_all(&artifacts).unwrap(); - fs::copy( - &packed.path, - artifacts.join(format!("{}.tar.gz", packed.sha256)), - ) - .unwrap(); + let artifacts = registry_root.join("artifacts"); + fs::create_dir_all(&artifacts).unwrap(); + fs::copy( + &packed.path, + artifacts.join(format!("{}.tar.gz", packed.sha256)), + ) + .unwrap(); - let package_dir = registry_root.join("packages").join(org).join(name); - fs::create_dir_all(package_dir.join("versions")).unwrap(); - let version_metadata = VersionMetadata { - org: org.to_string(), - name: name.to_string(), - version: version.to_string(), - sha256: packed.sha256.clone(), - size: packed.size, - format: packed.format.clone(), - vcs_tag: format!("v{version}"), - vcs_commit: Some("0123456789abcdef0123456789abcdef01234567".to_string()), - download_url: format!("file://{}/{}.tar.gz", artifacts.display(), packed.sha256), - published_at: "1970-01-01T00:00:00Z".to_string(), - yanked: false, - }; - fs::write( - package_dir.join("versions").join(format!("{version}.json")), - serde_json::to_string_pretty(&version_metadata).unwrap(), - ) - .unwrap(); - let package_metadata = PackageMetadata { - org: org.to_string(), - name: name.to_string(), - description: Some(format!("fixture {name}")), - vcs: Vcs::Git, - repo_url: format!("https://example.invalid/{org}/{name}"), - version_scheme: manifest.package.version_scheme, - latest: Some(version.to_string()), - tags: Vec::new(), - versions: vec![version.to_string()], - }; - fs::write( - package_dir.join("package.json"), - serde_json::to_string_pretty(&package_metadata).unwrap(), - ) - .unwrap(); - packed.sha256 - } + let package_dir = registry_root.join("packages").join(org).join(name); + fs::create_dir_all(package_dir.join("versions")).unwrap(); + let version_metadata = VersionMetadata { + org: org.to_string(), + name: name.to_string(), + version: version.to_string(), + sha256: packed.sha256.clone(), + size: packed.size, + format: packed.format.clone(), + vcs_tag: format!("v{version}"), + vcs_commit: Some("0123456789abcdef0123456789abcdef01234567".to_string()), + download_url: format!("file://{}/{}.tar.gz", artifacts.display(), packed.sha256), + published_at: "1970-01-01T00:00:00Z".to_string(), + yanked: false, + }; + fs::write( + package_dir.join("versions").join(format!("{version}.json")), + serde_json::to_string_pretty(&version_metadata).unwrap(), + ) + .unwrap(); + let package_metadata = PackageMetadata { + org: org.to_string(), + name: name.to_string(), + description: Some(format!("fixture {name}")), + vcs: Vcs::Git, + repo_url: format!("https://example.invalid/{org}/{name}"), + version_scheme: manifest.package.version_scheme, + latest: Some(version.to_string()), + tags: Vec::new(), + versions: vec![version.to_string()], + }; + fs::write( + package_dir.join("package.json"), + serde_json::to_string_pretty(&package_metadata).unwrap(), + ) + .unwrap(); + packed.sha256 +} - #[test] - fn defaults_to_five_workers_and_bounds_overrides() { - assert_eq!(normalize_concurrency(None), 5); - assert_eq!(normalize_concurrency(Some("not-a-number")), 5); - assert_eq!(normalize_concurrency(Some("0")), 5); - assert_eq!(normalize_concurrency(Some("1")), 1); - assert_eq!(normalize_concurrency(Some("999")), MAX_INSTALL_CONCURRENCY); - } +#[test] +fn defaults_to_five_workers_and_bounds_overrides() { + assert_eq!(normalize_concurrency(None), 5); + assert_eq!(normalize_concurrency(Some("not-a-number")), 5); + assert_eq!(normalize_concurrency(Some("0")), 5); + assert_eq!(normalize_concurrency(Some("1")), 1); + assert_eq!(normalize_concurrency(Some("999")), MAX_INSTALL_CONCURRENCY); +} - #[test] - fn recursively_prefetches_packages_of_packages_and_deduplicates_diamonds() { - let temp = tempfile::tempdir().unwrap(); - let registry = temp.path().join("registry"); - let scratch = temp.path().join("scratch"); - let home = temp.path().join("home"); - let project = temp.path().join("project"); - fs::create_dir_all(&project).unwrap(); +#[test] +fn recursively_prefetches_packages_of_packages_and_deduplicates_diamonds() { + let temp = tempfile::tempdir().unwrap(); + let registry = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); - let leaf = publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); - let left = publish_fixture( - ®istry, - &scratch, - "test", - "left", - "1.0.0", - &[("test/leaf", "^1")], - ); - let right = publish_fixture( - ®istry, - &scratch, - "test", - "right", - "1.0.0", - &[("test/leaf", "^1")], - ); - let root = publish_fixture( - ®istry, - &scratch, - "test", - "root", - "1.0.0", - &[("test/left", "^1"), ("test/right", "^1")], - ); - fs::write( - project.join(MANIFEST_FILE), - manifest_text("consumer", "app", "0.1.0", &[("test/root", "^1")]), - ) - .unwrap(); + let leaf = publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); + let left = publish_fixture( + ®istry, + &scratch, + "test", + "left", + "1.0.0", + &[("test/leaf", "^1")], + ); + let right = publish_fixture( + ®istry, + &scratch, + "test", + "right", + "1.0.0", + &[("test/leaf", "^1")], + ); + let root = publish_fixture( + ®istry, + &scratch, + "test", + "root", + "1.0.0", + &[("test/left", "^1"), ("test/right", "^1")], + ); + fs::write( + project.join(MANIFEST_FILE), + manifest_text("consumer", "app", "0.1.0", &[("test/root", "^1")]), + ) + .unwrap(); - let report = prefetch(&project, &test_config(®istry, &home), false).unwrap(); - assert_eq!(report.resolved, 4); - assert_eq!(report.downloaded, 4); - let store = Store::new(&home); - for sha in [leaf, left, right, root] { - assert!(store.has(&sha), "missing prefetched artifact {sha}"); - } + let report = prefetch(&project, &test_config(®istry, &home), false).unwrap(); + assert_eq!(report.resolved, 4); + assert_eq!(report.downloaded, 4); + let store = Store::new(&home); + for sha in [leaf, left, right, root] { + assert!(store.has(&sha), "missing prefetched artifact {sha}"); } +} - #[test] - fn dependency_cycles_terminate_after_each_package_is_resolved_once() { - let temp = tempfile::tempdir().unwrap(); - let registry = temp.path().join("registry"); - let scratch = temp.path().join("scratch"); - let home = temp.path().join("home"); - let project = temp.path().join("project"); - fs::create_dir_all(&project).unwrap(); +#[test] +fn dependency_cycles_terminate_after_each_package_is_resolved_once() { + let temp = tempfile::tempdir().unwrap(); + let registry = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); - publish_fixture( - ®istry, - &scratch, - "test", - "a", - "1.0.0", - &[("test/b", "^1")], - ); - publish_fixture( - ®istry, - &scratch, - "test", - "b", - "1.0.0", - &[("test/a", "^1")], - ); - fs::write( - project.join(MANIFEST_FILE), - manifest_text("consumer", "app", "0.1.0", &[("test/a", "^1")]), - ) - .unwrap(); + publish_fixture( + ®istry, + &scratch, + "test", + "a", + "1.0.0", + &[("test/b", "^1")], + ); + publish_fixture( + ®istry, + &scratch, + "test", + "b", + "1.0.0", + &[("test/a", "^1")], + ); + fs::write( + project.join(MANIFEST_FILE), + manifest_text("consumer", "app", "0.1.0", &[("test/a", "^1")]), + ) + .unwrap(); - let report = prefetch(&project, &test_config(®istry, &home), false).unwrap(); - assert_eq!(report.resolved, 2); - assert_eq!(report.downloaded, 2); - } + let report = prefetch(&project, &test_config(®istry, &home), false).unwrap(); + assert_eq!(report.resolved, 2); + assert_eq!(report.downloaded, 2); +} - #[test] - fn a_corrupt_partial_cache_is_replaced_under_the_artifact_lock() { - let temp = tempfile::tempdir().unwrap(); - let registry = temp.path().join("registry"); - let scratch = temp.path().join("scratch"); - let home = temp.path().join("home"); - let project = temp.path().join("project"); - fs::create_dir_all(&project).unwrap(); +#[test] +fn concurrent_prefetches_share_one_artifact_download() { + let temp = tempfile::tempdir().unwrap(); + let registry = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + let first_project = temp.path().join("first-project"); + let second_project = temp.path().join("second-project"); + fs::create_dir_all(&first_project).unwrap(); + fs::create_dir_all(&second_project).unwrap(); - let sha = publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); - fs::write( - project.join(MANIFEST_FILE), - manifest_text("consumer", "app", "0.1.0", &[("test/leaf", "^1")]), - ) - .unwrap(); - let store = Store::new(&home); - let cached = store.cached_artifact(&sha); - fs::create_dir_all(cached.parent().unwrap()).unwrap(); - fs::write(&cached, b"partial download").unwrap(); + publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); + let consumer_manifest = + manifest_text("consumer", "app", "0.1.0", &[("test/leaf", "^1")]); + fs::write(first_project.join(MANIFEST_FILE), &consumer_manifest).unwrap(); + fs::write(second_project.join(MANIFEST_FILE), &consumer_manifest).unwrap(); - let report = prefetch(&project, &test_config(®istry, &home), false).unwrap(); - assert_eq!(report.resolved, 1); - assert_eq!(report.downloaded, 1); - assert!(store.has(&sha)); - assert_eq!(sha256_file(&cached).unwrap().0, sha); - } + let barrier = Arc::new(Barrier::new(3)); + let first_cfg = test_config(®istry, &home); + let second_cfg = first_cfg.clone(); + let first_barrier = Arc::clone(&barrier); + let second_barrier = Arc::clone(&barrier); + let first = thread::spawn(move || { + first_barrier.wait(); + prefetch(&first_project, &first_cfg, false).unwrap() + }); + let second = thread::spawn(move || { + second_barrier.wait(); + prefetch(&second_project, &second_cfg, false).unwrap() + }); - #[cfg(unix)] - #[test] - fn artifact_waiters_block_until_the_owner_releases_the_kernel_lock() { - let temp = tempfile::tempdir().unwrap(); - let sha = "a".repeat(64); - let owner = ArtifactProcessLock::acquire(temp.path(), &sha).unwrap(); - let second_home = temp.path().to_path_buf(); - let second_sha = sha.clone(); - let (acquired_tx, acquired_rx) = test_mpsc::channel(); - let waiter = thread::spawn(move || { - let _waiter = ArtifactProcessLock::acquire(&second_home, &second_sha).unwrap(); - acquired_tx.send(()).unwrap(); - }); + barrier.wait(); + let first = first.join().unwrap(); + let second = second.join().unwrap(); + assert_eq!(first.resolved + second.resolved, 2); + assert_eq!( + first.downloaded + second.downloaded, + 1, + "one process should publish the shared content hash and the other should reuse it" + ); +} - assert!(acquired_rx.recv_timeout(Duration::from_millis(100)).is_err()); - drop(owner); - acquired_rx.recv_timeout(Duration::from_secs(2)).unwrap(); - waiter.join().unwrap(); - } +#[test] +fn a_corrupt_partial_cache_is_replaced_under_the_artifact_lock() { + let temp = tempfile::tempdir().unwrap(); + let registry = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); + + let sha = publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); + fs::write( + project.join(MANIFEST_FILE), + manifest_text("consumer", "app", "0.1.0", &[("test/leaf", "^1")]), + ) + .unwrap(); + let store = Store::new(&home); + let cached = store.cached_artifact(&sha); + fs::create_dir_all(cached.parent().unwrap()).unwrap(); + fs::write(&cached, b"partial download").unwrap(); + + let report = prefetch(&project, &test_config(®istry, &home), false).unwrap(); + assert_eq!(report.resolved, 1); + assert_eq!(report.downloaded, 1); + assert!(store.has(&sha)); + assert_eq!(sha256_file(&cached).unwrap().0, sha); +} + +#[cfg(unix)] +#[test] +fn artifact_waiters_block_until_the_owner_releases_the_kernel_lock() { + let temp = tempfile::tempdir().unwrap(); + let sha = "a".repeat(64); + let owner = ArtifactProcessLock::acquire(temp.path(), &sha).unwrap(); + let second_home = temp.path().to_path_buf(); + let second_sha = sha.clone(); + let (acquired_tx, acquired_rx) = test_mpsc::channel(); + let waiter = thread::spawn(move || { + let _waiter = ArtifactProcessLock::acquire(&second_home, &second_sha).unwrap(); + acquired_tx.send(()).unwrap(); + }); + + assert!(acquired_rx.recv_timeout(Duration::from_millis(100)).is_err()); + drop(owner); + acquired_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + waiter.join().unwrap(); } From 98d4e3234de89d21b9ca86476b325d958e757ae4 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:56:07 -0500 Subject: [PATCH 12/80] fix: preserve deterministic dependency expansion --- src/install_graph.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/install_graph.rs b/src/install_graph.rs index 6c6f36db..be2e41a9 100644 --- a/src/install_graph.rs +++ b/src/install_graph.rs @@ -50,6 +50,7 @@ struct DependencyRequest { #[derive(Debug, Clone)] struct FetchTask { + sequence: usize, key: String, version: VersionMetadata, } @@ -60,6 +61,12 @@ struct FetchResult { downloaded: bool, } +#[derive(Debug)] +struct FetchMessage { + sequence: usize, + result: Result, +} + #[derive(Debug, Default)] struct QueueState { tasks: VecDeque, @@ -121,7 +128,7 @@ impl TaskQueue { struct FetchPool { queue: Arc, - results: mpsc::Receiver>, + results: mpsc::Receiver, workers: Vec>, } @@ -172,10 +179,10 @@ impl FetchPool { Ok(()) } - fn receive(&self) -> Result { + fn receive(&self) -> Result { self.results .recv() - .context("all recursive install workers stopped unexpectedly")? + .context("all recursive install workers stopped unexpectedly") } fn shutdown(self, cancel: bool) -> Result<()> { From 24038efa9e6273f794bee75b7fb7bc4f1825f659 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:56:36 -0500 Subject: [PATCH 13/80] fix: tag worker results with deterministic sequence --- src/install_graph/artifact.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/install_graph/artifact.rs b/src/install_graph/artifact.rs index c6ea610d..3b140d89 100644 --- a/src/install_graph/artifact.rs +++ b/src/install_graph/artifact.rs @@ -2,7 +2,7 @@ use super::*; pub(super) fn worker_loop( queue: Arc, - results: mpsc::Sender>, + results: mpsc::Sender, registry_url: String, home: PathBuf, ) { @@ -10,6 +10,7 @@ pub(super) fn worker_loop( let mut registry: Option> = None; while let Some(task) = queue.pop() { + let sequence = task.sequence; let result = (|| -> Result { if registry.is_none() { registry = Some(registry_for(®istry_url)?); @@ -19,7 +20,7 @@ pub(super) fn worker_loop( .context("recursive install worker has no registry")?; prefetch_one(registry, &store, &home, task) })(); - if results.send(result).is_err() { + if results.send(FetchMessage { sequence, result }).is_err() { return; } } From bbca23825fd163faf4acd885009f147b08a3a3eb Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 00:57:11 -0500 Subject: [PATCH 14/80] fix: expand completed manifests in submission order --- src/install_graph/resolver.rs | 39 ++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/install_graph/resolver.rs b/src/install_graph/resolver.rs index 48ab829f..d222759e 100644 --- a/src/install_graph/resolver.rs +++ b/src/install_graph/resolver.rs @@ -92,7 +92,11 @@ fn prefetch_locked( version.sha256 ); } - tasks.push(FetchTask { key, version }); + tasks.push(FetchTask { + sequence: tasks.len(), + key, + version, + }); } if tasks.is_empty() { @@ -106,7 +110,8 @@ fn prefetch_locked( } let mut downloaded = 0usize; for _ in 0..total { - let result = pool.receive()?; + let message = pool.receive()?; + let result = message.result?; downloaded += usize::from(result.downloaded); } Ok(PrefetchReport { @@ -140,6 +145,9 @@ fn prefetch_recursive( let mut resolved: BTreeMap = BTreeMap::new(); let mut expanded_workspace = BTreeSet::new(); let mut in_flight = 0usize; + let mut next_sequence = 0usize; + let mut next_result = 0usize; + let mut buffered_results: BTreeMap> = BTreeMap::new(); let mut downloaded = 0usize; loop { @@ -173,9 +181,11 @@ fn prefetch_recursive( let version = resolve_version(registry, &org, &name, &request.requirement)?; resolved.insert(request.key.clone(), version.clone()); pool.submit(FetchTask { + sequence: next_sequence, key: request.key, version, })?; + next_sequence += 1; in_flight += 1; } @@ -183,7 +193,8 @@ fn prefetch_recursive( break; } - let result = pool.receive()?; + let result = receive_in_order(pool, &mut buffered_results, next_result)?; + next_result += 1; in_flight -= 1; downloaded += usize::from(result.downloaded); pending.extend(result.dependencies.into_iter().map( @@ -197,3 +208,25 @@ fn prefetch_recursive( }) }) } + +fn receive_in_order( + pool: &FetchPool, + buffered: &mut BTreeMap>, + expected: usize, +) -> Result { + loop { + if let Some(result) = buffered.remove(&expected) { + return result; + } + let message = pool.receive()?; + if message.sequence == expected { + return message.result; + } + if buffered.insert(message.sequence, message.result).is_some() { + bail!( + "recursive install worker returned duplicate result sequence {}", + message.sequence + ); + } + } +} From 75658c88610591276a804b5bf12fcaf09b75d1f1 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 01:05:32 -0500 Subject: [PATCH 15/80] style: apply rustfmt to installer facade --- src/ops_entry.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ops_entry.rs b/src/ops_entry.rs index 6edb4871..4342943a 100644 --- a/src/ops_entry.rs +++ b/src/ops_entry.rs @@ -16,8 +16,8 @@ use crate::config::Config; mod implementation; pub use implementation::{ - InstallOutcome, WorkspaceInfo, add, build_cmd, build_publish_meta, cache_clean, find, gc, - init, login, org_audit, org_claim, pack_cmd, publish, remove, run, split_key, store_prune, + InstallOutcome, WorkspaceInfo, add, build_cmd, build_publish_meta, cache_clean, find, gc, init, + login, org_audit, org_claim, pack_cmd, publish, remove, run, split_key, store_prune, store_status, uninstall, yank, }; From df485770abb938b4df4e92a27a48ab91c8c1eb25 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 01:06:15 -0500 Subject: [PATCH 16/80] style: apply rustfmt to install graph --- src/install_graph.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/install_graph.rs b/src/install_graph.rs index be2e41a9..210e54e8 100644 --- a/src/install_graph.rs +++ b/src/install_graph.rs @@ -145,14 +145,7 @@ impl FetchPool { let worker_home = home.to_path_buf(); let spawn = thread::Builder::new() .name(format!("zed-install-{index}")) - .spawn(move || { - worker_loop( - worker_queue, - worker_tx, - worker_registry, - worker_home, - ) - }); + .spawn(move || worker_loop(worker_queue, worker_tx, worker_registry, worker_home)); match spawn { Ok(worker) => workers.push(worker), Err(error) => { @@ -212,9 +205,7 @@ struct ArtifactProcessLock { impl ArtifactProcessLock { fn acquire(home: &Path, sha256: &str) -> Result { require_sha256(sha256)?; - let path = home - .join("locks") - .join(format!("artifact-{sha256}.lock")); + let path = home.join("locks").join(format!("artifact-{sha256}.lock")); if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } From bc77b5b859fa45d9e00f7e11b13373221132f7d1 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 01:06:57 -0500 Subject: [PATCH 17/80] style: apply rustfmt to artifact worker --- src/install_graph/artifact.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/install_graph/artifact.rs b/src/install_graph/artifact.rs index 3b140d89..f7a2d5b6 100644 --- a/src/install_graph/artifact.rs +++ b/src/install_graph/artifact.rs @@ -33,12 +33,7 @@ fn prefetch_one( task: FetchTask, ) -> Result { let (package_dir, downloaded) = ensure_artifact(registry, store, home, &task.version) - .with_context(|| { - format!( - "prefetching {}@{}", - task.key, task.version.version - ) - })?; + .with_context(|| format!("prefetching {}@{}", task.key, task.version.version))?; let dependencies = read_manifest(&package_dir) .map(|manifest| manifest.dependencies) .unwrap_or_default(); @@ -81,9 +76,7 @@ fn ensure_artifact( store .add_artifact(&cached, &version.sha256) .with_context(|| { - format!( - "cached artifact was invalid ({first_error:#}); redownload also failed" - ) + format!("cached artifact was invalid ({first_error:#}); redownload also failed") }) .map(|package_dir| (package_dir, true)) } From 2d3bce43272c1fd590ef85b92cd2cd2352951bd3 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 01:08:37 -0500 Subject: [PATCH 18/80] style: apply rustfmt to graph resolver --- src/install_graph/resolver.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/install_graph/resolver.rs b/src/install_graph/resolver.rs index d222759e..7fc169e4 100644 --- a/src/install_graph/resolver.rs +++ b/src/install_graph/resolver.rs @@ -1,5 +1,5 @@ -use super::*; use super::artifact::{resolve_version, split_key, validate_version_identity}; +use super::*; /// Warm the content-addressed store for an install. The normal installer runs /// immediately afterward and remains responsible for the project transaction. @@ -23,11 +23,7 @@ pub fn prefetch(project: &Path, cfg: &Config, frozen: bool) -> Result usize { - normalize_concurrency( - std::env::var("ZED_PKG_INSTALL_CONCURRENCY") - .ok() - .as_deref(), - ) + normalize_concurrency(std::env::var("ZED_PKG_INSTALL_CONCURRENCY").ok().as_deref()) } pub(super) fn normalize_concurrency(raw: Option<&str>) -> usize { @@ -154,12 +150,12 @@ fn prefetch_recursive( while let Some(request) = pending.pop_front() { if let Some(member_dependencies) = workspace.dependencies.get(&request.key) { if expanded_workspace.insert(request.key.clone()) { - pending.extend(member_dependencies.iter().map( - |(key, requirement)| DependencyRequest { + pending.extend(member_dependencies.iter().map(|(key, requirement)| { + DependencyRequest { key: key.clone(), requirement: requirement.clone(), - }, - )); + } + })); } continue; } @@ -197,9 +193,12 @@ fn prefetch_recursive( next_result += 1; in_flight -= 1; downloaded += usize::from(result.downloaded); - pending.extend(result.dependencies.into_iter().map( - |(key, requirement)| DependencyRequest { key, requirement }, - )); + pending.extend( + result + .dependencies + .into_iter() + .map(|(key, requirement)| DependencyRequest { key, requirement }), + ); } Ok(PrefetchReport { From 99ca21a363b38f42a3cd42e1021049019753c1c6 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 01:09:28 -0500 Subject: [PATCH 19/80] style: apply rustfmt to recursive installer tests --- src/install_graph/tests.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/install_graph/tests.rs b/src/install_graph/tests.rs index c9ecc6c0..ad93c24b 100644 --- a/src/install_graph/tests.rs +++ b/src/install_graph/tests.rs @@ -21,12 +21,7 @@ fn test_config(registry: &Path, home: &Path) -> Config { } } -fn manifest_text( - org: &str, - name: &str, - version: &str, - dependencies: &[(&str, &str)], -) -> String { +fn manifest_text(org: &str, name: &str, version: &str, dependencies: &[(&str, &str)]) -> String { let mut text = format!( r#"[package] org = "{org}" @@ -59,7 +54,11 @@ fn publish_fixture( fs::create_dir_all(&source).unwrap(); let manifest_text = manifest_text(org, name, version, dependencies); fs::write(source.join(MANIFEST_FILE), &manifest_text).unwrap(); - fs::write(source.join("payload.txt"), format!("{org}/{name}@{version}\n")).unwrap(); + fs::write( + source.join("payload.txt"), + format!("{org}/{name}@{version}\n"), + ) + .unwrap(); let manifest = Manifest::parse(&manifest_text).unwrap(); let packed = pack( &source, @@ -221,8 +220,7 @@ fn concurrent_prefetches_share_one_artifact_download() { fs::create_dir_all(&second_project).unwrap(); publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); - let consumer_manifest = - manifest_text("consumer", "app", "0.1.0", &[("test/leaf", "^1")]); + let consumer_manifest = manifest_text("consumer", "app", "0.1.0", &[("test/leaf", "^1")]); fs::write(first_project.join(MANIFEST_FILE), &consumer_manifest).unwrap(); fs::write(second_project.join(MANIFEST_FILE), &consumer_manifest).unwrap(); @@ -292,7 +290,11 @@ fn artifact_waiters_block_until_the_owner_releases_the_kernel_lock() { acquired_tx.send(()).unwrap(); }); - assert!(acquired_rx.recv_timeout(Duration::from_millis(100)).is_err()); + assert!( + acquired_rx + .recv_timeout(Duration::from_millis(100)) + .is_err() + ); drop(owner); acquired_rx.recv_timeout(Duration::from_secs(2)).unwrap(); waiter.join().unwrap(); From c8be1792c12f2d894d28b7fa0aaa0c396052edc8 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 01:16:49 -0500 Subject: [PATCH 20/80] test: cover warm stores frozen locks and conflicts --- src/install_graph/tests.rs | 167 +++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/src/install_graph/tests.rs b/src/install_graph/tests.rs index ad93c24b..b85b9685 100644 --- a/src/install_graph/tests.rs +++ b/src/install_graph/tests.rs @@ -2,6 +2,7 @@ use std::sync::{Arc, Barrier, mpsc as test_mpsc}; use std::thread; use std::time::Duration; +use zed_interfaces::lockfile::LockedPackage; use zed_interfaces::registry::{PackageMetadata, VersionMetadata}; use zed_interfaces::vcs::Vcs; @@ -114,6 +115,21 @@ fn publish_fixture( packed.sha256 } +fn published_version( + registry_root: &Path, + org: &str, + name: &str, + version: &str, +) -> VersionMetadata { + let path = registry_root + .join("packages") + .join(org) + .join(name) + .join("versions") + .join(format!("{version}.json")); + serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap() +} + #[test] fn defaults_to_five_workers_and_bounds_overrides() { assert_eq!(normalize_concurrency(None), 5); @@ -249,6 +265,157 @@ fn concurrent_prefetches_share_one_artifact_download() { ); } +#[test] +fn a_warm_store_resolves_the_graph_without_redownloading_artifacts() { + let temp = tempfile::tempdir().unwrap(); + let registry = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); + + publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); + publish_fixture( + ®istry, + &scratch, + "test", + "left", + "1.0.0", + &[("test/leaf", "^1")], + ); + publish_fixture( + ®istry, + &scratch, + "test", + "right", + "1.0.0", + &[("test/leaf", "^1")], + ); + publish_fixture( + ®istry, + &scratch, + "test", + "root", + "1.0.0", + &[("test/left", "^1"), ("test/right", "^1")], + ); + fs::write( + project.join(MANIFEST_FILE), + manifest_text("consumer", "app", "0.1.0", &[("test/root", "^1")]), + ) + .unwrap(); + let cfg = test_config(®istry, &home); + + let cold = prefetch(&project, &cfg, false).unwrap(); + let warm = prefetch(&project, &cfg, false).unwrap(); + assert_eq!(cold.resolved, 4); + assert_eq!(cold.downloaded, 4); + assert_eq!(warm.resolved, 4); + assert_eq!(warm.downloaded, 0); +} + +#[test] +fn conflicting_transitive_requirements_fail_in_deterministic_graph_order() { + let temp = tempfile::tempdir().unwrap(); + let registry = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); + + publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); + publish_fixture( + ®istry, + &scratch, + "test", + "left", + "1.0.0", + &[("test/leaf", "^1")], + ); + publish_fixture( + ®istry, + &scratch, + "test", + "right", + "1.0.0", + &[("test/leaf", "^2")], + ); + publish_fixture( + ®istry, + &scratch, + "test", + "root", + "1.0.0", + &[("test/left", "^1"), ("test/right", "^1")], + ); + fs::write( + project.join(MANIFEST_FILE), + manifest_text("consumer", "app", "0.1.0", &[("test/root", "^1")]), + ) + .unwrap(); + + let error = prefetch(&project, &test_config(®istry, &home), false).unwrap_err(); + let message = format!("{error:#}"); + assert!(message.contains("version conflict for test/leaf"), "{message}"); + assert!(message.contains("resolved 1.0.0"), "{message}"); + assert!(message.contains("requires `^2`"), "{message}"); +} + +#[test] +fn frozen_prefetch_installs_every_locked_artifact_without_a_manifest() { + let temp = tempfile::tempdir().unwrap(); + let registry = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + let project = temp.path().join("project"); + fs::create_dir_all(&project).unwrap(); + + for (name, dependencies) in [ + ("leaf", vec![]), + ("left", vec![("test/leaf", "^1")]), + ("right", vec![("test/leaf", "^1")]), + ( + "root", + vec![("test/left", "^1"), ("test/right", "^1")], + ), + ] { + publish_fixture( + ®istry, + &scratch, + "test", + name, + "1.0.0", + &dependencies, + ); + } + + let source = format!("file://{}", registry.display()); + let mut lock = Lockfile::default(); + for name in ["leaf", "left", "right", "root"] { + let version = published_version(®istry, "test", name, "1.0.0"); + lock.upsert(LockedPackage { + org: version.org, + name: version.name, + version: version.version, + sha256: version.sha256, + size: version.size, + format: version.format, + vcs_tag: version.vcs_tag, + vcs_commit: version.vcs_commit, + source: source.clone(), + }); + } + fs::write( + project.join(LOCKFILE_FILE), + lock.to_toml_string().unwrap(), + ) + .unwrap(); + + let report = prefetch(&project, &test_config(®istry, &home), true).unwrap(); + assert_eq!(report.resolved, 4); + assert_eq!(report.downloaded, 4); +} + #[test] fn a_corrupt_partial_cache_is_replaced_under_the_artifact_lock() { let temp = tempfile::tempdir().unwrap(); From 58d688b1a1bfbbb9b1ff551a303b7360a8ca94be Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 01:20:16 -0500 Subject: [PATCH 21/80] style: format expanded recursive installer tests --- src/install_graph/tests.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/install_graph/tests.rs b/src/install_graph/tests.rs index b85b9685..acabd85d 100644 --- a/src/install_graph/tests.rs +++ b/src/install_graph/tests.rs @@ -356,7 +356,10 @@ fn conflicting_transitive_requirements_fail_in_deterministic_graph_order() { let error = prefetch(&project, &test_config(®istry, &home), false).unwrap_err(); let message = format!("{error:#}"); - assert!(message.contains("version conflict for test/leaf"), "{message}"); + assert!( + message.contains("version conflict for test/leaf"), + "{message}" + ); assert!(message.contains("resolved 1.0.0"), "{message}"); assert!(message.contains("requires `^2`"), "{message}"); } @@ -374,19 +377,9 @@ fn frozen_prefetch_installs_every_locked_artifact_without_a_manifest() { ("leaf", vec![]), ("left", vec![("test/leaf", "^1")]), ("right", vec![("test/leaf", "^1")]), - ( - "root", - vec![("test/left", "^1"), ("test/right", "^1")], - ), + ("root", vec![("test/left", "^1"), ("test/right", "^1")]), ] { - publish_fixture( - ®istry, - &scratch, - "test", - name, - "1.0.0", - &dependencies, - ); + publish_fixture(®istry, &scratch, "test", name, "1.0.0", &dependencies); } let source = format!("file://{}", registry.display()); @@ -405,11 +398,7 @@ fn frozen_prefetch_installs_every_locked_artifact_without_a_manifest() { source: source.clone(), }); } - fs::write( - project.join(LOCKFILE_FILE), - lock.to_toml_string().unwrap(), - ) - .unwrap(); + fs::write(project.join(LOCKFILE_FILE), lock.to_toml_string().unwrap()).unwrap(); let report = prefetch(&project, &test_config(®istry, &home), true).unwrap(); assert_eq!(report.resolved, 4); From df9a5526d72dcb71e6ba02c3a8f788a5eb848d3a Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 01:31:32 -0500 Subject: [PATCH 22/80] feat: prefetch in-memory add and remove manifests --- src/config.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 34bf3811..89210180 100644 --- a/src/config.rs +++ b/src/config.rs @@ -156,6 +156,7 @@ struct ManifestOverride { thread_local! { static MANIFEST_OVERRIDE: RefCell> = const { RefCell::new(None) }; + static INSTALL_PREFETCH_CONFIG: RefCell> = const { RefCell::new(None) }; } struct ManifestOverrideGuard; @@ -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( + cfg: &Config, + operation: impl FnOnce() -> Result, +) -> Result { + 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 @@ -189,10 +224,17 @@ pub(crate) fn with_manifest_override( "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 @@ -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 { From 0b1e11ac2ae0af75fa31e969f8a58dda80be329e Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 01:31:51 -0500 Subject: [PATCH 23/80] fix: route add and remove through recursive prefetch --- src/ops_entry.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ops_entry.rs b/src/ops_entry.rs index 4342943a..dde8eb67 100644 --- a/src/ops_entry.rs +++ b/src/ops_entry.rs @@ -16,15 +16,23 @@ use crate::config::Config; mod implementation; pub use implementation::{ - InstallOutcome, WorkspaceInfo, add, build_cmd, build_publish_meta, cache_clean, find, gc, init, - login, org_audit, org_claim, pack_cmd, publish, remove, run, split_key, store_prune, - store_status, uninstall, yank, + InstallOutcome, WorkspaceInfo, build_cmd, build_publish_meta, cache_clean, find, gc, init, + login, org_audit, org_claim, pack_cmd, publish, run, split_key, store_prune, store_status, + uninstall, yank, }; pub(crate) use implementation::{ detect_adapter, detect_native_manifest_target, detect_structure_target, detect_target, }; +pub fn add(project: &Path, cfg: &Config, spec: &str) -> Result<()> { + crate::config::with_install_prefetch(cfg, || implementation::add(project, cfg, spec)) +} + +pub fn remove(project: &Path, cfg: &Config, spec: &str) -> Result<()> { + crate::config::with_install_prefetch(cfg, || implementation::remove(project, cfg, spec)) +} + #[allow(clippy::too_many_arguments)] pub fn install( project: &Path, From f84c36672f0fbc15616f256add197b5ca9cb81fe Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 01:59:38 -0500 Subject: [PATCH 24/80] test: measure five concurrent recursive artifact transfers --- tests/recursive_install_concurrency.rs | 447 +++++++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 tests/recursive_install_concurrency.rs diff --git a/tests/recursive_install_concurrency.rs b/tests/recursive_install_concurrency.rs new file mode 100644 index 00000000..9d0c4239 --- /dev/null +++ b/tests/recursive_install_concurrency.rs @@ -0,0 +1,447 @@ +use std::collections::HashMap; +use std::fs; +use std::io::{BufRead, BufReader, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Condvar, Mutex}; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result}; +use zed_cli::config::Config; +use zed_cli::install_graph::prefetch; +use zed_cli::pack::pack; +use zed_cli::store::Store; +use zed_interfaces::manifest::Manifest; +use zed_interfaces::registry::{self, PackageMetadata, VersionMetadata}; +use zed_interfaces::vcs::Vcs; + +const ORG: &str = "recursive-concurrency"; +const VERSION: &str = "1.0.0"; +const PACKAGE_COUNT: usize = 12; +const EXPECTED_CONCURRENCY: usize = 5; + +#[derive(Clone)] +struct Response { + content_type: &'static str, + body: Arc>, + artifact: bool, +} + +#[derive(Default)] +struct GateState { + arrived: usize, + open: bool, + timed_out: bool, +} + +struct ServerState { + responses: HashMap, + active_artifacts: AtomicUsize, + max_active_artifacts: AtomicUsize, + artifact_requests: AtomicUsize, + handler_failed: AtomicBool, + gate: Mutex, + gate_ready: Condvar, +} + +struct RegistryServer { + address: SocketAddr, + state: Arc, + shutdown: Arc, + accept_thread: Option>, +} + +impl RegistryServer { + fn start(listener: TcpListener, responses: HashMap) -> Result { + let address = listener.local_addr()?; + let state = Arc::new(ServerState { + responses, + active_artifacts: AtomicUsize::new(0), + max_active_artifacts: AtomicUsize::new(0), + artifact_requests: AtomicUsize::new(0), + handler_failed: AtomicBool::new(false), + gate: Mutex::new(GateState::default()), + gate_ready: Condvar::new(), + }); + let shutdown = Arc::new(AtomicBool::new(false)); + let accept_state = Arc::clone(&state); + let accept_shutdown = Arc::clone(&shutdown); + let accept_thread = thread::Builder::new() + .name("recursive-registry".to_string()) + .spawn(move || { + while let Ok((stream, _)) = listener.accept() { + if accept_shutdown.load(Ordering::SeqCst) { + break; + } + let handler_state = Arc::clone(&accept_state); + thread::spawn(move || { + if let Err(error) = handle_connection(stream, &handler_state) { + handler_state.handler_failed.store(true, Ordering::SeqCst); + eprintln!("test registry handler failed: {error:#}"); + } + }); + } + }) + .context("starting test registry accept loop")?; + Ok(Self { + address, + state, + shutdown, + accept_thread: Some(accept_thread), + }) + } + + fn base_url(&self) -> String { + format!("http://{}", self.address) + } + + fn max_active_artifacts(&self) -> usize { + self.state.max_active_artifacts.load(Ordering::SeqCst) + } + + fn artifact_requests(&self) -> usize { + self.state.artifact_requests.load(Ordering::SeqCst) + } + + fn gate_timed_out(&self) -> bool { + self.state + .gate + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .timed_out + } +} + +impl Drop for RegistryServer { + fn drop(&mut self) { + self.shutdown.store(true, Ordering::SeqCst); + let _ = TcpStream::connect(self.address); + if let Some(thread) = self.accept_thread.take() { + let _ = thread.join(); + } + } +} + +fn handle_connection(mut stream: TcpStream, state: &ServerState) -> Result<()> { + let mut reader = BufReader::new(stream.try_clone()?); + let mut request_line = String::new(); + if reader.read_line(&mut request_line)? == 0 { + return Ok(()); + } + let path = request_line + .split_whitespace() + .nth(1) + .context("HTTP request has a path")? + .split('?') + .next() + .unwrap_or_default() + .to_string(); + + loop { + let mut header = String::new(); + if reader.read_line(&mut header)? == 0 || header == "\r\n" || header == "\n" { + break; + } + } + + let Some(response) = state.responses.get(&path) else { + return write_response(&mut stream, "404 Not Found", "text/plain", b"not found\n"); + }; + + if !response.artifact { + return write_response( + &mut stream, + "200 OK", + response.content_type, + response.body.as_slice(), + ); + } + + state.artifact_requests.fetch_add(1, Ordering::SeqCst); + let active = state.active_artifacts.fetch_add(1, Ordering::SeqCst) + 1; + state + .max_active_artifacts + .fetch_max(active, Ordering::SeqCst); + + let mut gate = state + .gate + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + gate.arrived += 1; + if gate.arrived >= EXPECTED_CONCURRENCY { + gate.open = true; + state.gate_ready.notify_all(); + } + let deadline = Instant::now() + Duration::from_secs(5); + while !gate.open { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + gate.timed_out = true; + gate.open = true; + state.gate_ready.notify_all(); + break; + } + let (next, timeout) = state + .gate_ready + .wait_timeout(gate, remaining) + .unwrap_or_else(|poisoned| poisoned.into_inner()); + gate = next; + if timeout.timed_out() && !gate.open { + gate.timed_out = true; + gate.open = true; + state.gate_ready.notify_all(); + } + } + drop(gate); + + // Keep the first wave alive briefly so an implementation that exceeds the + // configured bound is visible in max_active_artifacts rather than hidden + // by very small fixture archives. + thread::sleep(Duration::from_millis(75)); + let result = write_response( + &mut stream, + "200 OK", + response.content_type, + response.body.as_slice(), + ); + state.active_artifacts.fetch_sub(1, Ordering::SeqCst); + result +} + +fn write_response( + stream: &mut TcpStream, + status: &str, + content_type: &str, + body: &[u8], +) -> Result<()> { + write!( + stream, + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + )?; + stream.write_all(body)?; + stream.flush()?; + Ok(()) +} + +struct PublishedPackage { + name: String, + package: PackageMetadata, + version: VersionMetadata, + artifact: Vec, +} + +fn package_manifest(name: &str) -> String { + format!( + r#"[package] +org = "{ORG}" +name = "{name}" +version = "{VERSION}" +description = "bounded recursive install fixture" + +[package.repository] +vcs = "git" +url = "https://example.invalid/{ORG}/{name}" +"#, + ) +} + +fn build_package(scratch: &Path, name: &str) -> Result { + let source = scratch.join(format!("source-{name}")); + fs::create_dir_all(&source)?; + let manifest_text = package_manifest(name); + fs::write(source.join(".zpkg.toml"), &manifest_text)?; + fs::write( + source.join("payload.txt"), + format!("{ORG}/{name}@{VERSION}\n"), + )?; + let manifest = Manifest::parse(&manifest_text)?; + let packed = pack( + &source, + &manifest, + Some(&scratch.join(format!("packed-{name}"))), + )?; + let artifact = fs::read(&packed.path)?; + Ok(PublishedPackage { + name: name.to_string(), + package: PackageMetadata { + org: ORG.to_string(), + name: name.to_string(), + description: Some(format!("fixture {name}")), + vcs: Vcs::Git, + repo_url: format!("https://example.invalid/{ORG}/{name}"), + version_scheme: manifest.package.version_scheme, + latest: Some(VERSION.to_string()), + tags: Vec::new(), + versions: vec![VERSION.to_string()], + }, + version: VersionMetadata { + org: ORG.to_string(), + name: name.to_string(), + version: VERSION.to_string(), + sha256: packed.sha256, + size: packed.size, + format: packed.format, + vcs_tag: format!("v{VERSION}"), + vcs_commit: Some("0123456789abcdef0123456789abcdef01234567".to_string()), + download_url: String::new(), + published_at: "1970-01-01T00:00:00Z".to_string(), + yanked: false, + }, + artifact, + }) +} + +fn registry_responses( + address: SocketAddr, + packages: &mut [PublishedPackage], +) -> Result> { + let mut responses = HashMap::new(); + for package in packages { + let artifact_path = registry::artifact_path(&package.version.sha256); + package.version.download_url = format!("http://{address}{artifact_path}"); + responses.insert( + registry::package_path(ORG, &package.name), + Response { + content_type: "application/json", + body: Arc::new(serde_json::to_vec(&package.package)?), + artifact: false, + }, + ); + responses.insert( + registry::version_path(ORG, &package.name, VERSION), + Response { + content_type: "application/json", + body: Arc::new(serde_json::to_vec(&package.version)?), + artifact: false, + }, + ); + responses.insert( + artifact_path, + Response { + content_type: "application/octet-stream", + body: Arc::new(package.artifact.clone()), + artifact: true, + }, + ); + } + Ok(responses) +} + +fn write_consumer(project: &Path, packages: &[PublishedPackage]) -> Result<()> { + fs::create_dir_all(project)?; + let mut manifest = String::from( + r#"[package] +org = "recursive-consumer" +name = "wide-frontier" +version = "0.1.0" + +[package.repository] +vcs = "git" +url = "https://example.invalid/recursive-consumer/wide-frontier" + +[dependencies] +"#, + ); + for package in packages { + manifest.push_str(&format!( + "\"{ORG}/{}\" = \"={VERSION}\"\n", + package.name + )); + } + fs::write(project.join(".zpkg.toml"), manifest)?; + Ok(()) +} + +fn test_config(registry: &str, home: PathBuf) -> Config { + Config { + registry: registry.to_string(), + home, + token: None, + auth_url: "http://127.0.0.1/unused".to_string(), + supabase_url: None, + supabase_key: None, + interactive: false, + } +} + +#[test] +fn recursive_http_prefetch_saturates_at_five_and_warm_runs_do_not_redownload() -> Result<()> { + if let Ok(raw) = std::env::var("ZED_PKG_INSTALL_CONCURRENCY") { + assert_eq!( + raw, "5", + "this contract test expects the default five-worker setting or an explicit value of 5" + ); + } + + let temp = tempfile::tempdir()?; + let scratch = temp.path().join("scratch"); + let project = temp.path().join("project"); + let home = temp.path().join("home"); + let listener = TcpListener::bind("127.0.0.1:0")?; + let address = listener.local_addr()?; + + let mut packages = (0..PACKAGE_COUNT) + .map(|index| build_package(&scratch, &format!("pkg-{index:02}"))) + .collect::>>()?; + write_consumer(&project, &packages)?; + let server = RegistryServer::start(listener, registry_responses(address, &mut packages)?)?; + let config = test_config(&server.base_url(), home.clone()); + + let cold = prefetch(&project, &config, false)?; + assert_eq!(cold.resolved, PACKAGE_COUNT); + assert_eq!(cold.downloaded, PACKAGE_COUNT); + assert_eq!(server.artifact_requests(), PACKAGE_COUNT); + assert_eq!(server.max_active_artifacts(), EXPECTED_CONCURRENCY); + assert!( + !server.gate_timed_out(), + "five artifact handlers never became active together" + ); + assert!( + !server.state.handler_failed.load(Ordering::SeqCst), + "the fixture HTTP registry reported a handler failure" + ); + + let warm = prefetch(&project, &config, false)?; + assert_eq!(warm.resolved, PACKAGE_COUNT); + assert_eq!(warm.downloaded, 0); + assert_eq!( + server.artifact_requests(), + PACKAGE_COUNT, + "a warm content-addressed store must not fetch artifact bytes again" + ); + + let store = Store::new(&home); + for package in &packages { + assert!( + store.has(&package.version.sha256), + "missing store entry for {}/{}", + package.version.org, + package.version.name + ); + } + + let artifact_locks = fs::read_dir(home.join("locks"))? + .filter_map(|entry| entry.ok()) + .filter(|entry| { + entry + .file_name() + .to_string_lossy() + .starts_with("artifact-") + }) + .count(); + assert_eq!(artifact_locks, PACKAGE_COUNT); + + let cache_entries = fs::read_dir(home.join("cache"))? + .collect::>>()?; + assert_eq!(cache_entries.len(), PACKAGE_COUNT); + assert!( + cache_entries + .iter() + .all(|entry| entry.file_type().is_ok_and(|kind| kind.is_file())), + "atomic download staging directories must be removed" + ); + Ok(()) +} From bceed2a1f21f2515a30c4517f855f3f54fa5a584 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 02:05:21 -0500 Subject: [PATCH 25/80] style: format recursive concurrency integration test --- tests/recursive_install_concurrency.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/recursive_install_concurrency.rs b/tests/recursive_install_concurrency.rs index 9d0c4239..a8b2042a 100644 --- a/tests/recursive_install_concurrency.rs +++ b/tests/recursive_install_concurrency.rs @@ -346,10 +346,7 @@ url = "https://example.invalid/recursive-consumer/wide-frontier" "#, ); for package in packages { - manifest.push_str(&format!( - "\"{ORG}/{}\" = \"={VERSION}\"\n", - package.name - )); + manifest.push_str(&format!("\"{ORG}/{}\" = \"={VERSION}\"\n", package.name)); } fs::write(project.join(".zpkg.toml"), manifest)?; Ok(()) @@ -425,17 +422,11 @@ fn recursive_http_prefetch_saturates_at_five_and_warm_runs_do_not_redownload() - let artifact_locks = fs::read_dir(home.join("locks"))? .filter_map(|entry| entry.ok()) - .filter(|entry| { - entry - .file_name() - .to_string_lossy() - .starts_with("artifact-") - }) + .filter(|entry| entry.file_name().to_string_lossy().starts_with("artifact-")) .count(); assert_eq!(artifact_locks, PACKAGE_COUNT); - let cache_entries = fs::read_dir(home.join("cache"))? - .collect::>>()?; + let cache_entries = fs::read_dir(home.join("cache"))?.collect::>>()?; assert_eq!(cache_entries.len(), PACKAGE_COUNT); assert!( cache_entries From 6cb56afa524b1322827e15edc8ba2616280c1c62 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 10:27:42 -0500 Subject: [PATCH 26/80] chore: align recursive branch with bootstrap cleanup --- .github/workflows/apply-den-1420-mise.yml | 112 ---------------------- 1 file changed, 112 deletions(-) delete mode 100644 .github/workflows/apply-den-1420-mise.yml diff --git a/.github/workflows/apply-den-1420-mise.yml b/.github/workflows/apply-den-1420-mise.yml deleted file mode 100644 index fae8ceb5..00000000 --- a/.github/workflows/apply-den-1420-mise.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: apply DEN-1420 mise adapter - -on: - push: - branches: - - feat/den-1420-mise-dev-adapter - -permissions: - contents: write - -jobs: - apply: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: dtolnay/rust-toolchain@stable - - - name: Apply reviewed mise adapter patch - shell: bash - run: | - set -euo pipefail - base64 -d <<'PATCH_GZ' | gzip -d > "$RUNNER_TEMP/den-1420.patch" - H4sIAAMqcGoC/+08a3PbtrLf9SsQpjeWKpKybOclNzl1HCX1aWJ7bCc9bZqhKAqyGFOkyocdNc5/v7sLgAQfkpW2d+bOneuZ2CKx - WCz2vQsoE386ZZZ16afM7SWx15vwaztO2Fh7aFmWVRptdbvdMsSPPzJr56H5kHXh9yP2448t5kVhkrKXw1cH795cOO+Hx+8H7EGS - xuwZM+w/+QSn9hbLdBaFvWseXhv7as7x0X+cs+Hw+OLsV6c07bfhS+fl8L2DAAeHF0fvhzCpKya9PTofrp9FEPm0nL73zuEJTIKB - fIofekE24Q483Wsbtt2zgVbLC3xrGriXiZ1G88DoAI4Wu/9hwmP/mrdf8nF2abLDIAo5/IkWS5OdunHqu8HwD5Phv/dukPFhmM07 - H4lhu7vAqu7untkXHMOfXq/Hjvk1j1nMLR6m8CGdxVF2OWOj0P/MgBAeRIuRLcAJ1CymnvE/Mj/mzGUhd+PxkgHBV/AYTuDVTRRf - +eElIRox/pl7WeqOA263ujhfzp2Yre7XVrfV/atb6y6yMePwmb3k12/9hL+NJpx9EYsgkYfRfBElnN346Ywt4ugT91IriDw3YHMA - R8lM/cssdlM/CtnNjIfivZ8w99r1A43mgyyNzALzKs7RdNzwSM6TfOtW+XYnNRVOIkSZlazMSvYVtASF/egJCvvxU7O/nQv7/gc3 - vmwDW4G718hBB9lmAvOuK+pumCD5qZsFqUOAOOzC3g3gN6FCnoNYB8jzY/8zshwWLzb4y4ynM2RJBBtayX7aD6n3iEUxG9nFs2Tc - hiSjra2juatoxgUGuqKAUAp1fim0fQ7iREqnfsD32cj1R8ydTBKQL2fRIrX8kB0cwdaigCUzf84mwHsvjeKl/U18Pj07eXX0ppFu - +aLEbkkQUX8qPpvCEfafmHvgCcG+H+ey9qcs4Ck7j+a87cE+O4B27i7H3Ik5aasjtdUBMbYfxFGUmsybAsUPYIugeknnX8qI8Cfm - aRaH7ORKYNsXI1/Fn80XQ/6vXo2tXI2J1YRuP9x5iht+uPfYLDwZ/tjId8OyYEtGp/o6RBuE190qNNJUe48vnTks7oQu7EpSSQra - 6dRXlMKprSrfl5HIl4CHdvMUQ1j3Uf+h2X/yMN/PMXg/Yc7dabiOm4JwZCkElFM3nUk/A+yFF4fkTuQrSQC8lpp+EF8mMNZh1nNw - Igmo3A8nBPODv7vz/LlSAJCvzgD27JluQoMBubeCgbe3qOqDwbUbO1HSrobKju0nToK60lk5x3BE/Hx59OqVUZvQqJfIME0vxQdN - LZEPpJgoWfHYRq51GA8SruO8a7vK3epz8GcMweJeu/wOf6SOAa1y3o2b0ANPUnDabJylLIw29400fxplEBv8MPEh2pFnugkxRngz - 7l1FgHEKs758Ncw6Pbhre+Ini8Bd6jLAH8VBzbrXsHlf8RlYdg/ov4zduZMHzbYhTOuf4+0mnJRBEqJ3GKVFBGcQTU8PLn4yVuyQ - L2I/TIOwKkADckeVBg0k1798rS6VhflC+xjCUz/MUBoU9FA6IcTza85+A2Sg534chRhlqtIRatkkG53qDXQedOgKfYzSd/XcfiAN - Yb9m2dM4+hNSnwcP2D0FjXZHs0oibFJzEIycTzv+8lWJSERNd/LJ9TCqkvRy4urqWWVAZTgnbA2HSnwoUiXgBDkYL4tjoMSBkXbH - RlHxz2nbQKNLUWJIbpyFZEso+gKD0fnXfoF5DvKH1GaOGdozzDHx02AQ8hul9hJYAlXjDuJtika1dwUBNWiplk1oRAjs/p0QuBq8 - iHbdb4l2dTxUjllUjtVw0ZiDYw1xWBttQtyIUaUaNs3SBuGxFqRMZvSNKhClbqc/vwbA10fnAAm52wOIs3bML32o3pYrJ/x08nao - gGcQj1YCHry7+Ml5d/ZGAUMGO3OyONAnKBWGvFNEsH1d5cfootx4qUxfhhVHvc+n1DI37geg9RgjFay9cHGlsv1LhRZ0E98Oh0dv - jo5fO+hfz4FyhalqkXVvo6FFOoT548aACApTnyI/bBcFPO6oJ6BKjhwnJ0sIAnNnMxwC2GpANU3AScTcTTnicNwgUE4Tnyvh0kZ3 - 5ygvAkkMRN25m0KkIhToRPwkCgDZRC/vyPNpuy38WSd3MqtoqW2zUyOiEjsVTQ2ZyQoqxRo6sfW5NTpWphRavC3tTpM4egg0cBCZ - eFNhc+FWqgLIPXhD9byAbJjNXMwEGAUcxGCUaKh558IkSbXfnBz+PHxpVN1BBerw5PjV0WvIVs9KYl094fzX84vh2/K8umBXTj95 - Pzw7O3qZL4x15PHB2+F5sbzi6AZILk5O3jjwcH50cnxewmWEkGAYTXace400uuIh+gxyV/S00lkoJ3dx8vPwGNCLuatrSfR7CnOS - LdyxC/4MXt65wPm704MXB7BH4UkRz+pVrviyvgq83HyVn4cYCRDPOk4lMx6I/SgnSG+alpFhjMah/MFHMXv1LkRELKEXr9bgFwBy - AYmgtoLCFkYOVBupu5biMLIkkLEaU933V7AIgDUYAH9044wzP5isQUNQFkGtwaU8n4NxeQ02BWch3Bp8VEc42OtZw3iEsShJWS3R - xIv9RVqSqEo4V2L2lLaIuU0pMUgnzRJytGKmeNFeG0aaQ4hB7QgMH+R8J1r7TCtyGIhzvkGur6XykuzcU0OpI1hCpNrYFAJ6s/Am - dhdOFLf7HcwCaZvTsFbhy74IdTlkewOfX2TTvMPxzYkTVtcecM+fgE5AGS4yDTf0oBKN4qScMSFwHuEA9oORF/PoZIvS3vhYjd1F - jESJqQVFSpO7+P3yHFAiydvG+q1SRurtkQqir42VMqLPd/6sSBZrJTvkFlf1Ylv+of5WWWSNDNeFJ6WmFqqyXEuRp344ad/mZNZS - og+GfemnxPzZJf359Mn4WOeR7YOCV3MOUVGEy/bt3I2veHxbFYx43bH5Z6gKgLBSBqRRWSgwqZY2kkYOJi/g4qbtqmbntbwQ2hoG - ofLkzemG1CpP70ucC3DLt7j8LbuHf2w3caIEj6fa1Irj80W6LO2q2ImDzTR0HkiKLIdto6M0q2iVi6ScNB83ZFR3WdR8+KnUs6et - PthCZ+B7DM/Q5H7BRXkzNtfOf/Cn1F/CExz27Lk8GjBXQImDHQQTNfAquLxbhaCqKaWgv4odMdhRqTBuaOQ37YgVO5Iz9A55MXcw - eClOC4gIdXIgjgb620+28ZS0v/P4aaVXrsWOARmjWR6jfKNxpHLuI7lVce61g5YcrISrgRf5fqrL5vnHgKVxJg8/+v2dbTwM6O/C - 3/oOZW8KuyFmw0hNupWWbeOQJuemtVRzpGnU9ZVg+rvb5i6SvbsD5FfJRsOVLfq8sySf2xDeE0wKvMBX+cB+ebabJDyGFOSPewRs - w+7NusCqzr46C/dvNqt7544FJQvMklQPfJhGe9/Z2aG97+08NJ80isy+5KkDqDLMH9Czs2b/CyC3DH4RPB64QaSDiEQhzaAWmDZV - 37CguJrPyASmwdtX6en+BXpEk63csRT8eEznWf29J4/N3eIID89+5HliCiFORqdyfgNO2V0mjjgGcNIZd8QxgHPtJY46CiiHf9Qt - qDMXoEL4RwgHP2E7SSVV7WpzJT9WELNsjAsALby4Gqy1ZELqy6OySgg5wV0skt4NH9/VeKlMo5jdaaSxqVMiVl8JfhNDcG/Xd1Nk - Yx1IDD7gOW/y8XcskzREVVUi5dcTT7W6yVSTvmn16gY3Wrth5brzrdFRBhFZ3+rVK431DWj/q6Q3M62JPp28WmnTaCQqVXIga1Fn - EA5k45BiilKHTGgBkQ5sppK0r+dunoRpeU7MF1FPJ7LCc5mdDQZYEOngKgFa7am+mQz7n6SjxmSmmOyG7iWUzlq151xxvkicwA0v - MxhDBwU2CQk9j51JlDp/8gnxeaLfBGu48MTGTW/l3bCmEXFJrGkEnezjPXSx8LvfJwc7UTmTzARbDPId9IkGREl5Z8WnJqJIPxHI - ZJQvmHgeq5IA28AT+g8yECe2DCG2WB65CirZdFul1XUD3004VuKiHsT6I10u6AIIpICgkQBUo7Or6JT9zm8gFHLRVYTKiP2xLJZJ - 5CU9BTifAHsrb6Q4qm+FKKpvRayjyI+/UQoXM54fovkJCCXwxzwGhQmWzGXff+/FUZJYSpdKTQUviuKJH7pQTHz/vSnOelsW6HIA - Vi7aDrD7QwjGyI7F3GQL+p1dm+x1hMzKAp6Y7FWQpSny6hizJJjyG59sYcc4nrugbgvXu8KlY55EAfDUbnX/xhoipyqt1OqKpVh9 - Jbrcs40a28U/Ik3csdlRyEaU05KwRyR4s3zPio6cOVR3ScpGmAD26GaeTVfxopgMeKS9Gi/za2ziELTpDiB7B+jlyqRgI7zfhRkI - 4ZNH3HhZQcAo1SOwqesHkLMChRO7Ze0CshB4kxBs3jSks/QFUA2UmMgJkC5XnElysmh3gmvAw1YXsB1M8QYeJGHIXbmXeGkiejIU - wSnJo+SOy2jk7KqXLppu9ymO0KuCJePlAty2aIpF4mLMqHJzgeDkA8NopKhpuACI2y1dn9z7J7knyszQnwJ8Lz+alyhBDdlBPkxL - IgguJPFPmGznIqVh1BJHQyEsgqXsyP5zcXUpOQiTMHNIeQgaAHs44xlKJmTUJ6ELjnQ2OyLrENkcYqhf3h2R4bmgmimwDFXg4T+K - jqKk6J5LsdjsMAKNigJgny/1Z1Q6wSYVuyUluM2lLIrAHBBIQTaNxAn1CNgAdL+gNjZKRYbTkp9DeO7TZUpxy9QFzoPofHxwPTI5 - qtUhyD/6B5FRgy1DT+P1LEs6acFKPofo8O/zk+OCDaqfPRJe6xG5q+6jXXOXHL11/z7ZJa6PLaDwutUVr5RP1Eag6im5N0wexTWS - yu0Z/faTcnbk07YwlFyLndgyysyQL354Tfc8QK4gRGrTTWhpcBi4WbWfwsOZaFAgLNSnFoB5WZzAdJsNlZLhvq7d2EfbTKD4Q9sB - BkOGPNmXwYR/XgS+B0yTMmkxFdJ6ngtJbmV+ileQI1g9cP0JqWaa70DtqWWNtItJIzaJuLjwpHyKYKbgqxpLoiz20AZgJPbAINOE - B1Ow8JY4amJuAOowWQrl4JPcZUhk6BL1iCBitrAZBQuEHR3Tlfnzn4Zv3oyYjO2CNktitqEqArpSxCpaBeISPDIULG6SeSQkVAcL - xyGI+uh63AkKDNRijp0ycFBLcIdlHx9E0VXCohBSCGTd5rfq6JgCON3qAlfEnpRPzhboroFd7LUPrvUtj0EPfDcgc/h39ilLk6wo - kfPbE+woxXRBbDppiCCQaTFNjMy27ZFM3nJoV8SR1E2uTJAg5R+Y8mFLF1/SoXaCLWjBYtRQCCkueukQqFZ6AtFKMwSl/ZCG1tVf - D2fSXxUWYWMxgEoPODjeWKZrA1M/TsBJ/oKqi04lJxFdS4jCRGEBHV6AhkohzyS9BlStrqCKYld8LS+N5ff6BAOorYvUqMv5gbsk - JdW8AOHSPEQ9ZWGarhRuOjcyoGDKY8QaglsndARObXDc+C+SRSLojUxWS8ObL76N8gJrhBfBydgF7i0RUnGblMf5lxCccXBkYw0N - kYI8TiJjB9BGPXSZXlwG0Rj0GkdKlzZU/kAqQA37VrdsCqoV73O8vgjlmRYb6V4j6otIKND/ARDIxhLrgRJmINQ5MAa4LyzHm4FP - Q/t184zgEneOGfIRhZKbGWYWAXevEUyk7/ESE+BQ3R6UhgDE6CzFz7AdvMVHKgqpCRqYVIUxnwHGKCb5HLBmTwYJrpa4NTkuJoWh - qQ+UX1gOieOcVY5sYz8M2O50xGxDPwzacZcjZpv6YaRrA0fM7vLDELdPQaX4hIceGuhAXtd/TF/UevrYfKgKmCJBUyXx9yPWE094 - sQKe9DBPX4tI9lsM8nwqV60dzDXoni1mLaqYTyguHhXuIFm4N6Fw4x6krmaeg+QRWvK7iL8xn7t+WFwbNlsWhWNMPURIXgrLhuqW - usFFlJB2Rd8LyRGCUt5BkJ4B3UUXeOucMPY36RLl+CPRiX8iy0vIyu4TPRYGkMINah4drWw0GiUz6RplpdDqSjtgDd42v69teWyL - 3IJF0XKLUCFGce2M4q7YBzgW5JT0M4mpa4RgFnUYKcVStQg5TSo4lKP9LMo9tFFHluM9e+yrRDy6CeVq5S9iiUqr1c07D5SmJTZ7 - Rd6Mim1MgjWvLr6qNZlowQEdErsv8lzFT42PmOcSH0kSe09JEk/UcdAtIiItGOFnZSjSmG8lABXv2jDYOw52b1UYL41i46mYK3s+ - JQj5nSQEKjfo4M8sG/fwm2jTILrJGzuCRHspGnZ3Q6kG3gaQsqG3ASQxkE7TqNgQzYg5H7AXbgJZxm/465WPv2VRh9WXSS2JLEbd - QtmdXvzKknl0xUWrcwUCYasboUGq9nZRqvC7fsZ3CarArD8wBdwqFZJbzPgOm3zGOvjSbaRiRnf1DApvG6D2TJbXewW8PLSkc6r+ - nvmkftSKpo0Xcz5vsd/LI7f5AkmajS0CUl8W1HrZYJbPjO/O3h0fD8+ci+Hb055wRH5Y2tf8Cq/2WgsgTU0rjUN9x55rg4TFYD/8 - sIXqv6WD3r/Xy5K4h0AYT8YuOjXtCJynzOJZBL5owVHK+qA/ZR8+wDJf+gPrq4GnfHTvnHI1g338uE/+uSwQKpOnbOu/kt/DLbZF - Pm9ne+eR/cTetpA3W2V4iJMp29bfTX39CZvxGgmU25Q2MPOn6boJlrUReMNXmiWCfiNLCG74n9Ph4YXz6uzkt+HxwNoWTOqvYI1a - Srt027SEBLTCHLa4Qovw62Frt25XTFHwK6/ZStLy4nETHCtu2UpUIV2HWiVojGOx/NL5+cW7F1IIz/plICgoje9+LO0Ip5SsYwZh - kHU/1wxEP+NjpftYTUYph9YaZhNMxQK+fN0qbFXC92TD0v6UROG6yfKIET6FWBqCAe7s2Hv2ttGAszgNKzFjso5W/DbDs4JNg+/o - 62MwA83hxdGxQdnO72XZ6/3YhrE8MaoNlfOk2rDeaFyBQaSMxndtdaJiCZfWMWqw4KqlklZViixuX6lw0/9lIACkLEgZoqut6gqF - y5cQZfUCT1H3Ec/6/xMsl4Xl/2JR4GUm0tialWWh6AzYeLeC7ayH4XFcUt+6j+XeLGJbkiGiGNaqrCwEL0MVJB6+eR5fYA1B3adE - XfGlVBfM68FOQ6Dqr/ZfShm2NvhW4NYGu9RxD4B3OW1V1/f/Gra5sevfd6kbudjlBraeA5bFFEbOmqAC/N0krlSQbBJaKlNWRxcK - BusWAKf1fzokrPZD+R2CNX4oh9nUD+Ub29ATKT9UPif9O87oW75zv7XBnvWqR35BtVnbpeRyJftvfe+pTBZJAAA= - PATCH_GZ - printf '%s %s\n' 'd0d0a867d249556a41322169483f27f9107a58d4131a2a92270e16d4967623bc' "$RUNNER_TEMP/den-1420.patch" | sha256sum -c - - git apply --check "$RUNNER_TEMP/den-1420.patch" - git apply "$RUNNER_TEMP/den-1420.patch" - rm .github/workflows/apply-den-1420-mise.yml - cargo fmt --all - git diff --check - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -A - git commit -m 'feat(DEN-1420): compose zed dev with mise' - git push origin "HEAD:${GITHUB_REF_NAME}" From 51346b76fec473d72b779a7d334f7c32fbb5608a Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 10:27:50 -0500 Subject: [PATCH 27/80] chore: align recursive branch with bootstrap cleanup --- .github/workflows/kick-den-1420-mise.yml | 125 ----------------------- 1 file changed, 125 deletions(-) delete mode 100644 .github/workflows/kick-den-1420-mise.yml diff --git a/.github/workflows/kick-den-1420-mise.yml b/.github/workflows/kick-den-1420-mise.yml deleted file mode 100644 index b6a2ba74..00000000 --- a/.github/workflows/kick-den-1420-mise.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: kick DEN-1420 mise adapter - -on: - pull_request: - types: [opened, reopened, synchronize] - -permissions: - contents: write - -jobs: - apply: - if: github.event.pull_request.head.ref == 'feat/den-1420-mise-dev-adapter' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: feat/den-1420-mise-dev-adapter - fetch-depth: 0 - - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt - - - name: Extract, verify, and apply reviewed patch - shell: bash - run: | - set -euo pipefail - python3 - <<'PY' - import base64 - import gzip - from pathlib import Path - - source = Path('.github/workflows/apply-den-1420-mise.yml').read_text(encoding='utf-8') - lines = source.splitlines() - start = next(i for i, line in enumerate(lines) if "base64 -d <<'PATCH_GZ'" in line) + 1 - end = next(i for i in range(start, len(lines)) if lines[i].strip() == 'PATCH_GZ') - payload = ''.join(line.strip() for line in lines[start:end]) - Path('/tmp/den-1420.patch').write_bytes(gzip.decompress(base64.b64decode(payload))) - PY - - printf '%s %s\n' 'd0d0a867d249556a41322169483f27f9107a58d4131a2a92270e16d4967623bc' /tmp/den-1420.patch | sha256sum -c - - patch --batch --forward --fuzz=3 -p1 < /tmp/den-1420.patch - - # One low-context assertion hunk can land beside the contract-parity - # test under GNU patch fuzz. Relocate it structurally, then verify - # every product file against the reviewed pre-format blob. - python3 - <<'PY' - from pathlib import Path - - path = Path('src/dev.rs') - lines = path.read_text(encoding='utf-8').splitlines(keepends=True) - - def assertion_bounds(index: int) -> tuple[int, int]: - start = index - while start >= 0 and lines[start].strip() != 'assert!(': - start -= 1 - if start < 0: - raise SystemExit('could not locate assertion start') - end = index - while end < len(lines) and lines[end].strip() != ');': - end += 1 - if end == len(lines): - raise SystemExit('could not locate assertion end') - return start, end + 1 - - help_index = next( - i for i, line in enumerate(lines) - if 'fn augmented_root_help_contains_the_command_and_alias()' in line - ) - - misplaced = [ - i for i, line in enumerate(lines[:help_index]) - if 'Some("mise")' in line - ] - for index in reversed(misplaced): - start, end = assertion_bounds(index) - del lines[start:end] - - help_index = next( - i for i, line in enumerate(lines) - if 'fn augmented_root_help_contains_the_command_and_alias()' in line - ) - next_test = next( - (i for i in range(help_index + 1, len(lines)) if lines[i].strip() == '#[test]'), - len(lines), - ) - if not any('Some("mise")' in line for line in lines[help_index:next_test]): - nix_index = next( - i for i in range(help_index, next_test) - if 'Some("nix")' in lines[i] - ) - _, insert_at = assertion_bounds(nix_index) - lines[insert_at:insert_at] = [ - ' assert!(\n', - ' develop\n', - ' .get_arguments()\n', - ' .any(|arg| arg.get_long() == Some("mise"))\n', - ' );\n', - ] - - path.write_text(''.join(lines), encoding='utf-8') - PY - - if find . -name '*.rej' -print -quit | grep -q .; then - echo 'patch left reject files:' >&2 - find . -name '*.rej' -print -exec cat {} \; >&2 - exit 1 - fi - - test "$(git hash-object src/dev.rs)" = 'ad55ef52dc4703afa7025bcba3c60de5d444cca1' - test "$(git hash-object .dev-cli-flags.toml)" = 'd4b6a979f3274a87370ef518d58a49705fcdc3af' - test "$(git hash-object docs/develop.md)" = 'a53eb8e6f35aaf84465149d1146d65166e6d12ff' - test "$(git hash-object .github/workflows/develop-shell.yml)" = '14c23135d931e20423084be1a698d67ac155e15e' - - rm .github/workflows/apply-den-1420-mise.yml - rm .github/workflows/kick-den-1420-mise.yml - cargo fmt --all - git diff --check - cargo test --locked - - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -A - git commit -m 'feat(DEN-1420): compose zed dev with mise' - git push origin HEAD:feat/den-1420-mise-dev-adapter From 2c158298da78aecaf8fb88742692809f2dca52f4 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 10:27:58 -0500 Subject: [PATCH 28/80] chore: align recursive branch with bootstrap cleanup --- .github/den1420_post_patch.py | 254 ---------------------------------- 1 file changed, 254 deletions(-) delete mode 100644 .github/den1420_post_patch.py diff --git a/.github/den1420_post_patch.py b/.github/den1420_post_patch.py deleted file mode 100644 index 07bcab35..00000000 --- a/.github/den1420_post_patch.py +++ /dev/null @@ -1,254 +0,0 @@ -from pathlib import Path - -path = Path('src/mise.rs') -text = path.read_text() - -old = '''#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -struct MiseConfig { - #[serde(default)] - tools: BTreeMap, - #[serde(default)] - env: BTreeMap, - #[serde(default)] - tasks: BTreeMap, - #[serde(default)] - settings: BTreeMap, -} -''' -new = '''#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -struct MiseConfig { - #[serde(default)] - tools: BTreeMap, - #[serde(default)] - env: BTreeMap, - #[serde(default)] - vars: BTreeMap, - #[serde(default)] - tasks: BTreeMap, - #[serde(default)] - settings: BTreeMap, - /// Manager-native sections that Zed does not execute yet. They remain - /// lossless and inert instead of being silently discarded. - #[serde(flatten, default)] - unknown: BTreeMap, -} -''' -assert old in text, 'MiseConfig block not found' -text = text.replace(old, new, 1) - -old = ''' Self::Versions(values) => { - values.sort(); - values.dedup(); - } - Self::Options(options) => { - if let Some(version) = &mut options.version { - let mut values = version.as_vec(); - values.sort(); - values.dedup(); - options.version = Some(OneOrManyString::Many(values)); - } -''' -new = ''' Self::Versions(values) => dedup_preserving_order(values), - Self::Options(options) => { - if let Some(version) = &mut options.version { - let mut values = version.as_vec(); - dedup_preserving_order(&mut values); - options.version = Some(OneOrManyString::Many(values)); - } -''' -assert old in text, 'version normalization block not found' -text = text.replace(old, new, 1) - -old = ''' plan.extensions.insert( - "mise.env".to_string(), - serde_json::to_value(&config.env).context("failed to encode mise env")?, - ); - plan.extensions.insert( - "mise.tasks".to_string(), - serde_json::to_value(&config.tasks).context("failed to encode mise tasks")?, - ); -''' -new = ''' plan.extensions.insert( - "mise.env".to_string(), - serde_json::to_value(&config.env).context("failed to encode mise env")?, - ); - plan.extensions.insert( - "mise.vars".to_string(), - serde_json::to_value(&config.vars).context("failed to encode mise vars")?, - ); - plan.extensions.insert( - "mise.tasks".to_string(), - serde_json::to_value(&config.tasks).context("failed to encode mise tasks")?, - ); -''' -assert old in text, 'env/tasks extension block not found' -text = text.replace(old, new, 1) - -old = ''' plan.extensions.insert( - "mise.settings".to_string(), - serde_json::to_value(&config.settings).context("failed to encode mise settings")?, - ); - plan.extensions.insert( - "mise.source_sha256".to_string(), -''' -new = ''' plan.extensions.insert( - "mise.settings".to_string(), - serde_json::to_value(&config.settings).context("failed to encode mise settings")?, - ); - plan.extensions.insert( - "mise.unknown_top_level".to_string(), - serde_json::to_value(&config.unknown) - .context("failed to encode unsupported mise sections")?, - ); - plan.extensions.insert( - "mise.source_sha256".to_string(), -''' -assert old in text, 'settings/source extension block not found' -text = text.replace(old, new, 1) - -old = '''fn ensure_project_local_path(path: &str, field: &str) -> Result<()> { - let path = path.trim(); - ensure!(!path.is_empty(), "{field} cannot be empty"); - let parsed = Path::new(path); - ensure!(!parsed.is_absolute(), "{field} must be project-relative"); - ensure!( - parsed - .components() - .all(|component| !matches!(component, Component::ParentDir)), - "{field} cannot escape the project root" - ); - Ok(()) -} -''' -new = '''fn ensure_project_local_path(path: &str, field: &str) -> Result<()> { - let path = path.trim(); - ensure!(!path.is_empty(), "{field} cannot be empty"); - let parsed = Path::new(path); - let has_windows_drive = path.as_bytes().get(1).is_some_and(|value| *value == b':') - && path - .as_bytes() - .first() - .is_some_and(|value| value.is_ascii_alphabetic()); - let has_parent = path.split(['/', '\\']).any(|part| part == ".."); - ensure!( - !parsed.is_absolute() - && !has_windows_drive - && !path.starts_with('~') - && !path.starts_with("$HOME") - && !path.starts_with("${HOME}") - && !path.starts_with("%USERPROFILE%") - && !path.starts_with("\\\\") - && !path.starts_with("//"), - "{field} must be project-relative on every supported platform" - ); - ensure!(!has_parent, "{field} cannot escape the project root"); - ensure!( - parsed - .components() - .all(|component| !matches!(component, Component::ParentDir)), - "{field} cannot escape the project root" - ); - Ok(()) -} - -fn dedup_preserving_order(values: &mut Vec) { - let mut seen = BTreeSet::new(); - values.retain(|value| seen.insert(value.clone())); -} -''' -assert old in text, 'path validation block not found' -text = text.replace(old, new, 1) -path.write_text(text) - -test_path = Path('tests/mise_adapter.rs') -tests = test_path.read_text() -insert = r''' - -#[test] -fn multi_version_order_is_semantic_and_preserved() { - let root = temp_project("version-order"); - write( - &root.join(".mise.toml"), - r#" -[tools] -node = ["22", "20", "22"] -"#, - ); - - let imported = import_mise(MiseAdapter::discover(&root).unwrap()).unwrap(); - assert!(imported.source_document.contains("[\"22\", \"20\"]")); - assert!(!imported.source_document.contains("[\"20\", \"22\"]")); - fs::remove_dir_all(root).unwrap(); -} - -#[test] -fn unknown_top_level_sections_remain_lossless_and_inert() { - let root = temp_project("unknown-sections"); - write( - &root.join(".mise.toml"), - r#" -[tools] -node = "22" - -[hooks] -postinstall = "echo never-run-by-import" - -[plugins] -custom = "https://example.invalid/plugin.git" -"#, - ); - - let imported = import_mise(MiseAdapter::discover(&root).unwrap()).unwrap(); - let unknown = imported - .plan - .extensions - .get("mise.unknown_top_level") - .expect("unknown sections are represented"); - assert_eq!(unknown["hooks"]["postinstall"], "echo never-run-by-import"); - assert_eq!( - unknown["plugins"]["custom"], - "https://example.invalid/plugin.git" - ); - assert!(imported.source_document.contains("postinstall")); - fs::remove_dir_all(root).unwrap(); -} - -#[test] -fn vars_are_preserved_for_future_native_template_evaluation() { - let root = temp_project("vars"); - write( - &root.join(".mise.toml"), - r#" -[vars] -channel = "stable" -retries = 3 -"#, - ); - - let imported = import_mise(MiseAdapter::discover(&root).unwrap()).unwrap(); - let vars = imported.plan.extensions.get("mise.vars").unwrap(); - assert_eq!(vars["channel"], "stable"); - assert_eq!(vars["retries"], 3); - fs::remove_dir_all(root).unwrap(); -} - -#[test] -fn portable_path_validation_rejects_windows_and_unc_absolute_paths() { - let root = temp_project("portable-paths"); - for path in [r"C:\Users\alex\mise.toml", r"\\server\share\mise.toml"] { - let adapter = MiseAdapter::for_source(&root, path).unwrap(); - let error = import_mise(adapter).unwrap_err().to_string(); - assert!(error.contains("project-relative")); - } - fs::remove_dir_all(root).unwrap(); -} -''' -assert 'fn multi_version_order_is_semantic_and_preserved' not in tests -test_path.write_text(tests + insert) - -doc_path = Path('docs/mise.md') -doc = doc_path.read_text() -needle = '- Project-local config only; global/user configuration is ignored.\n' -replacement = '''- Project-local config only; global/user configuration is ignored.\n- Unknown top-level mise sections are preserved losslessly under `mise.unknown_top_level` and remain inert; import never executes hooks or plugins.\n- Multi-version order is preserved because PATH/default selection can be order-sensitive.\n''' -assert needle in doc -doc_path.write_text(doc.replace(needle, replacement, 1)) From c1421f609354bd0f070783597e67c4a23e9d5d97 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 10:31:07 -0500 Subject: [PATCH 29/80] test: satisfy clippy in recursive install fixtures --- src/install_graph/tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/install_graph/tests.rs b/src/install_graph/tests.rs index acabd85d..0a025fa5 100644 --- a/src/install_graph/tests.rs +++ b/src/install_graph/tests.rs @@ -84,7 +84,7 @@ fn publish_fixture( version: version.to_string(), sha256: packed.sha256.clone(), size: packed.size, - format: packed.format.clone(), + format: packed.format, vcs_tag: format!("v{version}"), vcs_commit: Some("0123456789abcdef0123456789abcdef01234567".to_string()), download_url: format!("file://{}/{}.tar.gz", artifacts.display(), packed.sha256), @@ -147,7 +147,6 @@ fn recursively_prefetches_packages_of_packages_and_deduplicates_diamonds() { let home = temp.path().join("home"); let project = temp.path().join("project"); fs::create_dir_all(&project).unwrap(); - let leaf = publish_fixture(®istry, &scratch, "test", "leaf", "1.0.0", &[]); let left = publish_fixture( ®istry, From 08fd56f4d1be03ac5cce2b6eb6b4f1a35fb1de7f Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 10:43:33 -0500 Subject: [PATCH 30/80] chore: stage shared artifact acquisition patch --- .github/unify_artifact_acquisition.py | 327 ++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 .github/unify_artifact_acquisition.py diff --git a/.github/unify_artifact_acquisition.py b/.github/unify_artifact_acquisition.py new file mode 100644 index 00000000..77f315dd --- /dev/null +++ b/.github/unify_artifact_acquisition.py @@ -0,0 +1,327 @@ +from pathlib import Path + + +def replace_once(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text() + count = text.count(old) + if count != 1: + raise RuntimeError(f"expected one match in {path}, found {count}") + file.write_text(text.replace(old, new, 1)) + + +replace_once( + "src/store.rs", + ''' pub fn root(&self) -> PathBuf { + self.home.join("store") + } + + pub fn cache_dir(&self) -> PathBuf { +''', + ''' pub(crate) fn home(&self) -> &Path { + &self.home + } + + pub fn root(&self) -> PathBuf { + self.home.join("store") + } + + pub fn cache_dir(&self) -> PathBuf { +''', +) + +replace_once( + "src/install_graph/artifact.rs", + ''' prefetch_one(registry, &store, &home, task) +''', + ''' prefetch_one(registry, &store, task) +''', +) + +replace_once( + "src/install_graph/artifact.rs", + '''fn prefetch_one( + registry: &dyn Registry, + store: &Store, + home: &Path, + task: FetchTask, +) -> Result { + let (package_dir, downloaded) = ensure_artifact(registry, store, home, &task.version) +''', + '''fn prefetch_one( + registry: &dyn Registry, + store: &Store, + task: FetchTask, +) -> Result { + let (package_dir, downloaded) = ensure_artifact(registry, store, &task.version) +''', +) + +replace_once( + "src/install_graph/artifact.rs", + '''fn ensure_artifact( + registry: &dyn Registry, + store: &Store, + home: &Path, + version: &VersionMetadata, +) -> Result<(PathBuf, bool)> { +''', + '''/// Acquire one immutable artifact through the shared cache/store path. +/// +/// Every caller uses the same blocking per-hash process lock, staged download, +/// integrity check, atomic cache publication, and extraction sequence. This is +/// intentionally crate-visible so the legacy transactional installer cannot +/// bypass recursive-prefetch locking. +pub(crate) fn ensure_artifact( + registry: &dyn Registry, + store: &Store, + version: &VersionMetadata, +) -> Result<(PathBuf, bool)> { +''', +) + +replace_once( + "src/install_graph/artifact.rs", + ''' let _artifact_lock = ArtifactProcessLock::acquire(home, &version.sha256)?; +''', + ''' let _artifact_lock = ArtifactProcessLock::acquire(store.home(), &version.sha256)?; +''', +) + +replace_once( + "src/install_graph.rs", + '''use artifact::worker_loop; +pub use resolver::prefetch; +''', + '''pub(crate) use artifact::ensure_artifact; +use artifact::worker_loop; +pub use resolver::prefetch; +''', +) + +replace_once( + "src/ops.rs", + '''fn ensure_artifact(reg: &dyn Registry, store: &Store, vm: &VersionMetadata) -> Result { + validate_version_metadata(vm)?; + if store.has(&vm.sha256) { + return Ok(store.pkg_dir(&vm.sha256)); + } + let cached = store.cached_artifact(&vm.sha256); + if !cached.exists() { + reg.download(vm, &cached)?; + } + store.add_artifact(&cached, &vm.sha256) +} +''', + '''fn ensure_artifact(reg: &dyn Registry, store: &Store, vm: &VersionMetadata) -> Result { + validate_version_metadata(vm)?; + crate::install_graph::ensure_artifact(reg, store, vm) + .map(|(package_dir, _downloaded)| package_dir) +} + +#[cfg(test)] +pub(crate) fn legacy_ensure_artifact_for_test( + reg: &dyn Registry, + store: &Store, + vm: &VersionMetadata, +) -> Result { + ensure_artifact(reg, store, vm) +} +''', +) + +replace_once( + "src/ops_entry.rs", + '''pub(crate) use implementation::{ + detect_adapter, detect_native_manifest_target, detect_structure_target, detect_target, +}; + +pub fn add(project: &Path, cfg: &Config, spec: &str) -> Result<()> { +''', + '''pub(crate) use implementation::{ + detect_adapter, detect_native_manifest_target, detect_structure_target, detect_target, +}; + +#[cfg(test)] +pub(crate) use implementation::legacy_ensure_artifact_for_test; + +pub fn add(project: &Path, cfg: &Config, spec: &str) -> Result<()> { +''', +) + +replace_once( + "src/install_graph/tests.rs", + '''use std::sync::{Arc, Barrier, mpsc as test_mpsc}; +''', + '''use std::sync::{ + Arc, Barrier, + atomic::{AtomicUsize, Ordering}, + mpsc as test_mpsc, +}; +''', +) + +path = Path("src/install_graph/tests.rs") +text = path.read_text() +if "legacy_and_recursive_acquisition_paths_share_one_download_lock" in text: + raise RuntimeError("mixed-path lock test already exists") + +marker = '''#[test] +fn defaults_to_five_workers_and_bounds_overrides() { +''' +if text.count(marker) != 1: + raise RuntimeError("could not locate first install-graph test") + +block = r'''struct CountingRegistry { + artifact: PathBuf, + downloads: Arc, + started: Option>, + release: Option>, +} + +impl Registry for CountingRegistry { + fn get_package(&self, _org: &str, _name: &str) -> Result { + unreachable!("mixed-path acquisition test does not resolve package metadata") + } + + fn get_version( + &self, + _org: &str, + _name: &str, + _version: &str, + ) -> Result { + unreachable!("mixed-path acquisition test receives resolved version metadata") + } + + fn download(&self, _version: &VersionMetadata, dest: &Path) -> Result<()> { + self.downloads.fetch_add(1, Ordering::SeqCst); + if let Some(started) = &self.started { + started.send(()).unwrap(); + } + if let Some(release) = &self.release { + release.recv_timeout(Duration::from_secs(5)).unwrap(); + } + fs::create_dir_all(dest.parent().unwrap())?; + fs::copy(&self.artifact, dest)?; + Ok(()) + } + + fn publish( + &self, + _meta: &zed_interfaces::registry::PublishMeta, + _artifact: &Path, + _token: Option<&str>, + ) -> Result { + unreachable!("mixed-path acquisition test does not publish") + } + + fn claim_org( + &self, + _slug: &str, + _token: Option<&str>, + ) -> Result { + unreachable!("mixed-path acquisition test does not claim organizations") + } + + fn search(&self, _query: &str) -> Result { + unreachable!("mixed-path acquisition test does not search") + } + + fn yank( + &self, + _org: &str, + _name: &str, + _version: &str, + _yanked: bool, + _token: Option<&str>, + ) -> Result { + unreachable!("mixed-path acquisition test does not yank") + } + + fn audit_log( + &self, + _org: &str, + _limit: Option, + _token: Option<&str>, + ) -> Result { + unreachable!("mixed-path acquisition test does not read audit logs") + } +} + +#[test] +fn legacy_and_recursive_acquisition_paths_share_one_download_lock() { + let temp = tempfile::tempdir().unwrap(); + let registry_root = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + + let sha = publish_fixture( + ®istry_root, + &scratch, + "test", + "shared", + "1.0.0", + &[], + ); + let version = published_version(®istry_root, "test", "shared", "1.0.0"); + let artifact = registry_root + .join("artifacts") + .join(format!("{sha}.tar.gz")); + let downloads = Arc::new(AtomicUsize::new(0)); + let (recursive_started_tx, recursive_started_rx) = test_mpsc::channel(); + let (recursive_release_tx, recursive_release_rx) = test_mpsc::channel(); + let (legacy_started_tx, legacy_started_rx) = test_mpsc::channel(); + + let recursive_registry = CountingRegistry { + artifact: artifact.clone(), + downloads: Arc::clone(&downloads), + started: Some(recursive_started_tx), + release: Some(recursive_release_rx), + }; + let legacy_registry = CountingRegistry { + artifact, + downloads: Arc::clone(&downloads), + started: Some(legacy_started_tx), + release: None, + }; + let recursive_store = Store::new(&home); + let legacy_store = Store::new(&home); + let recursive_version = version.clone(); + let legacy_version = version; + + let recursive = thread::spawn(move || { + super::ensure_artifact(&recursive_registry, &recursive_store, &recursive_version).unwrap() + }); + recursive_started_rx + .recv_timeout(Duration::from_secs(2)) + .unwrap(); + + let legacy = thread::spawn(move || { + crate::ops::legacy_ensure_artifact_for_test( + &legacy_registry, + &legacy_store, + &legacy_version, + ) + .unwrap() + }); + + let legacy_entered_download = legacy_started_rx + .recv_timeout(Duration::from_millis(300)) + .is_ok(); + recursive_release_tx.send(()).unwrap(); + + let (recursive_path, recursive_downloaded) = recursive.join().unwrap(); + let legacy_path = legacy.join().unwrap(); + assert!(recursive_downloaded); + assert!( + !legacy_entered_download, + "the legacy path bypassed the shared artifact lock and started a duplicate download" + ); + assert_eq!(downloads.load(Ordering::SeqCst), 1); + assert_eq!(legacy_path, recursive_path); + assert!(recursive_path.is_dir()); +} + +''' + +path.write_text(text.replace(marker, block + marker, 1)) From e464e0340f34616fa43767541767e1d2633987fc Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 10:44:01 -0500 Subject: [PATCH 31/80] ci: validate shared artifact acquisition patch --- .../apply-shared-artifact-acquisition.yml | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/apply-shared-artifact-acquisition.yml diff --git a/.github/workflows/apply-shared-artifact-acquisition.yml b/.github/workflows/apply-shared-artifact-acquisition.yml new file mode 100644 index 00000000..6af3ecb0 --- /dev/null +++ b/.github/workflows/apply-shared-artifact-acquisition.yml @@ -0,0 +1,64 @@ +name: apply shared artifact acquisition + +on: + push: + branches: + - agent/recursive-install-queue + +permissions: + contents: read + +concurrency: + group: apply-shared-artifact-acquisition-${{ github.ref }} + cancel-in-progress: true + +jobs: + apply: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + permissions: + contents: write + steps: + - name: Check out recursive installer branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: agent/recursive-install-queue + fetch-depth: 0 + persist-credentials: false + show-progress: false + + - name: Apply exact shared-acquisition patch + run: | + set -euo pipefail + python3 .github/unify_artifact_acquisition.py + cargo fmt --all + git diff --check + + - name: Validate mixed and recursive acquisition paths + run: | + set -euo pipefail + rustup component add rustfmt clippy + cargo fmt --all --check + cargo test --locked --lib \ + install_graph::tests::legacy_and_recursive_acquisition_paths_share_one_download_lock \ + -- --nocapture + cargo test --locked --lib install_graph::tests + cargo clippy --locked --all-targets -- -D warnings + + - name: Commit validated code and remove bootstrap files + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + rm -f \ + .github/unify_artifact_acquisition.py \ + .github/workflows/apply-shared-artifact-acquisition.yml + git add -A + git diff --cached --check + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git commit -m 'fix: unify artifact acquisition locks' + git push \ + "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ + HEAD:agent/recursive-install-queue From 798468a2da66cf895adccde43fe77a1bc4f87381 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:47:04 +0000 Subject: [PATCH 32/80] fix: unify artifact acquisition locks --- .github/unify_artifact_acquisition.py | 327 ------------------ .../apply-shared-artifact-acquisition.yml | 64 ---- src/install_graph.rs | 1 + src/install_graph/artifact.rs | 22 +- src/install_graph/tests.rs | 144 +++++++- src/ops.rs | 19 +- src/ops_entry.rs | 3 + src/store.rs | 4 + 8 files changed, 173 insertions(+), 411 deletions(-) delete mode 100644 .github/unify_artifact_acquisition.py delete mode 100644 .github/workflows/apply-shared-artifact-acquisition.yml diff --git a/.github/unify_artifact_acquisition.py b/.github/unify_artifact_acquisition.py deleted file mode 100644 index 77f315dd..00000000 --- a/.github/unify_artifact_acquisition.py +++ /dev/null @@ -1,327 +0,0 @@ -from pathlib import Path - - -def replace_once(path: str, old: str, new: str) -> None: - file = Path(path) - text = file.read_text() - count = text.count(old) - if count != 1: - raise RuntimeError(f"expected one match in {path}, found {count}") - file.write_text(text.replace(old, new, 1)) - - -replace_once( - "src/store.rs", - ''' pub fn root(&self) -> PathBuf { - self.home.join("store") - } - - pub fn cache_dir(&self) -> PathBuf { -''', - ''' pub(crate) fn home(&self) -> &Path { - &self.home - } - - pub fn root(&self) -> PathBuf { - self.home.join("store") - } - - pub fn cache_dir(&self) -> PathBuf { -''', -) - -replace_once( - "src/install_graph/artifact.rs", - ''' prefetch_one(registry, &store, &home, task) -''', - ''' prefetch_one(registry, &store, task) -''', -) - -replace_once( - "src/install_graph/artifact.rs", - '''fn prefetch_one( - registry: &dyn Registry, - store: &Store, - home: &Path, - task: FetchTask, -) -> Result { - let (package_dir, downloaded) = ensure_artifact(registry, store, home, &task.version) -''', - '''fn prefetch_one( - registry: &dyn Registry, - store: &Store, - task: FetchTask, -) -> Result { - let (package_dir, downloaded) = ensure_artifact(registry, store, &task.version) -''', -) - -replace_once( - "src/install_graph/artifact.rs", - '''fn ensure_artifact( - registry: &dyn Registry, - store: &Store, - home: &Path, - version: &VersionMetadata, -) -> Result<(PathBuf, bool)> { -''', - '''/// Acquire one immutable artifact through the shared cache/store path. -/// -/// Every caller uses the same blocking per-hash process lock, staged download, -/// integrity check, atomic cache publication, and extraction sequence. This is -/// intentionally crate-visible so the legacy transactional installer cannot -/// bypass recursive-prefetch locking. -pub(crate) fn ensure_artifact( - registry: &dyn Registry, - store: &Store, - version: &VersionMetadata, -) -> Result<(PathBuf, bool)> { -''', -) - -replace_once( - "src/install_graph/artifact.rs", - ''' let _artifact_lock = ArtifactProcessLock::acquire(home, &version.sha256)?; -''', - ''' let _artifact_lock = ArtifactProcessLock::acquire(store.home(), &version.sha256)?; -''', -) - -replace_once( - "src/install_graph.rs", - '''use artifact::worker_loop; -pub use resolver::prefetch; -''', - '''pub(crate) use artifact::ensure_artifact; -use artifact::worker_loop; -pub use resolver::prefetch; -''', -) - -replace_once( - "src/ops.rs", - '''fn ensure_artifact(reg: &dyn Registry, store: &Store, vm: &VersionMetadata) -> Result { - validate_version_metadata(vm)?; - if store.has(&vm.sha256) { - return Ok(store.pkg_dir(&vm.sha256)); - } - let cached = store.cached_artifact(&vm.sha256); - if !cached.exists() { - reg.download(vm, &cached)?; - } - store.add_artifact(&cached, &vm.sha256) -} -''', - '''fn ensure_artifact(reg: &dyn Registry, store: &Store, vm: &VersionMetadata) -> Result { - validate_version_metadata(vm)?; - crate::install_graph::ensure_artifact(reg, store, vm) - .map(|(package_dir, _downloaded)| package_dir) -} - -#[cfg(test)] -pub(crate) fn legacy_ensure_artifact_for_test( - reg: &dyn Registry, - store: &Store, - vm: &VersionMetadata, -) -> Result { - ensure_artifact(reg, store, vm) -} -''', -) - -replace_once( - "src/ops_entry.rs", - '''pub(crate) use implementation::{ - detect_adapter, detect_native_manifest_target, detect_structure_target, detect_target, -}; - -pub fn add(project: &Path, cfg: &Config, spec: &str) -> Result<()> { -''', - '''pub(crate) use implementation::{ - detect_adapter, detect_native_manifest_target, detect_structure_target, detect_target, -}; - -#[cfg(test)] -pub(crate) use implementation::legacy_ensure_artifact_for_test; - -pub fn add(project: &Path, cfg: &Config, spec: &str) -> Result<()> { -''', -) - -replace_once( - "src/install_graph/tests.rs", - '''use std::sync::{Arc, Barrier, mpsc as test_mpsc}; -''', - '''use std::sync::{ - Arc, Barrier, - atomic::{AtomicUsize, Ordering}, - mpsc as test_mpsc, -}; -''', -) - -path = Path("src/install_graph/tests.rs") -text = path.read_text() -if "legacy_and_recursive_acquisition_paths_share_one_download_lock" in text: - raise RuntimeError("mixed-path lock test already exists") - -marker = '''#[test] -fn defaults_to_five_workers_and_bounds_overrides() { -''' -if text.count(marker) != 1: - raise RuntimeError("could not locate first install-graph test") - -block = r'''struct CountingRegistry { - artifact: PathBuf, - downloads: Arc, - started: Option>, - release: Option>, -} - -impl Registry for CountingRegistry { - fn get_package(&self, _org: &str, _name: &str) -> Result { - unreachable!("mixed-path acquisition test does not resolve package metadata") - } - - fn get_version( - &self, - _org: &str, - _name: &str, - _version: &str, - ) -> Result { - unreachable!("mixed-path acquisition test receives resolved version metadata") - } - - fn download(&self, _version: &VersionMetadata, dest: &Path) -> Result<()> { - self.downloads.fetch_add(1, Ordering::SeqCst); - if let Some(started) = &self.started { - started.send(()).unwrap(); - } - if let Some(release) = &self.release { - release.recv_timeout(Duration::from_secs(5)).unwrap(); - } - fs::create_dir_all(dest.parent().unwrap())?; - fs::copy(&self.artifact, dest)?; - Ok(()) - } - - fn publish( - &self, - _meta: &zed_interfaces::registry::PublishMeta, - _artifact: &Path, - _token: Option<&str>, - ) -> Result { - unreachable!("mixed-path acquisition test does not publish") - } - - fn claim_org( - &self, - _slug: &str, - _token: Option<&str>, - ) -> Result { - unreachable!("mixed-path acquisition test does not claim organizations") - } - - fn search(&self, _query: &str) -> Result { - unreachable!("mixed-path acquisition test does not search") - } - - fn yank( - &self, - _org: &str, - _name: &str, - _version: &str, - _yanked: bool, - _token: Option<&str>, - ) -> Result { - unreachable!("mixed-path acquisition test does not yank") - } - - fn audit_log( - &self, - _org: &str, - _limit: Option, - _token: Option<&str>, - ) -> Result { - unreachable!("mixed-path acquisition test does not read audit logs") - } -} - -#[test] -fn legacy_and_recursive_acquisition_paths_share_one_download_lock() { - let temp = tempfile::tempdir().unwrap(); - let registry_root = temp.path().join("registry"); - let scratch = temp.path().join("scratch"); - let home = temp.path().join("home"); - - let sha = publish_fixture( - ®istry_root, - &scratch, - "test", - "shared", - "1.0.0", - &[], - ); - let version = published_version(®istry_root, "test", "shared", "1.0.0"); - let artifact = registry_root - .join("artifacts") - .join(format!("{sha}.tar.gz")); - let downloads = Arc::new(AtomicUsize::new(0)); - let (recursive_started_tx, recursive_started_rx) = test_mpsc::channel(); - let (recursive_release_tx, recursive_release_rx) = test_mpsc::channel(); - let (legacy_started_tx, legacy_started_rx) = test_mpsc::channel(); - - let recursive_registry = CountingRegistry { - artifact: artifact.clone(), - downloads: Arc::clone(&downloads), - started: Some(recursive_started_tx), - release: Some(recursive_release_rx), - }; - let legacy_registry = CountingRegistry { - artifact, - downloads: Arc::clone(&downloads), - started: Some(legacy_started_tx), - release: None, - }; - let recursive_store = Store::new(&home); - let legacy_store = Store::new(&home); - let recursive_version = version.clone(); - let legacy_version = version; - - let recursive = thread::spawn(move || { - super::ensure_artifact(&recursive_registry, &recursive_store, &recursive_version).unwrap() - }); - recursive_started_rx - .recv_timeout(Duration::from_secs(2)) - .unwrap(); - - let legacy = thread::spawn(move || { - crate::ops::legacy_ensure_artifact_for_test( - &legacy_registry, - &legacy_store, - &legacy_version, - ) - .unwrap() - }); - - let legacy_entered_download = legacy_started_rx - .recv_timeout(Duration::from_millis(300)) - .is_ok(); - recursive_release_tx.send(()).unwrap(); - - let (recursive_path, recursive_downloaded) = recursive.join().unwrap(); - let legacy_path = legacy.join().unwrap(); - assert!(recursive_downloaded); - assert!( - !legacy_entered_download, - "the legacy path bypassed the shared artifact lock and started a duplicate download" - ); - assert_eq!(downloads.load(Ordering::SeqCst), 1); - assert_eq!(legacy_path, recursive_path); - assert!(recursive_path.is_dir()); -} - -''' - -path.write_text(text.replace(marker, block + marker, 1)) diff --git a/.github/workflows/apply-shared-artifact-acquisition.yml b/.github/workflows/apply-shared-artifact-acquisition.yml deleted file mode 100644 index 6af3ecb0..00000000 --- a/.github/workflows/apply-shared-artifact-acquisition.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: apply shared artifact acquisition - -on: - push: - branches: - - agent/recursive-install-queue - -permissions: - contents: read - -concurrency: - group: apply-shared-artifact-acquisition-${{ github.ref }} - cancel-in-progress: true - -jobs: - apply: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 - timeout-minutes: 45 - permissions: - contents: write - steps: - - name: Check out recursive installer branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: agent/recursive-install-queue - fetch-depth: 0 - persist-credentials: false - show-progress: false - - - name: Apply exact shared-acquisition patch - run: | - set -euo pipefail - python3 .github/unify_artifact_acquisition.py - cargo fmt --all - git diff --check - - - name: Validate mixed and recursive acquisition paths - run: | - set -euo pipefail - rustup component add rustfmt clippy - cargo fmt --all --check - cargo test --locked --lib \ - install_graph::tests::legacy_and_recursive_acquisition_paths_share_one_download_lock \ - -- --nocapture - cargo test --locked --lib install_graph::tests - cargo clippy --locked --all-targets -- -D warnings - - - name: Commit validated code and remove bootstrap files - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - rm -f \ - .github/unify_artifact_acquisition.py \ - .github/workflows/apply-shared-artifact-acquisition.yml - git add -A - git diff --cached --check - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m 'fix: unify artifact acquisition locks' - git push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ - HEAD:agent/recursive-install-queue diff --git a/src/install_graph.rs b/src/install_graph.rs index 210e54e8..2e5318b9 100644 --- a/src/install_graph.rs +++ b/src/install_graph.rs @@ -296,5 +296,6 @@ mod resolver; #[cfg(test)] mod tests; +pub(crate) use artifact::ensure_artifact; use artifact::worker_loop; pub use resolver::prefetch; diff --git a/src/install_graph/artifact.rs b/src/install_graph/artifact.rs index f7a2d5b6..8333c134 100644 --- a/src/install_graph/artifact.rs +++ b/src/install_graph/artifact.rs @@ -18,7 +18,7 @@ pub(super) fn worker_loop( let registry = registry .as_deref() .context("recursive install worker has no registry")?; - prefetch_one(registry, &store, &home, task) + prefetch_one(registry, &store, task) })(); if results.send(FetchMessage { sequence, result }).is_err() { return; @@ -26,13 +26,8 @@ pub(super) fn worker_loop( } } -fn prefetch_one( - registry: &dyn Registry, - store: &Store, - home: &Path, - task: FetchTask, -) -> Result { - let (package_dir, downloaded) = ensure_artifact(registry, store, home, &task.version) +fn prefetch_one(registry: &dyn Registry, store: &Store, task: FetchTask) -> Result { + let (package_dir, downloaded) = ensure_artifact(registry, store, &task.version) .with_context(|| format!("prefetching {}@{}", task.key, task.version.version))?; let dependencies = read_manifest(&package_dir) .map(|manifest| manifest.dependencies) @@ -43,10 +38,15 @@ fn prefetch_one( }) } -fn ensure_artifact( +/// Acquire one immutable artifact through the shared cache/store path. +/// +/// Every caller uses the same blocking per-hash process lock, staged download, +/// integrity check, atomic cache publication, and extraction sequence. This is +/// intentionally crate-visible so the legacy transactional installer cannot +/// bypass recursive-prefetch locking. +pub(crate) fn ensure_artifact( registry: &dyn Registry, store: &Store, - home: &Path, version: &VersionMetadata, ) -> Result<(PathBuf, bool)> { validate_version_identity(version, &version.org, &version.name, &version.version)?; @@ -54,7 +54,7 @@ fn ensure_artifact( return Ok((store.pkg_dir(&version.sha256), false)); } - let _artifact_lock = ArtifactProcessLock::acquire(home, &version.sha256)?; + let _artifact_lock = ArtifactProcessLock::acquire(store.home(), &version.sha256)?; if store.has(&version.sha256) { return Ok((store.pkg_dir(&version.sha256), false)); } diff --git a/src/install_graph/tests.rs b/src/install_graph/tests.rs index 0a025fa5..1a15498f 100644 --- a/src/install_graph/tests.rs +++ b/src/install_graph/tests.rs @@ -1,4 +1,8 @@ -use std::sync::{Arc, Barrier, mpsc as test_mpsc}; +use std::sync::{ + Arc, Barrier, + atomic::{AtomicUsize, Ordering}, + mpsc as test_mpsc, +}; use std::thread; use std::time::Duration; @@ -130,6 +134,144 @@ fn published_version( serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap() } +struct CountingRegistry { + artifact: PathBuf, + downloads: Arc, + started: Option>, + release: Option>, +} + +impl Registry for CountingRegistry { + fn get_package(&self, _org: &str, _name: &str) -> Result { + unreachable!("mixed-path acquisition test does not resolve package metadata") + } + + fn get_version(&self, _org: &str, _name: &str, _version: &str) -> Result { + unreachable!("mixed-path acquisition test receives resolved version metadata") + } + + fn download(&self, _version: &VersionMetadata, dest: &Path) -> Result<()> { + self.downloads.fetch_add(1, Ordering::SeqCst); + if let Some(started) = &self.started { + started.send(()).unwrap(); + } + if let Some(release) = &self.release { + release.recv_timeout(Duration::from_secs(5)).unwrap(); + } + fs::create_dir_all(dest.parent().unwrap())?; + fs::copy(&self.artifact, dest)?; + Ok(()) + } + + fn publish( + &self, + _meta: &zed_interfaces::registry::PublishMeta, + _artifact: &Path, + _token: Option<&str>, + ) -> Result { + unreachable!("mixed-path acquisition test does not publish") + } + + fn claim_org( + &self, + _slug: &str, + _token: Option<&str>, + ) -> Result { + unreachable!("mixed-path acquisition test does not claim organizations") + } + + fn search(&self, _query: &str) -> Result { + unreachable!("mixed-path acquisition test does not search") + } + + fn yank( + &self, + _org: &str, + _name: &str, + _version: &str, + _yanked: bool, + _token: Option<&str>, + ) -> Result { + unreachable!("mixed-path acquisition test does not yank") + } + + fn audit_log( + &self, + _org: &str, + _limit: Option, + _token: Option<&str>, + ) -> Result { + unreachable!("mixed-path acquisition test does not read audit logs") + } +} + +#[test] +fn legacy_and_recursive_acquisition_paths_share_one_download_lock() { + let temp = tempfile::tempdir().unwrap(); + let registry_root = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + + let sha = publish_fixture(®istry_root, &scratch, "test", "shared", "1.0.0", &[]); + let version = published_version(®istry_root, "test", "shared", "1.0.0"); + let artifact = registry_root + .join("artifacts") + .join(format!("{sha}.tar.gz")); + let downloads = Arc::new(AtomicUsize::new(0)); + let (recursive_started_tx, recursive_started_rx) = test_mpsc::channel(); + let (recursive_release_tx, recursive_release_rx) = test_mpsc::channel(); + let (legacy_started_tx, legacy_started_rx) = test_mpsc::channel(); + + let recursive_registry = CountingRegistry { + artifact: artifact.clone(), + downloads: Arc::clone(&downloads), + started: Some(recursive_started_tx), + release: Some(recursive_release_rx), + }; + let legacy_registry = CountingRegistry { + artifact, + downloads: Arc::clone(&downloads), + started: Some(legacy_started_tx), + release: None, + }; + let recursive_store = Store::new(&home); + let legacy_store = Store::new(&home); + let recursive_version = version.clone(); + let legacy_version = version; + + let recursive = thread::spawn(move || { + super::ensure_artifact(&recursive_registry, &recursive_store, &recursive_version).unwrap() + }); + recursive_started_rx + .recv_timeout(Duration::from_secs(2)) + .unwrap(); + + let legacy = thread::spawn(move || { + crate::ops::legacy_ensure_artifact_for_test( + &legacy_registry, + &legacy_store, + &legacy_version, + ) + .unwrap() + }); + + let legacy_entered_download = legacy_started_rx + .recv_timeout(Duration::from_millis(300)) + .is_ok(); + recursive_release_tx.send(()).unwrap(); + + let (recursive_path, recursive_downloaded) = recursive.join().unwrap(); + let legacy_path = legacy.join().unwrap(); + assert!(recursive_downloaded); + assert!( + !legacy_entered_download, + "the legacy path bypassed the shared artifact lock and started a duplicate download" + ); + assert_eq!(downloads.load(Ordering::SeqCst), 1); + assert_eq!(legacy_path, recursive_path); + assert!(recursive_path.is_dir()); +} + #[test] fn defaults_to_five_workers_and_bounds_overrides() { assert_eq!(normalize_concurrency(None), 5); diff --git a/src/ops.rs b/src/ops.rs index 2c365a2c..8d4696bf 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -229,14 +229,17 @@ pub struct InstallOutcome { fn ensure_artifact(reg: &dyn Registry, store: &Store, vm: &VersionMetadata) -> Result { validate_version_metadata(vm)?; - if store.has(&vm.sha256) { - return Ok(store.pkg_dir(&vm.sha256)); - } - let cached = store.cached_artifact(&vm.sha256); - if !cached.exists() { - reg.download(vm, &cached)?; - } - store.add_artifact(&cached, &vm.sha256) + crate::install_graph::ensure_artifact(reg, store, vm) + .map(|(package_dir, _downloaded)| package_dir) +} + +#[cfg(test)] +pub(crate) fn legacy_ensure_artifact_for_test( + reg: &dyn Registry, + store: &Store, + vm: &VersionMetadata, +) -> Result { + ensure_artifact(reg, store, vm) } fn replace_dest(dest: &Path) -> Result<()> { diff --git a/src/ops_entry.rs b/src/ops_entry.rs index dde8eb67..36815c27 100644 --- a/src/ops_entry.rs +++ b/src/ops_entry.rs @@ -25,6 +25,9 @@ pub(crate) use implementation::{ detect_adapter, detect_native_manifest_target, detect_structure_target, detect_target, }; +#[cfg(test)] +pub(crate) use implementation::legacy_ensure_artifact_for_test; + pub fn add(project: &Path, cfg: &Config, spec: &str) -> Result<()> { crate::config::with_install_prefetch(cfg, || implementation::add(project, cfg, spec)) } diff --git a/src/store.rs b/src/store.rs index 6f4dfcce..018446af 100644 --- a/src/store.rs +++ b/src/store.rs @@ -161,6 +161,10 @@ impl Store { } } + pub(crate) fn home(&self) -> &Path { + &self.home + } + pub fn root(&self) -> PathBuf { self.home.join("store") } From 60db3f7a8180233b51d0370186154410db92c572 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 10:50:53 -0500 Subject: [PATCH 33/80] docs: describe unified artifact acquisition lock --- docs/recursive-installs.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/recursive-installs.md b/docs/recursive-installs.md index b320d867..c0d67d8c 100644 --- a/docs/recursive-installs.md +++ b/docs/recursive-installs.md @@ -34,6 +34,11 @@ 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. @@ -45,3 +50,15 @@ 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. + +## 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. + +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. From 6aa8edc4f8436dc48f1231001374ca6ef08887dc Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:01:20 -0500 Subject: [PATCH 34/80] Harden recursive worker failure handling (#63) Catch per-task worker panics and return sequenced errors so the coordinator cannot wait forever for a lost task. Add regression coverage for panic conversion and failed staged-download cleanup. --- src/install_graph/artifact.rs | 33 +++++++++++++++++++-- src/install_graph/tests.rs | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/install_graph/artifact.rs b/src/install_graph/artifact.rs index 8333c134..f159cb60 100644 --- a/src/install_graph/artifact.rs +++ b/src/install_graph/artifact.rs @@ -11,7 +11,8 @@ pub(super) fn worker_loop( while let Some(task) = queue.pop() { let sequence = task.sequence; - let result = (|| -> Result { + let key = task.key.clone(); + let message = run_fetch_task(sequence, &key, || -> Result { if registry.is_none() { registry = Some(registry_for(®istry_url)?); } @@ -19,13 +20,39 @@ pub(super) fn worker_loop( .as_deref() .context("recursive install worker has no registry")?; prefetch_one(registry, &store, task) - })(); - if results.send(FetchMessage { sequence, result }).is_err() { + }); + if results.send(message).is_err() { return; } } } +/// Convert a task panic into the same sequenced result channel used for normal +/// failures. Without this boundary, the worker would unwind after popping the +/// task, permanently losing that sequence while the coordinator waited for a +/// result that could never arrive. +pub(super) fn run_fetch_task(sequence: usize, key: &str, work: F) -> FetchMessage +where + F: FnOnce() -> Result, +{ + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(work)).unwrap_or_else( + |payload| { + let detail = payload + .downcast_ref::<&str>() + .copied() + .or_else(|| payload.downcast_ref::().map(String::as_str)) + .unwrap_or("non-string panic payload"); + Err(anyhow::anyhow!( + "recursive install worker panicked while processing {} (sequence {}): {}", + key, + sequence, + detail + )) + }, + ); + FetchMessage { sequence, result } +} + fn prefetch_one(registry: &dyn Registry, store: &Store, task: FetchTask) -> Result { let (package_dir, downloaded) = ensure_artifact(registry, store, &task.version) .with_context(|| format!("prefetching {}@{}", task.key, task.version.version))?; diff --git a/src/install_graph/tests.rs b/src/install_graph/tests.rs index 1a15498f..d07a62d3 100644 --- a/src/install_graph/tests.rs +++ b/src/install_graph/tests.rs @@ -139,6 +139,7 @@ struct CountingRegistry { downloads: Arc, started: Option>, release: Option>, + fail_after_write: bool, } impl Registry for CountingRegistry { @@ -159,6 +160,10 @@ impl Registry for CountingRegistry { release.recv_timeout(Duration::from_secs(5)).unwrap(); } fs::create_dir_all(dest.parent().unwrap())?; + if self.fail_after_write { + fs::write(dest, b"partial fixture download")?; + bail!("injected artifact download failure"); + } fs::copy(&self.artifact, dest)?; Ok(()) } @@ -227,12 +232,14 @@ fn legacy_and_recursive_acquisition_paths_share_one_download_lock() { downloads: Arc::clone(&downloads), started: Some(recursive_started_tx), release: Some(recursive_release_rx), + fail_after_write: false, }; let legacy_registry = CountingRegistry { artifact, downloads: Arc::clone(&downloads), started: Some(legacy_started_tx), release: None, + fail_after_write: false, }; let recursive_store = Store::new(&home); let legacy_store = Store::new(&home); @@ -272,6 +279,54 @@ fn legacy_and_recursive_acquisition_paths_share_one_download_lock() { assert!(recursive_path.is_dir()); } +#[test] +fn worker_panics_are_reported_without_losing_the_task_sequence() { + let message = super::artifact::run_fetch_task(17, "test/panic", || -> Result { + panic!("fixture worker panic") + }); + + assert_eq!(message.sequence, 17); + let error = message.result.unwrap_err().to_string(); + assert!(error.contains("test/panic"), "{error}"); + assert!(error.contains("sequence 17"), "{error}"); + assert!(error.contains("fixture worker panic"), "{error}"); +} + +#[test] +fn failed_downloads_remove_staging_files_and_never_publish_cache_entries() { + let temp = tempfile::tempdir().unwrap(); + let registry_root = temp.path().join("registry"); + let scratch = temp.path().join("scratch"); + let home = temp.path().join("home"); + + let sha = publish_fixture(®istry_root, &scratch, "test", "failure", "1.0.0", &[]); + let version = published_version(®istry_root, "test", "failure", "1.0.0"); + let registry = CountingRegistry { + artifact: registry_root + .join("artifacts") + .join(format!("{sha}.tar.gz")), + downloads: Arc::new(AtomicUsize::new(0)), + started: None, + release: None, + fail_after_write: true, + }; + let store = Store::new(&home); + + let error = super::ensure_artifact(®istry, &store, &version) + .unwrap_err() + .to_string(); + assert!( + error.contains("injected artifact download failure"), + "{error}" + ); + assert!(!store.cached_artifact(&sha).exists()); + assert!(!store.has(&sha)); + let cache_entries = fs::read_dir(store.cache_dir()) + .map(|entries| entries.count()) + .unwrap_or(0); + assert_eq!(cache_entries, 0, "failed staging directory leaked"); +} + #[test] fn defaults_to_five_workers_and_bounds_overrides() { assert_eq!(normalize_concurrency(None), 5); From c2e7fa8f7f4ea5a0acbdb51d6b4ac34f46b406ea Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:08:03 -0500 Subject: [PATCH 35/80] style: apply rustfmt to worker panic boundary --- src/install_graph/artifact.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/install_graph/artifact.rs b/src/install_graph/artifact.rs index f159cb60..1912e673 100644 --- a/src/install_graph/artifact.rs +++ b/src/install_graph/artifact.rs @@ -35,8 +35,8 @@ pub(super) fn run_fetch_task(sequence: usize, key: &str, work: F) -> FetchMes where F: FnOnce() -> Result, { - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(work)).unwrap_or_else( - |payload| { + let result = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(work)).unwrap_or_else(|payload| { let detail = payload .downcast_ref::<&str>() .copied() @@ -48,8 +48,7 @@ where sequence, detail )) - }, - ); + }); FetchMessage { sequence, result } } From b559a32be07328f33c85cabab709b174d626fc78 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:14:49 -0500 Subject: [PATCH 36/80] chore: stage cross-platform process-lock test --- .github/cross_platform_artifact_lock.py | 141 ++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 .github/cross_platform_artifact_lock.py diff --git a/.github/cross_platform_artifact_lock.py b/.github/cross_platform_artifact_lock.py new file mode 100644 index 00000000..e079150a --- /dev/null +++ b/.github/cross_platform_artifact_lock.py @@ -0,0 +1,141 @@ +from pathlib import Path + + +def replace_once(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text() + count = text.count(old) + if count != 1: + raise RuntimeError(f"expected one match in {path}, found {count}") + file.write_text(text.replace(old, new, 1)) + + +replace_once( + "src/install_graph/tests.rs", + '''use std::sync::{ + Arc, Barrier, + atomic::{AtomicUsize, Ordering}, + mpsc as test_mpsc, +}; +use std::thread; +use std::time::Duration; +''', + '''use std::process::{Command, Stdio}; +use std::sync::{ + Arc, Barrier, + atomic::{AtomicUsize, Ordering}, + mpsc as test_mpsc, +}; +use std::thread; +use std::time::{Duration, Instant}; +''', +) + +replace_once( + "src/install_graph/tests.rs", + '''#[cfg(unix)] +#[test] +fn artifact_waiters_block_until_the_owner_releases_the_kernel_lock() { + let temp = tempfile::tempdir().unwrap(); + let sha = "a".repeat(64); + let owner = ArtifactProcessLock::acquire(temp.path(), &sha).unwrap(); + let second_home = temp.path().to_path_buf(); + let second_sha = sha.clone(); + let (acquired_tx, acquired_rx) = test_mpsc::channel(); + let waiter = thread::spawn(move || { + let _waiter = ArtifactProcessLock::acquire(&second_home, &second_sha).unwrap(); + acquired_tx.send(()).unwrap(); + }); + + assert!( + acquired_rx + .recv_timeout(Duration::from_millis(100)) + .is_err() + ); + drop(owner); + acquired_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + waiter.join().unwrap(); +} +''', + '''#[test] +fn artifact_waiters_block_until_the_owner_releases_the_kernel_lock() { + const CHILD_HOME: &str = "ZED_PKG_TEST_LOCK_CHILD_HOME"; + const CHILD_SHA: &str = "ZED_PKG_TEST_LOCK_CHILD_SHA"; + const CHILD_ATTEMPTING: &str = "ZED_PKG_TEST_LOCK_CHILD_ATTEMPTING"; + const CHILD_ACQUIRED: &str = "ZED_PKG_TEST_LOCK_CHILD_ACQUIRED"; + const TEST_NAME: &str = + "install_graph::tests::artifact_waiters_block_until_the_owner_releases_the_kernel_lock"; + + if let Some(home) = std::env::var_os(CHILD_HOME) { + let sha = std::env::var(CHILD_SHA).unwrap(); + let attempting = PathBuf::from(std::env::var_os(CHILD_ATTEMPTING).unwrap()); + let acquired = PathBuf::from(std::env::var_os(CHILD_ACQUIRED).unwrap()); + fs::write(attempting, b"attempting").unwrap(); + let _waiter = ArtifactProcessLock::acquire(Path::new(&home), &sha).unwrap(); + fs::write(acquired, b"acquired").unwrap(); + return; + } + + let temp = tempfile::tempdir().unwrap(); + let sha = "a".repeat(64); + let owner = ArtifactProcessLock::acquire(temp.path(), &sha).unwrap(); + let attempting = temp.path().join("waiter-attempting"); + let acquired = temp.path().join("waiter-acquired"); + let mut child = Command::new(std::env::current_exe().unwrap()) + .arg(TEST_NAME) + .arg("--exact") + .arg("--nocapture") + .env(CHILD_HOME, temp.path()) + .env(CHILD_SHA, &sha) + .env(CHILD_ATTEMPTING, &attempting) + .env(CHILD_ACQUIRED, &acquired) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap(); + + let attempting_deadline = Instant::now() + Duration::from_secs(5); + while !attempting.is_file() { + if let Some(status) = child.try_wait().unwrap() { + panic!("lock waiter child exited before attempting acquisition: {status}"); + } + if Instant::now() >= attempting_deadline { + let _ = child.kill(); + let _ = child.wait(); + panic!("lock waiter child did not reach acquisition"); + } + thread::sleep(Duration::from_millis(20)); + } + + thread::sleep(Duration::from_millis(200)); + assert!( + !acquired.exists(), + "a separate process acquired the artifact lock while the owner still held it" + ); + assert!( + child.try_wait().unwrap().is_none(), + "lock waiter child exited before the owner released the lock" + ); + + drop(owner); + let release_deadline = Instant::now() + Duration::from_secs(5); + loop { + if let Some(status) = child.try_wait().unwrap() { + assert!(status.success(), "lock waiter child failed: {status}"); + break; + } + if Instant::now() >= release_deadline { + let _ = child.kill(); + let _ = child.wait(); + panic!("lock waiter child did not wake after lock release"); + } + thread::sleep(Duration::from_millis(20)); + } + assert!( + acquired.is_file(), + "lock waiter child exited without recording acquisition" + ); +} +''', +) From 5cd31d0b4684164eb9e43534cbca3169882f5d6e Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:15:12 -0500 Subject: [PATCH 37/80] ci: add Windows recursive artifact locking contract --- .../workflows/recursive-install-windows.yml | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/recursive-install-windows.yml diff --git a/.github/workflows/recursive-install-windows.yml b/.github/workflows/recursive-install-windows.yml new file mode 100644 index 00000000..db568532 --- /dev/null +++ b/.github/workflows/recursive-install-windows.yml @@ -0,0 +1,81 @@ +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: ${{ github.event_name == 'pull_request' }} + +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 From 5e9b86d0f2269c41b8f66b4bc29a2af01e0953e1 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:15:36 -0500 Subject: [PATCH 38/80] ci: validate cross-platform artifact lock test --- .../apply-cross-platform-artifact-lock.yml | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/apply-cross-platform-artifact-lock.yml diff --git a/.github/workflows/apply-cross-platform-artifact-lock.yml b/.github/workflows/apply-cross-platform-artifact-lock.yml new file mode 100644 index 00000000..05a63907 --- /dev/null +++ b/.github/workflows/apply-cross-platform-artifact-lock.yml @@ -0,0 +1,64 @@ +name: apply cross-platform artifact lock test + +on: + push: + branches: + - agent/recursive-lock-cross-platform + +permissions: + contents: read + +concurrency: + group: apply-cross-platform-artifact-lock-${{ github.ref }} + cancel-in-progress: true + +jobs: + apply: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + permissions: + contents: write + steps: + - name: Check out lock-hardening branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: agent/recursive-lock-cross-platform + fetch-depth: 0 + persist-credentials: false + show-progress: false + + - name: Apply exact cross-platform process-lock patch + run: | + set -euo pipefail + python3 .github/cross_platform_artifact_lock.py + cargo fmt --all + git diff --check + + - name: Validate the subprocess lock contract and installer suite + run: | + set -euo pipefail + rustup component add rustfmt clippy + cargo fmt --all --check + cargo test --locked --lib \ + install_graph::tests::artifact_waiters_block_until_the_owner_releases_the_kernel_lock \ + -- --exact --nocapture + cargo test --locked --lib install_graph::tests + cargo clippy --locked --all-targets -- -D warnings + + - name: Commit validated code and remove bootstrap files + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + rm -f \ + .github/cross_platform_artifact_lock.py \ + .github/workflows/apply-cross-platform-artifact-lock.yml + git add -A + git diff --cached --check + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git commit -m 'test: exercise artifact lock across processes' + git push \ + "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ + HEAD:agent/recursive-lock-cross-platform From 96f586c8d3627e6efb29af7e5ddf640afb0c2ad3 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:17:45 -0500 Subject: [PATCH 39/80] ci: trigger cross-platform artifact lock validation --- .github/workflows/apply-cross-platform-artifact-lock.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/apply-cross-platform-artifact-lock.yml b/.github/workflows/apply-cross-platform-artifact-lock.yml index 05a63907..b08709e9 100644 --- a/.github/workflows/apply-cross-platform-artifact-lock.yml +++ b/.github/workflows/apply-cross-platform-artifact-lock.yml @@ -62,3 +62,5 @@ jobs: git push \ "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ HEAD:agent/recursive-lock-cross-platform + +# This second branch commit intentionally triggers the already-present workflow. From 96e809e1348a056e6c802fdeb0cc5a1844cc7e6d Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:18:29 -0500 Subject: [PATCH 40/80] ci: allow PR-triggered cross-platform lock validation --- .github/workflows/apply-cross-platform-artifact-lock.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apply-cross-platform-artifact-lock.yml b/.github/workflows/apply-cross-platform-artifact-lock.yml index b08709e9..de8023cf 100644 --- a/.github/workflows/apply-cross-platform-artifact-lock.yml +++ b/.github/workflows/apply-cross-platform-artifact-lock.yml @@ -4,6 +4,9 @@ on: push: branches: - agent/recursive-lock-cross-platform + pull_request: + branches: + - agent/recursive-install-queue permissions: contents: read @@ -62,5 +65,3 @@ jobs: git push \ "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ HEAD:agent/recursive-lock-cross-platform - -# This second branch commit intentionally triggers the already-present workflow. From 599cb180f3558dd7444ae5b325dc1051c4783acf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:26:29 +0000 Subject: [PATCH 41/80] test: exercise artifact lock across processes --- .github/cross_platform_artifact_lock.py | 141 ------------------ .../apply-cross-platform-artifact-lock.yml | 67 --------- src/install_graph/tests.rs | 86 +++++++++-- 3 files changed, 72 insertions(+), 222 deletions(-) delete mode 100644 .github/cross_platform_artifact_lock.py delete mode 100644 .github/workflows/apply-cross-platform-artifact-lock.yml diff --git a/.github/cross_platform_artifact_lock.py b/.github/cross_platform_artifact_lock.py deleted file mode 100644 index e079150a..00000000 --- a/.github/cross_platform_artifact_lock.py +++ /dev/null @@ -1,141 +0,0 @@ -from pathlib import Path - - -def replace_once(path: str, old: str, new: str) -> None: - file = Path(path) - text = file.read_text() - count = text.count(old) - if count != 1: - raise RuntimeError(f"expected one match in {path}, found {count}") - file.write_text(text.replace(old, new, 1)) - - -replace_once( - "src/install_graph/tests.rs", - '''use std::sync::{ - Arc, Barrier, - atomic::{AtomicUsize, Ordering}, - mpsc as test_mpsc, -}; -use std::thread; -use std::time::Duration; -''', - '''use std::process::{Command, Stdio}; -use std::sync::{ - Arc, Barrier, - atomic::{AtomicUsize, Ordering}, - mpsc as test_mpsc, -}; -use std::thread; -use std::time::{Duration, Instant}; -''', -) - -replace_once( - "src/install_graph/tests.rs", - '''#[cfg(unix)] -#[test] -fn artifact_waiters_block_until_the_owner_releases_the_kernel_lock() { - let temp = tempfile::tempdir().unwrap(); - let sha = "a".repeat(64); - let owner = ArtifactProcessLock::acquire(temp.path(), &sha).unwrap(); - let second_home = temp.path().to_path_buf(); - let second_sha = sha.clone(); - let (acquired_tx, acquired_rx) = test_mpsc::channel(); - let waiter = thread::spawn(move || { - let _waiter = ArtifactProcessLock::acquire(&second_home, &second_sha).unwrap(); - acquired_tx.send(()).unwrap(); - }); - - assert!( - acquired_rx - .recv_timeout(Duration::from_millis(100)) - .is_err() - ); - drop(owner); - acquired_rx.recv_timeout(Duration::from_secs(2)).unwrap(); - waiter.join().unwrap(); -} -''', - '''#[test] -fn artifact_waiters_block_until_the_owner_releases_the_kernel_lock() { - const CHILD_HOME: &str = "ZED_PKG_TEST_LOCK_CHILD_HOME"; - const CHILD_SHA: &str = "ZED_PKG_TEST_LOCK_CHILD_SHA"; - const CHILD_ATTEMPTING: &str = "ZED_PKG_TEST_LOCK_CHILD_ATTEMPTING"; - const CHILD_ACQUIRED: &str = "ZED_PKG_TEST_LOCK_CHILD_ACQUIRED"; - const TEST_NAME: &str = - "install_graph::tests::artifact_waiters_block_until_the_owner_releases_the_kernel_lock"; - - if let Some(home) = std::env::var_os(CHILD_HOME) { - let sha = std::env::var(CHILD_SHA).unwrap(); - let attempting = PathBuf::from(std::env::var_os(CHILD_ATTEMPTING).unwrap()); - let acquired = PathBuf::from(std::env::var_os(CHILD_ACQUIRED).unwrap()); - fs::write(attempting, b"attempting").unwrap(); - let _waiter = ArtifactProcessLock::acquire(Path::new(&home), &sha).unwrap(); - fs::write(acquired, b"acquired").unwrap(); - return; - } - - let temp = tempfile::tempdir().unwrap(); - let sha = "a".repeat(64); - let owner = ArtifactProcessLock::acquire(temp.path(), &sha).unwrap(); - let attempting = temp.path().join("waiter-attempting"); - let acquired = temp.path().join("waiter-acquired"); - let mut child = Command::new(std::env::current_exe().unwrap()) - .arg(TEST_NAME) - .arg("--exact") - .arg("--nocapture") - .env(CHILD_HOME, temp.path()) - .env(CHILD_SHA, &sha) - .env(CHILD_ATTEMPTING, &attempting) - .env(CHILD_ACQUIRED, &acquired) - .stdin(Stdio::null()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .unwrap(); - - let attempting_deadline = Instant::now() + Duration::from_secs(5); - while !attempting.is_file() { - if let Some(status) = child.try_wait().unwrap() { - panic!("lock waiter child exited before attempting acquisition: {status}"); - } - if Instant::now() >= attempting_deadline { - let _ = child.kill(); - let _ = child.wait(); - panic!("lock waiter child did not reach acquisition"); - } - thread::sleep(Duration::from_millis(20)); - } - - thread::sleep(Duration::from_millis(200)); - assert!( - !acquired.exists(), - "a separate process acquired the artifact lock while the owner still held it" - ); - assert!( - child.try_wait().unwrap().is_none(), - "lock waiter child exited before the owner released the lock" - ); - - drop(owner); - let release_deadline = Instant::now() + Duration::from_secs(5); - loop { - if let Some(status) = child.try_wait().unwrap() { - assert!(status.success(), "lock waiter child failed: {status}"); - break; - } - if Instant::now() >= release_deadline { - let _ = child.kill(); - let _ = child.wait(); - panic!("lock waiter child did not wake after lock release"); - } - thread::sleep(Duration::from_millis(20)); - } - assert!( - acquired.is_file(), - "lock waiter child exited without recording acquisition" - ); -} -''', -) diff --git a/.github/workflows/apply-cross-platform-artifact-lock.yml b/.github/workflows/apply-cross-platform-artifact-lock.yml deleted file mode 100644 index de8023cf..00000000 --- a/.github/workflows/apply-cross-platform-artifact-lock.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: apply cross-platform artifact lock test - -on: - push: - branches: - - agent/recursive-lock-cross-platform - pull_request: - branches: - - agent/recursive-install-queue - -permissions: - contents: read - -concurrency: - group: apply-cross-platform-artifact-lock-${{ github.ref }} - cancel-in-progress: true - -jobs: - apply: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 - timeout-minutes: 45 - permissions: - contents: write - steps: - - name: Check out lock-hardening branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: agent/recursive-lock-cross-platform - fetch-depth: 0 - persist-credentials: false - show-progress: false - - - name: Apply exact cross-platform process-lock patch - run: | - set -euo pipefail - python3 .github/cross_platform_artifact_lock.py - cargo fmt --all - git diff --check - - - name: Validate the subprocess lock contract and installer suite - run: | - set -euo pipefail - rustup component add rustfmt clippy - cargo fmt --all --check - cargo test --locked --lib \ - install_graph::tests::artifact_waiters_block_until_the_owner_releases_the_kernel_lock \ - -- --exact --nocapture - cargo test --locked --lib install_graph::tests - cargo clippy --locked --all-targets -- -D warnings - - - name: Commit validated code and remove bootstrap files - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - rm -f \ - .github/cross_platform_artifact_lock.py \ - .github/workflows/apply-cross-platform-artifact-lock.yml - git add -A - git diff --cached --check - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m 'test: exercise artifact lock across processes' - git push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ - HEAD:agent/recursive-lock-cross-platform diff --git a/src/install_graph/tests.rs b/src/install_graph/tests.rs index d07a62d3..fbe615ca 100644 --- a/src/install_graph/tests.rs +++ b/src/install_graph/tests.rs @@ -1,10 +1,11 @@ +use std::process::{Command, Stdio}; use std::sync::{ Arc, Barrier, atomic::{AtomicUsize, Ordering}, mpsc as test_mpsc, }; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; use zed_interfaces::lockfile::LockedPackage; use zed_interfaces::registry::{PackageMetadata, VersionMetadata}; @@ -628,26 +629,83 @@ fn a_corrupt_partial_cache_is_replaced_under_the_artifact_lock() { assert_eq!(sha256_file(&cached).unwrap().0, sha); } -#[cfg(unix)] #[test] fn artifact_waiters_block_until_the_owner_releases_the_kernel_lock() { + const CHILD_HOME: &str = "ZED_PKG_TEST_LOCK_CHILD_HOME"; + const CHILD_SHA: &str = "ZED_PKG_TEST_LOCK_CHILD_SHA"; + const CHILD_ATTEMPTING: &str = "ZED_PKG_TEST_LOCK_CHILD_ATTEMPTING"; + const CHILD_ACQUIRED: &str = "ZED_PKG_TEST_LOCK_CHILD_ACQUIRED"; + const TEST_NAME: &str = + "install_graph::tests::artifact_waiters_block_until_the_owner_releases_the_kernel_lock"; + + if let Some(home) = std::env::var_os(CHILD_HOME) { + let sha = std::env::var(CHILD_SHA).unwrap(); + let attempting = PathBuf::from(std::env::var_os(CHILD_ATTEMPTING).unwrap()); + let acquired = PathBuf::from(std::env::var_os(CHILD_ACQUIRED).unwrap()); + fs::write(attempting, b"attempting").unwrap(); + let _waiter = ArtifactProcessLock::acquire(Path::new(&home), &sha).unwrap(); + fs::write(acquired, b"acquired").unwrap(); + return; + } + let temp = tempfile::tempdir().unwrap(); let sha = "a".repeat(64); let owner = ArtifactProcessLock::acquire(temp.path(), &sha).unwrap(); - let second_home = temp.path().to_path_buf(); - let second_sha = sha.clone(); - let (acquired_tx, acquired_rx) = test_mpsc::channel(); - let waiter = thread::spawn(move || { - let _waiter = ArtifactProcessLock::acquire(&second_home, &second_sha).unwrap(); - acquired_tx.send(()).unwrap(); - }); + let attempting = temp.path().join("waiter-attempting"); + let acquired = temp.path().join("waiter-acquired"); + let mut child = Command::new(std::env::current_exe().unwrap()) + .arg(TEST_NAME) + .arg("--exact") + .arg("--nocapture") + .env(CHILD_HOME, temp.path()) + .env(CHILD_SHA, &sha) + .env(CHILD_ATTEMPTING, &attempting) + .env(CHILD_ACQUIRED, &acquired) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap(); + + let attempting_deadline = Instant::now() + Duration::from_secs(5); + while !attempting.is_file() { + if let Some(status) = child.try_wait().unwrap() { + panic!("lock waiter child exited before attempting acquisition: {status}"); + } + if Instant::now() >= attempting_deadline { + let _ = child.kill(); + let _ = child.wait(); + panic!("lock waiter child did not reach acquisition"); + } + thread::sleep(Duration::from_millis(20)); + } + thread::sleep(Duration::from_millis(200)); assert!( - acquired_rx - .recv_timeout(Duration::from_millis(100)) - .is_err() + !acquired.exists(), + "a separate process acquired the artifact lock while the owner still held it" ); + assert!( + child.try_wait().unwrap().is_none(), + "lock waiter child exited before the owner released the lock" + ); + drop(owner); - acquired_rx.recv_timeout(Duration::from_secs(2)).unwrap(); - waiter.join().unwrap(); + let release_deadline = Instant::now() + Duration::from_secs(5); + loop { + if let Some(status) = child.try_wait().unwrap() { + assert!(status.success(), "lock waiter child failed: {status}"); + break; + } + if Instant::now() >= release_deadline { + let _ = child.kill(); + let _ = child.wait(); + panic!("lock waiter child did not wake after lock release"); + } + thread::sleep(Duration::from_millis(20)); + } + assert!( + acquired.is_file(), + "lock waiter child exited without recording acquisition" + ); } From fabf5c1ef939bdfad2a089f2c58d6a5b493e257e Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:28:29 -0500 Subject: [PATCH 42/80] docs: describe cross-platform process lock validation --- docs/recursive-installs.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/recursive-installs.md b/docs/recursive-installs.md index c0d67d8c..be5dcd21 100644 --- a/docs/recursive-installs.md +++ b/docs/recursive-installs.md @@ -58,6 +58,12 @@ 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 From 8c9d3fae94584e2b651e1a3afb89e348240ac5c0 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:33:19 -0500 Subject: [PATCH 43/80] chore: stage kernel-blocking store lock patch --- .github/blocking_store_process_locks.py | 171 ++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 .github/blocking_store_process_locks.py diff --git a/.github/blocking_store_process_locks.py b/.github/blocking_store_process_locks.py new file mode 100644 index 00000000..f6750665 --- /dev/null +++ b/.github/blocking_store_process_locks.py @@ -0,0 +1,171 @@ +from pathlib import Path + + +def replace_once(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text() + count = text.count(old) + if count != 1: + raise RuntimeError(f"expected one match in {path}, found {count}") + file.write_text(text.replace(old, new, 1)) + + +store = Path("src/store.rs") +text = store.read_text() +text = text.replace( + "use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};", + "use std::time::{Duration, SystemTime, UNIX_EPOCH};", + 1, +) + +start_marker = "/// An advisory (flock-based) process lock held for the life of the guard." +end_marker = "/// What `Store::gc` did (or, with `dry_run`, would do)." +start = text.index(start_marker) +end = text.index(end_marker, start) +replacement = '''/// A descriptor-backed process lock held for the life of the guard. +/// +/// Acquisition uses the operating system's blocking lock primitive directly: +/// `flock`/`fcntl` semantics on Unix and `LockFileEx` semantics on Windows via +/// `fs2`. Contended callers sleep in the kernel and wake when the owner drops +/// the descriptor or exits. There is no retry timer, jitter loop, stale-file +/// reclamation, or userspace polling. +pub struct ProcessLock { + _file: fs::File, +} + +impl ProcessLock { + fn acquire(path: &Path, waiting_on: &str) -> Result { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let file = fs::OpenOptions::new() + .create(true) + .read(true) + .write(true) + .truncate(false) + .open(path) + .with_context(|| format!("opening lock file {}", path.display()))?; + + file.lock_exclusive().with_context(|| { + format!( + "waiting for {waiting_on} through operating-system lock {}", + path.display() + ) + })?; + Ok(Self { _file: file }) + } +} + +''' +text = text[:start] + replacement + text[end:] + +old_test_header = '''#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gc_survives_hostile_max_age() { +''' +new_test_header = '''#[cfg(test)] +mod tests { + use std::process::{Command, Stdio}; + use std::time::Instant; + + use super::*; + + #[test] + fn install_process_lock_blocks_without_polling_until_owner_release() { + const CHILD_HOME: &str = "ZED_PKG_TEST_STORE_LOCK_CHILD_HOME"; + const CHILD_ATTEMPTING: &str = "ZED_PKG_TEST_STORE_LOCK_CHILD_ATTEMPTING"; + const CHILD_ACQUIRED: &str = "ZED_PKG_TEST_STORE_LOCK_CHILD_ACQUIRED"; + const TEST_NAME: &str = + "store::tests::install_process_lock_blocks_without_polling_until_owner_release"; + + if let Some(home) = std::env::var_os(CHILD_HOME) { + let attempting = PathBuf::from(std::env::var_os(CHILD_ATTEMPTING).unwrap()); + let acquired = PathBuf::from(std::env::var_os(CHILD_ACQUIRED).unwrap()); + fs::write(attempting, b"attempting").unwrap(); + let store = Store::new(Path::new(&home)); + let _waiter = store.install_lock().unwrap(); + fs::write(acquired, b"acquired").unwrap(); + return; + } + + let temp = tempfile::tempdir().unwrap(); + let store = Store::new(temp.path()); + let owner = store.install_lock().unwrap(); + let attempting = temp.path().join("store-lock-waiter-attempting"); + let acquired = temp.path().join("store-lock-waiter-acquired"); + let mut child = Command::new(std::env::current_exe().unwrap()) + .arg(TEST_NAME) + .arg("--exact") + .arg("--nocapture") + .env(CHILD_HOME, temp.path()) + .env(CHILD_ATTEMPTING, &attempting) + .env(CHILD_ACQUIRED, &acquired) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap(); + + let attempting_deadline = Instant::now() + Duration::from_secs(5); + while !attempting.is_file() { + if let Some(status) = child.try_wait().unwrap() { + panic!("store-lock waiter exited before attempting acquisition: {status}"); + } + if Instant::now() >= attempting_deadline { + let _ = child.kill(); + let _ = child.wait(); + panic!("store-lock waiter did not reach acquisition"); + } + std::thread::sleep(Duration::from_millis(20)); + } + + std::thread::sleep(Duration::from_millis(200)); + assert!( + !acquired.exists(), + "a separate process acquired install.lock while the owner held it" + ); + assert!( + child.try_wait().unwrap().is_none(), + "store-lock waiter exited before owner release" + ); + + drop(owner); + let release_deadline = Instant::now() + Duration::from_secs(5); + loop { + if let Some(status) = child.try_wait().unwrap() { + assert!(status.success(), "store-lock waiter failed: {status}"); + break; + } + if Instant::now() >= release_deadline { + let _ = child.kill(); + let _ = child.wait(); + panic!("store-lock waiter did not wake after owner release"); + } + std::thread::sleep(Duration::from_millis(20)); + } + assert!(acquired.is_file(), "waiter exited without acquiring install.lock"); + } + + #[test] + fn gc_survives_hostile_max_age() { +''' +if text.count(old_test_header) != 1: + raise RuntimeError("could not locate store test module header") +text = text.replace(old_test_header, new_test_header, 1) +store.write_text(text) + +replace_once( + ".github/workflows/recursive-install-windows.yml", + ''' - 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 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 +''', +) From 2aec1bc83ee565c661487440b7570bc3317b9fde Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:33:36 -0500 Subject: [PATCH 44/80] ci: validate kernel-blocking store lock patch --- .../apply-blocking-store-process-locks.yml | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/apply-blocking-store-process-locks.yml diff --git a/.github/workflows/apply-blocking-store-process-locks.yml b/.github/workflows/apply-blocking-store-process-locks.yml new file mode 100644 index 00000000..3c26c799 --- /dev/null +++ b/.github/workflows/apply-blocking-store-process-locks.yml @@ -0,0 +1,65 @@ +name: apply blocking store process locks + +on: + pull_request: + branches: + - agent/recursive-lock-cross-platform + +permissions: + contents: read + +concurrency: + group: apply-blocking-store-process-locks-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + apply: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + permissions: + contents: write + steps: + - name: Check out store-lock branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: agent/blocking-store-process-locks + fetch-depth: 0 + persist-credentials: false + show-progress: false + + - name: Apply exact kernel-blocking lock patch + run: | + set -euo pipefail + python3 .github/blocking_store_process_locks.py + cargo fmt --all + git diff --check + + - name: Validate store and recursive lock contracts + run: | + set -euo pipefail + rustup component add rustfmt clippy + cargo fmt --all --check + cargo test --locked --lib \ + store::tests::install_process_lock_blocks_without_polling_until_owner_release \ + -- --exact --nocapture + cargo test --locked --lib store::tests + cargo test --locked --lib install_graph::tests + cargo clippy --locked --all-targets -- -D warnings + + - name: Commit validated code and remove bootstrap files + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + rm -f \ + .github/blocking_store_process_locks.py \ + .github/workflows/apply-blocking-store-process-locks.yml + git add -A + git diff --cached --check + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git commit -m 'fix: block store locks in the kernel' + git push \ + "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ + HEAD:agent/blocking-store-process-locks From b7893ae396d20badeeb56ba36019edfa69e6d0e0 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:56:39 -0500 Subject: [PATCH 45/80] ci: satisfy fail-closed workflow concurrency policy --- .github/workflows/recursive-install-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/recursive-install-windows.yml b/.github/workflows/recursive-install-windows.yml index db568532..8fdb860c 100644 --- a/.github/workflows/recursive-install-windows.yml +++ b/.github/workflows/recursive-install-windows.yml @@ -25,7 +25,7 @@ permissions: concurrency: group: recursive-install-windows-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: true env: CARGO_TERM_COLOR: always From 0b7f524d0508e88752879783bccf74915f890271 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:58:33 -0500 Subject: [PATCH 46/80] ci: make blocking-lock bootstrap idempotent and code-only --- .github/blocking_store_process_locks.py | 62 +++++++++---------------- 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/.github/blocking_store_process_locks.py b/.github/blocking_store_process_locks.py index f6750665..0a237ecb 100644 --- a/.github/blocking_store_process_locks.py +++ b/.github/blocking_store_process_locks.py @@ -1,28 +1,22 @@ from pathlib import Path -def replace_once(path: str, old: str, new: str) -> None: - file = Path(path) - text = file.read_text() - count = text.count(old) - if count != 1: - raise RuntimeError(f"expected one match in {path}, found {count}") - file.write_text(text.replace(old, new, 1)) - - store = Path("src/store.rs") text = store.read_text() -text = text.replace( - "use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};", - "use std::time::{Duration, SystemTime, UNIX_EPOCH};", - 1, -) - -start_marker = "/// An advisory (flock-based) process lock held for the life of the guard." -end_marker = "/// What `Store::gc` did (or, with `dry_run`, would do)." -start = text.index(start_marker) -end = text.index(end_marker, start) -replacement = '''/// A descriptor-backed process lock held for the life of the guard. + +new_lock_marker = "/// A descriptor-backed process lock held for the life of the guard." +if new_lock_marker not in text: + old_import = "use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};" + new_import = "use std::time::{Duration, SystemTime, UNIX_EPOCH};" + if text.count(old_import) != 1: + raise RuntimeError("could not locate the store time import") + text = text.replace(old_import, new_import, 1) + + start_marker = "/// An advisory (flock-based) process lock held for the life of the guard." + end_marker = "/// What `Store::gc` did (or, with `dry_run`, would do)." + start = text.index(start_marker) + end = text.index(end_marker, start) + replacement = '''/// A descriptor-backed process lock held for the life of the guard. /// /// Acquisition uses the operating system's blocking lock primitive directly: /// `flock`/`fcntl` semantics on Unix and `LockFileEx` semantics on Windows via @@ -57,16 +51,18 @@ def replace_once(path: str, old: str, new: str) -> None: } ''' -text = text[:start] + replacement + text[end:] + text = text[:start] + replacement + text[end:] -old_test_header = '''#[cfg(test)] +new_test_name = "fn install_process_lock_blocks_without_polling_until_owner_release()" +if new_test_name not in text: + old_test_header = '''#[cfg(test)] mod tests { use super::*; #[test] fn gc_survives_hostile_max_age() { ''' -new_test_header = '''#[cfg(test)] + new_test_header = '''#[cfg(test)] mod tests { use std::process::{Command, Stdio}; use std::time::Instant; @@ -152,20 +148,8 @@ def replace_once(path: str, old: str, new: str) -> None: #[test] fn gc_survives_hostile_max_age() { ''' -if text.count(old_test_header) != 1: - raise RuntimeError("could not locate store test module header") -text = text.replace(old_test_header, new_test_header, 1) -store.write_text(text) + if text.count(old_test_header) != 1: + raise RuntimeError("could not locate store test module header") + text = text.replace(old_test_header, new_test_header, 1) -replace_once( - ".github/workflows/recursive-install-windows.yml", - ''' - 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 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 -''', -) +store.write_text(text) From 43491279273a4e21d5a3f9110b718079b0ecdc54 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 11:58:46 -0500 Subject: [PATCH 47/80] ci: publish validated store code without workflow writes --- .../workflows/apply-blocking-store-process-locks.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/apply-blocking-store-process-locks.yml b/.github/workflows/apply-blocking-store-process-locks.yml index 3c26c799..4a15c94b 100644 --- a/.github/workflows/apply-blocking-store-process-locks.yml +++ b/.github/workflows/apply-blocking-store-process-locks.yml @@ -47,16 +47,17 @@ jobs: cargo test --locked --lib install_graph::tests cargo clippy --locked --all-targets -- -D warnings - - name: Commit validated code and remove bootstrap files + - name: Publish validated store implementation env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - rm -f \ - .github/blocking_store_process_locks.py \ - .github/workflows/apply-blocking-store-process-locks.yml - git add -A + git add src/store.rs git diff --cached --check + if git diff --cached --quiet; then + echo 'kernel-blocking store implementation is already published' + exit 0 + fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git commit -m 'fix: block store locks in the kernel' From a407898351fffc991b65b4aa1219476c796e20c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:00:20 +0000 Subject: [PATCH 48/80] fix: block store locks in the kernel --- src/store.rs | 155 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 97 insertions(+), 58 deletions(-) diff --git a/src/store.rs b/src/store.rs index 018446af..522973f0 100644 --- a/src/store.rs +++ b/src/store.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::io::Read; use std::path::{Component, Path, PathBuf}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use anyhow::{Context, Result, bail}; use flate2::read::GzDecoder; @@ -55,22 +55,18 @@ fn require_build_key(key: &str) -> Result<()> { Ok(()) } -/// An advisory (flock-based) process lock held for the life of the guard. -/// CLI commands are highly concurrent — two `zed install` runs in different -/// terminals, or N parallel CI runners — so store mutations that must not -/// interleave (extracting the same artifact, rewriting refs.json) take a -/// lock first. The OS releases it if the process dies, so a crash can't -/// wedge the store. +/// A descriptor-backed process lock held for the life of the guard. +/// +/// Acquisition uses the operating system's blocking lock primitive directly: +/// `flock`/`fcntl` semantics on Unix and `LockFileEx` semantics on Windows via +/// `fs2`. Contended callers sleep in the kernel and wake when the owner drops +/// the descriptor or exits. There is no retry timer, jitter loop, stale-file +/// reclamation, or userspace polling. pub struct ProcessLock { _file: fs::File, } impl ProcessLock { - /// Three-phase acquisition tuned for CLI latency: - /// spin-wait at 50ms while young (catches sub-second releases with no - /// perceptible lag), then exponential backoff with full jitter so - /// parallel CI runners don't wake in lockstep, and after 3s tell the - /// user what they are waiting on instead of freezing silently. fn acquire(path: &Path, waiting_on: &str) -> Result { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; @@ -83,52 +79,13 @@ impl ProcessLock { .open(path) .with_context(|| format!("opening lock file {}", path.display()))?; - let timeout = Duration::from_secs( - std::env::var("ZED_PKG_LOCK_TIMEOUT") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(600), - ); - let start = Instant::now(); - let mut backoff_attempt: u32 = 0; - let mut notified = false; - loop { - match file.try_lock_exclusive() { - Ok(()) => return Ok(Self { _file: file }), - Err(_) if start.elapsed() >= timeout => { - bail!( - "timed out after {}s waiting for {waiting_on} (lock {}); \ - another zed process may be stuck — raise ZED_PKG_LOCK_TIMEOUT \ - or investigate", - timeout.as_secs(), - path.display() - ); - } - Err(_) => {} - } - let elapsed = start.elapsed(); - if elapsed >= Duration::from_secs(3) && !notified { - eprintln!("waiting for {waiting_on} (held by another zed process)..."); - notified = true; - } - let sleep = if elapsed < Duration::from_millis(500) { - Duration::from_millis(50) - } else { - backoff_attempt = backoff_attempt.saturating_add(1); - let cap_ms: u64 = 2000; - let exp_ms = 100u64 - .saturating_mul(1u64 << backoff_attempt.min(20)) - .min(cap_ms); - // Full jitter: uniform in [0, exp_ms] without pulling in an - // RNG dependency — subsec_nanos is plenty for de-syncing. - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.subsec_nanos() as u64) - .unwrap_or(0); - Duration::from_millis(nanos % (exp_ms + 1)) - }; - std::thread::sleep(sleep); - } + file.lock_exclusive().with_context(|| { + format!( + "waiting for {waiting_on} through operating-system lock {}", + path.display() + ) + })?; + Ok(Self { _file: file }) } } @@ -688,8 +645,90 @@ pub fn human_size(bytes: u64) -> String { #[cfg(test)] mod tests { + use std::process::{Command, Stdio}; + use std::time::Instant; + use super::*; + #[test] + fn install_process_lock_blocks_without_polling_until_owner_release() { + const CHILD_HOME: &str = "ZED_PKG_TEST_STORE_LOCK_CHILD_HOME"; + const CHILD_ATTEMPTING: &str = "ZED_PKG_TEST_STORE_LOCK_CHILD_ATTEMPTING"; + const CHILD_ACQUIRED: &str = "ZED_PKG_TEST_STORE_LOCK_CHILD_ACQUIRED"; + const TEST_NAME: &str = + "store::tests::install_process_lock_blocks_without_polling_until_owner_release"; + + if let Some(home) = std::env::var_os(CHILD_HOME) { + let attempting = PathBuf::from(std::env::var_os(CHILD_ATTEMPTING).unwrap()); + let acquired = PathBuf::from(std::env::var_os(CHILD_ACQUIRED).unwrap()); + fs::write(attempting, b"attempting").unwrap(); + let store = Store::new(Path::new(&home)); + let _waiter = store.install_lock().unwrap(); + fs::write(acquired, b"acquired").unwrap(); + return; + } + + let temp = tempfile::tempdir().unwrap(); + let store = Store::new(temp.path()); + let owner = store.install_lock().unwrap(); + let attempting = temp.path().join("store-lock-waiter-attempting"); + let acquired = temp.path().join("store-lock-waiter-acquired"); + let mut child = Command::new(std::env::current_exe().unwrap()) + .arg(TEST_NAME) + .arg("--exact") + .arg("--nocapture") + .env(CHILD_HOME, temp.path()) + .env(CHILD_ATTEMPTING, &attempting) + .env(CHILD_ACQUIRED, &acquired) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap(); + + let attempting_deadline = Instant::now() + Duration::from_secs(5); + while !attempting.is_file() { + if let Some(status) = child.try_wait().unwrap() { + panic!("store-lock waiter exited before attempting acquisition: {status}"); + } + if Instant::now() >= attempting_deadline { + let _ = child.kill(); + let _ = child.wait(); + panic!("store-lock waiter did not reach acquisition"); + } + std::thread::sleep(Duration::from_millis(20)); + } + + std::thread::sleep(Duration::from_millis(200)); + assert!( + !acquired.exists(), + "a separate process acquired install.lock while the owner held it" + ); + assert!( + child.try_wait().unwrap().is_none(), + "store-lock waiter exited before owner release" + ); + + drop(owner); + let release_deadline = Instant::now() + Duration::from_secs(5); + loop { + if let Some(status) = child.try_wait().unwrap() { + assert!(status.success(), "store-lock waiter failed: {status}"); + break; + } + if Instant::now() >= release_deadline { + let _ = child.kill(); + let _ = child.wait(); + panic!("store-lock waiter did not wake after owner release"); + } + std::thread::sleep(Duration::from_millis(20)); + } + assert!( + acquired.is_file(), + "waiter exited without acquiring install.lock" + ); + } + #[test] fn gc_survives_hostile_max_age() { // A hostile age (`u64::MAX` seconds) would overflow naive epoch math From 6e49f62d8113139cfde00eeffe3e2337d8765bbd Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:03:41 -0500 Subject: [PATCH 49/80] test: stage owner-exit process lock regression --- .github/add_owner_exit_lock_test.py | 149 ++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 .github/add_owner_exit_lock_test.py diff --git a/.github/add_owner_exit_lock_test.py b/.github/add_owner_exit_lock_test.py new file mode 100644 index 00000000..3d33e717 --- /dev/null +++ b/.github/add_owner_exit_lock_test.py @@ -0,0 +1,149 @@ +from pathlib import Path + + +store = Path("src/store.rs") +text = store.read_text() +test_name = "fn install_process_lock_is_released_when_owner_process_is_terminated()" + +if test_name not in text: + marker = ''' #[test] + fn gc_survives_hostile_max_age() { +''' + replacement = ''' #[test] + fn install_process_lock_is_released_when_owner_process_is_terminated() { + const CHILD_ROLE: &str = "ZED_PKG_TEST_STORE_LOCK_EXIT_ROLE"; + const CHILD_HOME: &str = "ZED_PKG_TEST_STORE_LOCK_EXIT_HOME"; + const OWNER_ACQUIRED: &str = "ZED_PKG_TEST_STORE_LOCK_OWNER_ACQUIRED"; + const WAITER_ATTEMPTING: &str = "ZED_PKG_TEST_STORE_LOCK_WAITER_ATTEMPTING"; + const WAITER_ACQUIRED: &str = "ZED_PKG_TEST_STORE_LOCK_WAITER_ACQUIRED"; + const TEST_NAME: &str = + "store::tests::install_process_lock_is_released_when_owner_process_is_terminated"; + + if let Some(role) = std::env::var_os(CHILD_ROLE) { + let home = PathBuf::from(std::env::var_os(CHILD_HOME).unwrap()); + let store = Store::new(&home); + match role.to_string_lossy().as_ref() { + "owner" => { + let _owner = store.install_lock().unwrap(); + fs::write(std::env::var_os(OWNER_ACQUIRED).unwrap(), b"acquired").unwrap(); + loop { + std::thread::sleep(Duration::from_secs(60)); + } + } + "waiter" => { + fs::write(std::env::var_os(WAITER_ATTEMPTING).unwrap(), b"attempting") + .unwrap(); + let _waiter = store.install_lock().unwrap(); + fs::write(std::env::var_os(WAITER_ACQUIRED).unwrap(), b"acquired").unwrap(); + return; + } + other => panic!("unexpected child role: {other}"), + } + } + + let temp = tempfile::tempdir().unwrap(); + let owner_acquired = temp.path().join("store-lock-owner-acquired"); + let waiter_attempting = temp.path().join("store-lock-waiter-attempting-after-exit"); + let waiter_acquired = temp.path().join("store-lock-waiter-acquired-after-exit"); + + let mut owner = Command::new(std::env::current_exe().unwrap()) + .arg(TEST_NAME) + .arg("--exact") + .arg("--nocapture") + .env(CHILD_ROLE, "owner") + .env(CHILD_HOME, temp.path()) + .env(OWNER_ACQUIRED, &owner_acquired) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap(); + + let owner_deadline = Instant::now() + Duration::from_secs(5); + while !owner_acquired.is_file() { + if let Some(status) = owner.try_wait().unwrap() { + panic!("store-lock owner exited before acquiring install.lock: {status}"); + } + if Instant::now() >= owner_deadline { + let _ = owner.kill(); + let _ = owner.wait(); + panic!("store-lock owner did not acquire install.lock"); + } + std::thread::sleep(Duration::from_millis(20)); + } + + let mut waiter = Command::new(std::env::current_exe().unwrap()) + .arg(TEST_NAME) + .arg("--exact") + .arg("--nocapture") + .env(CHILD_ROLE, "waiter") + .env(CHILD_HOME, temp.path()) + .env(WAITER_ATTEMPTING, &waiter_attempting) + .env(WAITER_ACQUIRED, &waiter_acquired) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap(); + + let attempting_deadline = Instant::now() + Duration::from_secs(5); + while !waiter_attempting.is_file() { + if let Some(status) = waiter.try_wait().unwrap() { + let _ = owner.kill(); + let _ = owner.wait(); + panic!("store-lock waiter exited before attempting acquisition: {status}"); + } + if Instant::now() >= attempting_deadline { + let _ = waiter.kill(); + let _ = waiter.wait(); + let _ = owner.kill(); + let _ = owner.wait(); + panic!("store-lock waiter did not reach acquisition"); + } + std::thread::sleep(Duration::from_millis(20)); + } + + std::thread::sleep(Duration::from_millis(200)); + assert!( + !waiter_acquired.exists(), + "waiter acquired install.lock while the owner process was alive" + ); + assert!( + waiter.try_wait().unwrap().is_none(), + "store-lock waiter exited before owner termination" + ); + + owner.kill().unwrap(); + let owner_status = owner.wait().unwrap(); + assert!( + !owner_status.success(), + "owner process unexpectedly exited successfully instead of being terminated" + ); + + let release_deadline = Instant::now() + Duration::from_secs(5); + loop { + if let Some(status) = waiter.try_wait().unwrap() { + assert!(status.success(), "store-lock waiter failed after owner exit: {status}"); + break; + } + if Instant::now() >= release_deadline { + let _ = waiter.kill(); + let _ = waiter.wait(); + panic!("store-lock waiter did not wake after owner process termination"); + } + std::thread::sleep(Duration::from_millis(20)); + } + assert!( + waiter_acquired.is_file(), + "waiter exited without acquiring install.lock after owner termination" + ); + } + + #[test] + fn gc_survives_hostile_max_age() { +''' + if text.count(marker) != 1: + raise RuntimeError("could not locate insertion point for owner-exit lock test") + text = text.replace(marker, replacement, 1) + +store.write_text(text) From b0d2a97847c70183c96d5e183e3ab62dd6687742 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:04:02 -0500 Subject: [PATCH 50/80] test: validate owner-exit lock release before publishing --- .github/workflows/apply-blocking-store-process-locks.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/apply-blocking-store-process-locks.yml b/.github/workflows/apply-blocking-store-process-locks.yml index 4a15c94b..6fb1f425 100644 --- a/.github/workflows/apply-blocking-store-process-locks.yml +++ b/.github/workflows/apply-blocking-store-process-locks.yml @@ -32,6 +32,7 @@ jobs: run: | set -euo pipefail python3 .github/blocking_store_process_locks.py + python3 .github/add_owner_exit_lock_test.py cargo fmt --all git diff --check @@ -43,6 +44,9 @@ jobs: cargo test --locked --lib \ store::tests::install_process_lock_blocks_without_polling_until_owner_release \ -- --exact --nocapture + cargo test --locked --lib \ + store::tests::install_process_lock_is_released_when_owner_process_is_terminated \ + -- --exact --nocapture cargo test --locked --lib store::tests cargo test --locked --lib install_graph::tests cargo clippy --locked --all-targets -- -D warnings From e0b7480f237ddbca32b56789d601a8abbd4e4019 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:04:26 -0500 Subject: [PATCH 51/80] test: run store lock blocking and owner-exit regressions on Windows --- .github/workflows/recursive-install-windows.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/recursive-install-windows.yml b/.github/workflows/recursive-install-windows.yml index db568532..eda09e0f 100644 --- a/.github/workflows/recursive-install-windows.yml +++ b/.github/workflows/recursive-install-windows.yml @@ -25,7 +25,7 @@ permissions: concurrency: group: recursive-install-windows-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: true env: CARGO_TERM_COLOR: always @@ -79,3 +79,9 @@ jobs: - 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 From aed930e18ed33076af6f899059f48c281450b999 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:06:25 +0000 Subject: [PATCH 52/80] fix: block store locks in the kernel --- src/store.rs | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/src/store.rs b/src/store.rs index 522973f0..60c03286 100644 --- a/src/store.rs +++ b/src/store.rs @@ -729,6 +729,138 @@ mod tests { ); } + #[test] + fn install_process_lock_is_released_when_owner_process_is_terminated() { + const CHILD_ROLE: &str = "ZED_PKG_TEST_STORE_LOCK_EXIT_ROLE"; + const CHILD_HOME: &str = "ZED_PKG_TEST_STORE_LOCK_EXIT_HOME"; + const OWNER_ACQUIRED: &str = "ZED_PKG_TEST_STORE_LOCK_OWNER_ACQUIRED"; + const WAITER_ATTEMPTING: &str = "ZED_PKG_TEST_STORE_LOCK_WAITER_ATTEMPTING"; + const WAITER_ACQUIRED: &str = "ZED_PKG_TEST_STORE_LOCK_WAITER_ACQUIRED"; + const TEST_NAME: &str = + "store::tests::install_process_lock_is_released_when_owner_process_is_terminated"; + + if let Some(role) = std::env::var_os(CHILD_ROLE) { + let home = PathBuf::from(std::env::var_os(CHILD_HOME).unwrap()); + let store = Store::new(&home); + match role.to_string_lossy().as_ref() { + "owner" => { + let _owner = store.install_lock().unwrap(); + fs::write(std::env::var_os(OWNER_ACQUIRED).unwrap(), b"acquired").unwrap(); + loop { + std::thread::sleep(Duration::from_secs(60)); + } + } + "waiter" => { + fs::write(std::env::var_os(WAITER_ATTEMPTING).unwrap(), b"attempting").unwrap(); + let _waiter = store.install_lock().unwrap(); + fs::write(std::env::var_os(WAITER_ACQUIRED).unwrap(), b"acquired").unwrap(); + return; + } + other => panic!("unexpected child role: {other}"), + } + } + + let temp = tempfile::tempdir().unwrap(); + let owner_acquired = temp.path().join("store-lock-owner-acquired"); + let waiter_attempting = temp.path().join("store-lock-waiter-attempting-after-exit"); + let waiter_acquired = temp.path().join("store-lock-waiter-acquired-after-exit"); + + let mut owner = Command::new(std::env::current_exe().unwrap()) + .arg(TEST_NAME) + .arg("--exact") + .arg("--nocapture") + .env(CHILD_ROLE, "owner") + .env(CHILD_HOME, temp.path()) + .env(OWNER_ACQUIRED, &owner_acquired) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap(); + + let owner_deadline = Instant::now() + Duration::from_secs(5); + while !owner_acquired.is_file() { + if let Some(status) = owner.try_wait().unwrap() { + panic!("store-lock owner exited before acquiring install.lock: {status}"); + } + if Instant::now() >= owner_deadline { + let _ = owner.kill(); + let _ = owner.wait(); + panic!("store-lock owner did not acquire install.lock"); + } + std::thread::sleep(Duration::from_millis(20)); + } + + let mut waiter = Command::new(std::env::current_exe().unwrap()) + .arg(TEST_NAME) + .arg("--exact") + .arg("--nocapture") + .env(CHILD_ROLE, "waiter") + .env(CHILD_HOME, temp.path()) + .env(WAITER_ATTEMPTING, &waiter_attempting) + .env(WAITER_ACQUIRED, &waiter_acquired) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap(); + + let attempting_deadline = Instant::now() + Duration::from_secs(5); + while !waiter_attempting.is_file() { + if let Some(status) = waiter.try_wait().unwrap() { + let _ = owner.kill(); + let _ = owner.wait(); + panic!("store-lock waiter exited before attempting acquisition: {status}"); + } + if Instant::now() >= attempting_deadline { + let _ = waiter.kill(); + let _ = waiter.wait(); + let _ = owner.kill(); + let _ = owner.wait(); + panic!("store-lock waiter did not reach acquisition"); + } + std::thread::sleep(Duration::from_millis(20)); + } + + std::thread::sleep(Duration::from_millis(200)); + assert!( + !waiter_acquired.exists(), + "waiter acquired install.lock while the owner process was alive" + ); + assert!( + waiter.try_wait().unwrap().is_none(), + "store-lock waiter exited before owner termination" + ); + + owner.kill().unwrap(); + let owner_status = owner.wait().unwrap(); + assert!( + !owner_status.success(), + "owner process unexpectedly exited successfully instead of being terminated" + ); + + let release_deadline = Instant::now() + Duration::from_secs(5); + loop { + if let Some(status) = waiter.try_wait().unwrap() { + assert!( + status.success(), + "store-lock waiter failed after owner exit: {status}" + ); + break; + } + if Instant::now() >= release_deadline { + let _ = waiter.kill(); + let _ = waiter.wait(); + panic!("store-lock waiter did not wake after owner process termination"); + } + std::thread::sleep(Duration::from_millis(20)); + } + assert!( + waiter_acquired.is_file(), + "waiter exited without acquiring install.lock after owner termination" + ); + } + #[test] fn gc_survives_hostile_max_age() { // A hostile age (`u64::MAX` seconds) would overflow naive epoch math From dfa2e190f3a2b4f049358e7d8594857ceb814b52 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:11:16 -0500 Subject: [PATCH 53/80] ci: remove owner-exit lock test bootstrap --- .github/add_owner_exit_lock_test.py | 149 ---------------------------- 1 file changed, 149 deletions(-) delete mode 100644 .github/add_owner_exit_lock_test.py diff --git a/.github/add_owner_exit_lock_test.py b/.github/add_owner_exit_lock_test.py deleted file mode 100644 index 3d33e717..00000000 --- a/.github/add_owner_exit_lock_test.py +++ /dev/null @@ -1,149 +0,0 @@ -from pathlib import Path - - -store = Path("src/store.rs") -text = store.read_text() -test_name = "fn install_process_lock_is_released_when_owner_process_is_terminated()" - -if test_name not in text: - marker = ''' #[test] - fn gc_survives_hostile_max_age() { -''' - replacement = ''' #[test] - fn install_process_lock_is_released_when_owner_process_is_terminated() { - const CHILD_ROLE: &str = "ZED_PKG_TEST_STORE_LOCK_EXIT_ROLE"; - const CHILD_HOME: &str = "ZED_PKG_TEST_STORE_LOCK_EXIT_HOME"; - const OWNER_ACQUIRED: &str = "ZED_PKG_TEST_STORE_LOCK_OWNER_ACQUIRED"; - const WAITER_ATTEMPTING: &str = "ZED_PKG_TEST_STORE_LOCK_WAITER_ATTEMPTING"; - const WAITER_ACQUIRED: &str = "ZED_PKG_TEST_STORE_LOCK_WAITER_ACQUIRED"; - const TEST_NAME: &str = - "store::tests::install_process_lock_is_released_when_owner_process_is_terminated"; - - if let Some(role) = std::env::var_os(CHILD_ROLE) { - let home = PathBuf::from(std::env::var_os(CHILD_HOME).unwrap()); - let store = Store::new(&home); - match role.to_string_lossy().as_ref() { - "owner" => { - let _owner = store.install_lock().unwrap(); - fs::write(std::env::var_os(OWNER_ACQUIRED).unwrap(), b"acquired").unwrap(); - loop { - std::thread::sleep(Duration::from_secs(60)); - } - } - "waiter" => { - fs::write(std::env::var_os(WAITER_ATTEMPTING).unwrap(), b"attempting") - .unwrap(); - let _waiter = store.install_lock().unwrap(); - fs::write(std::env::var_os(WAITER_ACQUIRED).unwrap(), b"acquired").unwrap(); - return; - } - other => panic!("unexpected child role: {other}"), - } - } - - let temp = tempfile::tempdir().unwrap(); - let owner_acquired = temp.path().join("store-lock-owner-acquired"); - let waiter_attempting = temp.path().join("store-lock-waiter-attempting-after-exit"); - let waiter_acquired = temp.path().join("store-lock-waiter-acquired-after-exit"); - - let mut owner = Command::new(std::env::current_exe().unwrap()) - .arg(TEST_NAME) - .arg("--exact") - .arg("--nocapture") - .env(CHILD_ROLE, "owner") - .env(CHILD_HOME, temp.path()) - .env(OWNER_ACQUIRED, &owner_acquired) - .stdin(Stdio::null()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .unwrap(); - - let owner_deadline = Instant::now() + Duration::from_secs(5); - while !owner_acquired.is_file() { - if let Some(status) = owner.try_wait().unwrap() { - panic!("store-lock owner exited before acquiring install.lock: {status}"); - } - if Instant::now() >= owner_deadline { - let _ = owner.kill(); - let _ = owner.wait(); - panic!("store-lock owner did not acquire install.lock"); - } - std::thread::sleep(Duration::from_millis(20)); - } - - let mut waiter = Command::new(std::env::current_exe().unwrap()) - .arg(TEST_NAME) - .arg("--exact") - .arg("--nocapture") - .env(CHILD_ROLE, "waiter") - .env(CHILD_HOME, temp.path()) - .env(WAITER_ATTEMPTING, &waiter_attempting) - .env(WAITER_ACQUIRED, &waiter_acquired) - .stdin(Stdio::null()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .unwrap(); - - let attempting_deadline = Instant::now() + Duration::from_secs(5); - while !waiter_attempting.is_file() { - if let Some(status) = waiter.try_wait().unwrap() { - let _ = owner.kill(); - let _ = owner.wait(); - panic!("store-lock waiter exited before attempting acquisition: {status}"); - } - if Instant::now() >= attempting_deadline { - let _ = waiter.kill(); - let _ = waiter.wait(); - let _ = owner.kill(); - let _ = owner.wait(); - panic!("store-lock waiter did not reach acquisition"); - } - std::thread::sleep(Duration::from_millis(20)); - } - - std::thread::sleep(Duration::from_millis(200)); - assert!( - !waiter_acquired.exists(), - "waiter acquired install.lock while the owner process was alive" - ); - assert!( - waiter.try_wait().unwrap().is_none(), - "store-lock waiter exited before owner termination" - ); - - owner.kill().unwrap(); - let owner_status = owner.wait().unwrap(); - assert!( - !owner_status.success(), - "owner process unexpectedly exited successfully instead of being terminated" - ); - - let release_deadline = Instant::now() + Duration::from_secs(5); - loop { - if let Some(status) = waiter.try_wait().unwrap() { - assert!(status.success(), "store-lock waiter failed after owner exit: {status}"); - break; - } - if Instant::now() >= release_deadline { - let _ = waiter.kill(); - let _ = waiter.wait(); - panic!("store-lock waiter did not wake after owner process termination"); - } - std::thread::sleep(Duration::from_millis(20)); - } - assert!( - waiter_acquired.is_file(), - "waiter exited without acquiring install.lock after owner termination" - ); - } - - #[test] - fn gc_survives_hostile_max_age() { -''' - if text.count(marker) != 1: - raise RuntimeError("could not locate insertion point for owner-exit lock test") - text = text.replace(marker, replacement, 1) - -store.write_text(text) From f84a7540f098a9f14537fa6ca8c69ef51eccc86c Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:11:22 -0500 Subject: [PATCH 54/80] ci: remove store-lock patch bootstrap --- .github/blocking_store_process_locks.py | 155 ------------------------ 1 file changed, 155 deletions(-) delete mode 100644 .github/blocking_store_process_locks.py diff --git a/.github/blocking_store_process_locks.py b/.github/blocking_store_process_locks.py deleted file mode 100644 index 0a237ecb..00000000 --- a/.github/blocking_store_process_locks.py +++ /dev/null @@ -1,155 +0,0 @@ -from pathlib import Path - - -store = Path("src/store.rs") -text = store.read_text() - -new_lock_marker = "/// A descriptor-backed process lock held for the life of the guard." -if new_lock_marker not in text: - old_import = "use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};" - new_import = "use std::time::{Duration, SystemTime, UNIX_EPOCH};" - if text.count(old_import) != 1: - raise RuntimeError("could not locate the store time import") - text = text.replace(old_import, new_import, 1) - - start_marker = "/// An advisory (flock-based) process lock held for the life of the guard." - end_marker = "/// What `Store::gc` did (or, with `dry_run`, would do)." - start = text.index(start_marker) - end = text.index(end_marker, start) - replacement = '''/// A descriptor-backed process lock held for the life of the guard. -/// -/// Acquisition uses the operating system's blocking lock primitive directly: -/// `flock`/`fcntl` semantics on Unix and `LockFileEx` semantics on Windows via -/// `fs2`. Contended callers sleep in the kernel and wake when the owner drops -/// the descriptor or exits. There is no retry timer, jitter loop, stale-file -/// reclamation, or userspace polling. -pub struct ProcessLock { - _file: fs::File, -} - -impl ProcessLock { - fn acquire(path: &Path, waiting_on: &str) -> Result { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - let file = fs::OpenOptions::new() - .create(true) - .read(true) - .write(true) - .truncate(false) - .open(path) - .with_context(|| format!("opening lock file {}", path.display()))?; - - file.lock_exclusive().with_context(|| { - format!( - "waiting for {waiting_on} through operating-system lock {}", - path.display() - ) - })?; - Ok(Self { _file: file }) - } -} - -''' - text = text[:start] + replacement + text[end:] - -new_test_name = "fn install_process_lock_blocks_without_polling_until_owner_release()" -if new_test_name not in text: - old_test_header = '''#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn gc_survives_hostile_max_age() { -''' - new_test_header = '''#[cfg(test)] -mod tests { - use std::process::{Command, Stdio}; - use std::time::Instant; - - use super::*; - - #[test] - fn install_process_lock_blocks_without_polling_until_owner_release() { - const CHILD_HOME: &str = "ZED_PKG_TEST_STORE_LOCK_CHILD_HOME"; - const CHILD_ATTEMPTING: &str = "ZED_PKG_TEST_STORE_LOCK_CHILD_ATTEMPTING"; - const CHILD_ACQUIRED: &str = "ZED_PKG_TEST_STORE_LOCK_CHILD_ACQUIRED"; - const TEST_NAME: &str = - "store::tests::install_process_lock_blocks_without_polling_until_owner_release"; - - if let Some(home) = std::env::var_os(CHILD_HOME) { - let attempting = PathBuf::from(std::env::var_os(CHILD_ATTEMPTING).unwrap()); - let acquired = PathBuf::from(std::env::var_os(CHILD_ACQUIRED).unwrap()); - fs::write(attempting, b"attempting").unwrap(); - let store = Store::new(Path::new(&home)); - let _waiter = store.install_lock().unwrap(); - fs::write(acquired, b"acquired").unwrap(); - return; - } - - let temp = tempfile::tempdir().unwrap(); - let store = Store::new(temp.path()); - let owner = store.install_lock().unwrap(); - let attempting = temp.path().join("store-lock-waiter-attempting"); - let acquired = temp.path().join("store-lock-waiter-acquired"); - let mut child = Command::new(std::env::current_exe().unwrap()) - .arg(TEST_NAME) - .arg("--exact") - .arg("--nocapture") - .env(CHILD_HOME, temp.path()) - .env(CHILD_ATTEMPTING, &attempting) - .env(CHILD_ACQUIRED, &acquired) - .stdin(Stdio::null()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .unwrap(); - - let attempting_deadline = Instant::now() + Duration::from_secs(5); - while !attempting.is_file() { - if let Some(status) = child.try_wait().unwrap() { - panic!("store-lock waiter exited before attempting acquisition: {status}"); - } - if Instant::now() >= attempting_deadline { - let _ = child.kill(); - let _ = child.wait(); - panic!("store-lock waiter did not reach acquisition"); - } - std::thread::sleep(Duration::from_millis(20)); - } - - std::thread::sleep(Duration::from_millis(200)); - assert!( - !acquired.exists(), - "a separate process acquired install.lock while the owner held it" - ); - assert!( - child.try_wait().unwrap().is_none(), - "store-lock waiter exited before owner release" - ); - - drop(owner); - let release_deadline = Instant::now() + Duration::from_secs(5); - loop { - if let Some(status) = child.try_wait().unwrap() { - assert!(status.success(), "store-lock waiter failed: {status}"); - break; - } - if Instant::now() >= release_deadline { - let _ = child.kill(); - let _ = child.wait(); - panic!("store-lock waiter did not wake after owner release"); - } - std::thread::sleep(Duration::from_millis(20)); - } - assert!(acquired.is_file(), "waiter exited without acquiring install.lock"); - } - - #[test] - fn gc_survives_hostile_max_age() { -''' - if text.count(old_test_header) != 1: - raise RuntimeError("could not locate store test module header") - text = text.replace(old_test_header, new_test_header, 1) - -store.write_text(text) From 3a36b527de3264f616b32b84a2ca35bec554aaac Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:12:22 -0500 Subject: [PATCH 55/80] ci: keep permanent Linux blocking-lock regression gate --- .../apply-blocking-store-process-locks.yml | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/.github/workflows/apply-blocking-store-process-locks.yml b/.github/workflows/apply-blocking-store-process-locks.yml index 6fb1f425..85a39020 100644 --- a/.github/workflows/apply-blocking-store-process-locks.yml +++ b/.github/workflows/apply-blocking-store-process-locks.yml @@ -1,70 +1,70 @@ -name: apply blocking store process locks +name: blocking store process locks on: pull_request: - branches: - - agent/recursive-lock-cross-platform + paths: + - src/store.rs + - src/install_graph.rs + - src/install_graph/** + - Cargo.toml + - Cargo.lock + - .github/workflows/apply-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/apply-blocking-store-process-locks.yml + workflow_dispatch: permissions: contents: read concurrency: - group: apply-blocking-store-process-locks-${{ github.event.pull_request.number }} + 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: - apply: - if: github.actor != 'github-actions[bot]' + linux-kernel-lock: + name: Linux process-lock and recursive-cache contracts runs-on: ubuntu-24.04 - timeout-minutes: 45 - permissions: - contents: write + timeout-minutes: 35 steps: - - name: Check out store-lock branch + - name: Check out zed-cli uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: agent/blocking-store-process-locks - fetch-depth: 0 + fetch-depth: 1 persist-credentials: false show-progress: false - - name: Apply exact kernel-blocking lock patch + - name: Install stable Rust with formatting and lint support run: | - set -euo pipefail - python3 .github/blocking_store_process_locks.py - python3 .github/add_owner_exit_lock_test.py - cargo fmt --all - git diff --check + rustup toolchain install stable --profile minimal --component rustfmt,clippy + rustup default stable + rustc --version --verbose + cargo --version --verbose - - name: Validate store and recursive lock contracts - run: | - set -euo pipefail - rustup component add rustfmt clippy - cargo fmt --all --check - cargo test --locked --lib \ - store::tests::install_process_lock_blocks_without_polling_until_owner_release \ - -- --exact --nocapture - cargo test --locked --lib \ - store::tests::install_process_lock_is_released_when_owner_process_is_terminated \ - -- --exact --nocapture - cargo test --locked --lib store::tests - cargo test --locked --lib install_graph::tests - cargo clippy --locked --all-targets -- -D warnings + - name: Check formatting + run: cargo fmt --all --check - - name: Publish validated store implementation - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - git add src/store.rs - git diff --cached --check - if git diff --cached --quiet; then - echo 'kernel-blocking store implementation is already published' - exit 0 - fi - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m 'fix: block store locks in the kernel' - git push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ - HEAD:agent/blocking-store-process-locks + - 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 From 3e0f6e6c8c62106b60f8221a275dace88f213373 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:21:11 -0500 Subject: [PATCH 56/80] fix: validate dependency identities and embedded manifests --- src/install_graph/artifact.rs | 40 +++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/install_graph/artifact.rs b/src/install_graph/artifact.rs index 1912e673..fe4b051e 100644 --- a/src/install_graph/artifact.rs +++ b/src/install_graph/artifact.rs @@ -52,12 +52,38 @@ where FetchMessage { sequence, result } } -fn prefetch_one(registry: &dyn Registry, store: &Store, task: FetchTask) -> Result { +pub(super) fn prefetch_one( + registry: &dyn Registry, + store: &Store, + task: FetchTask, +) -> Result { let (package_dir, downloaded) = ensure_artifact(registry, store, &task.version) .with_context(|| format!("prefetching {}@{}", task.key, task.version.version))?; - let dependencies = read_manifest(&package_dir) - .map(|manifest| manifest.dependencies) - .unwrap_or_default(); + let manifest_path = package_dir.join(MANIFEST_FILE); + let dependencies = if manifest_path.is_file() { + let manifest = read_manifest(&package_dir).with_context(|| { + format!( + "reading dependency manifest for {}@{} from {}", + task.key, + task.version.version, + manifest_path.display() + ) + })?; + if manifest.full_name() != task.key || manifest.package.version != task.version.version { + bail!( + "artifact manifest declares {}@{} while registry metadata selected {}@{}; refusing transitive dependency expansion", + manifest.full_name(), + manifest.package.version, + task.key, + task.version.version + ); + } + manifest.dependencies + } else { + // Keep legacy manifestless artifacts installable as leaves, but never + // hide a malformed or identity-mismatched manifest when one is present. + BTreeMap::new() + }; Ok(FetchResult { dependencies, downloaded, @@ -157,10 +183,12 @@ fn download_atomic( pub(super) fn split_key(key: &str) -> Result<(String, String)> { let mut parts = key.splitn(2, '/'); match (parts.next(), parts.next()) { - (Some(org), Some(name)) if !org.is_empty() && !name.is_empty() => { + (Some(org), Some(name)) if is_slug(org) && is_slug(name) => { Ok((org.to_string(), name.to_string())) } - _ => bail!("invalid package spec `{key}` (expected org/name)"), + _ => bail!( + "invalid package spec `{key}` (expected slug/slug without path traversal or extra segments)" + ), } } From c51c5013a4cb44d75111c9a89dd401bb918b141a Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:21:42 -0500 Subject: [PATCH 57/80] fix: preserve lockfile order for frozen worker failures --- src/install_graph/resolver.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/install_graph/resolver.rs b/src/install_graph/resolver.rs index 7fc169e4..dd41abd4 100644 --- a/src/install_graph/resolver.rs +++ b/src/install_graph/resolver.rs @@ -105,9 +105,9 @@ fn prefetch_locked( pool.submit(task)?; } let mut downloaded = 0usize; - for _ in 0..total { - let message = pool.receive()?; - let result = message.result?; + let mut buffered_results: BTreeMap> = BTreeMap::new(); + for expected in 0..total { + let result = receive_in_order(pool, &mut buffered_results, expected)?; downloaded += usize::from(result.downloaded); } Ok(PrefetchReport { @@ -208,7 +208,7 @@ fn prefetch_recursive( }) } -fn receive_in_order( +pub(super) fn receive_in_order( pool: &FetchPool, buffered: &mut BTreeMap>, expected: usize, From dc6544e3fd343f97cf518a33ebb73f413eb0c8d4 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:22:36 -0500 Subject: [PATCH 58/80] test: harden recursive manifest, identity, slug, and ordering contracts --- src/install_graph/hardening_tests.rs | 150 +++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/install_graph/hardening_tests.rs diff --git a/src/install_graph/hardening_tests.rs b/src/install_graph/hardening_tests.rs new file mode 100644 index 00000000..078fc7b0 --- /dev/null +++ b/src/install_graph/hardening_tests.rs @@ -0,0 +1,150 @@ +use anyhow::anyhow; + +use super::*; +use crate::pack::pack; + +fn manifest_text(org: &str, name: &str, version: &str) -> String { + format!( + r#"[package] +org = "{org}" +name = "{name}" +version = "{version}" + +[package.repository] +vcs = "git" +url = "https://example.invalid/{org}/{name}" +"#, + ) +} + +fn prepared_artifact( + temp: &tempfile::TempDir, + stored_manifest: Option<&str>, +) -> (Store, FetchTask, Box) { + let source = temp.path().join("source"); + fs::create_dir_all(&source).unwrap(); + let selected_manifest = manifest_text("test", "selected", "1.0.0"); + fs::write(source.join(MANIFEST_FILE), &selected_manifest).unwrap(); + fs::write(source.join("payload.txt"), b"fixture\n").unwrap(); + let manifest = Manifest::parse(&selected_manifest).unwrap(); + let packed = pack(&source, &manifest, Some(&temp.path().join("packed"))).unwrap(); + + let store = Store::new(&temp.path().join("home")); + let package_dir = store + .add_artifact(&packed.path, &packed.sha256) + .unwrap(); + match stored_manifest { + Some(text) => fs::write(package_dir.join(MANIFEST_FILE), text).unwrap(), + None => fs::remove_file(package_dir.join(MANIFEST_FILE)).unwrap(), + } + + let task = FetchTask { + sequence: 0, + key: "test/selected".to_string(), + version: VersionMetadata { + org: "test".to_string(), + name: "selected".to_string(), + version: "1.0.0".to_string(), + sha256: packed.sha256, + size: packed.size, + format: packed.format, + vcs_tag: "v1.0.0".to_string(), + vcs_commit: Some("0123456789abcdef0123456789abcdef01234567".to_string()), + download_url: "file:///unused-artifact.tar.gz".to_string(), + published_at: "1970-01-01T00:00:00Z".to_string(), + yanked: false, + }, + }; + + let registry_root = temp.path().join("registry"); + fs::create_dir_all(®istry_root).unwrap(); + let registry = registry_for(&format!("file://{}", registry_root.display())).unwrap(); + (store, task, registry) +} + +#[test] +fn dependency_keys_are_slug_validated_before_registry_access() { + for key in ["../escape", "test/../escape", "test/name/extra", "/name", "org/"] { + let error = super::artifact::split_key(key).unwrap_err().to_string(); + assert!(error.contains("invalid package spec"), "{key}: {error}"); + } + + assert_eq!( + super::artifact::split_key("valid-org/valid-name").unwrap(), + ("valid-org".to_string(), "valid-name".to_string()) + ); +} + +#[test] +fn malformed_embedded_manifests_fail_closed_instead_of_truncating_the_graph() { + let temp = tempfile::tempdir().unwrap(); + let (store, task, registry) = prepared_artifact(&temp, Some("[package\ninvalid = true\n")); + + let error = super::artifact::prefetch_one(registry.as_ref(), &store, task) + .unwrap_err(); + let message = format!("{error:#}"); + assert!(message.contains("reading dependency manifest"), "{message}"); + assert!(message.contains("test/selected@1.0.0"), "{message}"); +} + +#[test] +fn embedded_manifest_identity_must_match_selected_registry_metadata() { + let temp = tempfile::tempdir().unwrap(); + let mismatched = manifest_text("test", "different", "2.0.0"); + let (store, task, registry) = prepared_artifact(&temp, Some(&mismatched)); + + let error = super::artifact::prefetch_one(registry.as_ref(), &store, task) + .unwrap_err() + .to_string(); + assert!( + error.contains("artifact manifest declares test/different@2.0.0"), + "{error}" + ); + assert!(error.contains("test/selected@1.0.0"), "{error}"); +} + +#[test] +fn legacy_manifestless_artifacts_remain_valid_leaf_packages() { + let temp = tempfile::tempdir().unwrap(); + let (store, task, registry) = prepared_artifact(&temp, None); + + let result = super::artifact::prefetch_one(registry.as_ref(), &store, task).unwrap(); + assert!(result.dependencies.is_empty()); + assert!(!result.downloaded); +} + +#[test] +fn sequenced_receive_buffers_later_failures_until_earlier_results_arrive() { + let queue = Arc::new(TaskQueue::default()); + let (sender, results) = mpsc::channel(); + let pool = FetchPool { + queue, + results, + workers: Vec::new(), + }; + + sender + .send(FetchMessage { + sequence: 1, + result: Err(anyhow!("later lockfile entry failed first in wall-clock time")), + }) + .unwrap(); + sender + .send(FetchMessage { + sequence: 0, + result: Ok(FetchResult { + dependencies: BTreeMap::new(), + downloaded: false, + }), + }) + .unwrap(); + drop(sender); + + let mut buffered = BTreeMap::new(); + let first = super::resolver::receive_in_order(&pool, &mut buffered, 0).unwrap(); + assert!(first.dependencies.is_empty()); + let error = super::resolver::receive_in_order(&pool, &mut buffered, 1) + .unwrap_err() + .to_string(); + assert!(error.contains("later lockfile entry failed"), "{error}"); +} From ddef3e4aba8c856dce715a14d8d026a546c9b08d Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:23:21 -0500 Subject: [PATCH 59/80] test: register recursive resolver hardening suite --- src/install_graph.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/install_graph.rs b/src/install_graph.rs index 2e5318b9..59452c14 100644 --- a/src/install_graph.rs +++ b/src/install_graph.rs @@ -292,6 +292,8 @@ impl WorkspaceGraph { } mod artifact; +#[cfg(test)] +mod hardening_tests; mod resolver; #[cfg(test)] mod tests; From cea860bec5fe87727d739a976aef977e5f4c1fef Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 12:25:14 -0500 Subject: [PATCH 60/80] style: apply Rustfmt to resolver hardening tests --- src/install_graph/hardening_tests.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/install_graph/hardening_tests.rs b/src/install_graph/hardening_tests.rs index 078fc7b0..0c248cdd 100644 --- a/src/install_graph/hardening_tests.rs +++ b/src/install_graph/hardening_tests.rs @@ -30,9 +30,7 @@ fn prepared_artifact( let packed = pack(&source, &manifest, Some(&temp.path().join("packed"))).unwrap(); let store = Store::new(&temp.path().join("home")); - let package_dir = store - .add_artifact(&packed.path, &packed.sha256) - .unwrap(); + let package_dir = store.add_artifact(&packed.path, &packed.sha256).unwrap(); match stored_manifest { Some(text) => fs::write(package_dir.join(MANIFEST_FILE), text).unwrap(), None => fs::remove_file(package_dir.join(MANIFEST_FILE)).unwrap(), @@ -64,7 +62,13 @@ fn prepared_artifact( #[test] fn dependency_keys_are_slug_validated_before_registry_access() { - for key in ["../escape", "test/../escape", "test/name/extra", "/name", "org/"] { + for key in [ + "../escape", + "test/../escape", + "test/name/extra", + "/name", + "org/", + ] { let error = super::artifact::split_key(key).unwrap_err().to_string(); assert!(error.contains("invalid package spec"), "{key}: {error}"); } @@ -80,8 +84,7 @@ fn malformed_embedded_manifests_fail_closed_instead_of_truncating_the_graph() { let temp = tempfile::tempdir().unwrap(); let (store, task, registry) = prepared_artifact(&temp, Some("[package\ninvalid = true\n")); - let error = super::artifact::prefetch_one(registry.as_ref(), &store, task) - .unwrap_err(); + let error = super::artifact::prefetch_one(registry.as_ref(), &store, task).unwrap_err(); let message = format!("{error:#}"); assert!(message.contains("reading dependency manifest"), "{message}"); assert!(message.contains("test/selected@1.0.0"), "{message}"); @@ -126,7 +129,9 @@ fn sequenced_receive_buffers_later_failures_until_earlier_results_arrive() { sender .send(FetchMessage { sequence: 1, - result: Err(anyhow!("later lockfile entry failed first in wall-clock time")), + result: Err(anyhow!( + "later lockfile entry failed first in wall-clock time" + )), }) .unwrap(); sender From e16fe93fc8eb7d8d8434cc57dd79bfe3ac5d0fc8 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 13:05:37 -0500 Subject: [PATCH 61/80] ci: synchronize final cross-platform lock ancestry --- .github/workflows/sync-lock-stack.yml | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/sync-lock-stack.yml diff --git a/.github/workflows/sync-lock-stack.yml b/.github/workflows/sync-lock-stack.yml new file mode 100644 index 00000000..05fcbce9 --- /dev/null +++ b/.github/workflows/sync-lock-stack.yml @@ -0,0 +1,68 @@ +name: synchronize blocking-lock stack + +on: + push: + branches: + - agent/blocking-store-process-locks + paths: + - .github/workflows/sync-lock-stack.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: synchronize-blocking-lock-stack + cancel-in-progress: true + +jobs: + merge-final-cross-platform-base: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - name: Check out the blocking-lock branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: agent/blocking-store-process-locks + fetch-depth: 0 + persist-credentials: false + show-progress: false + + - name: Merge the final cross-platform base semantically + env: + GH_TOKEN: ${{ github.token }} + BASE_SHA: b7893ae396d20badeeb56ba36019edfa69e6d0e0 + run: | + set -euo pipefail + if git merge-base --is-ancestor "$BASE_SHA" HEAD; then + echo "cross-platform lock base already present" + exit 0 + fi + + git fetch --no-tags origin agent/recursive-lock-cross-platform + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + + set +e + git merge --no-ff --no-commit FETCH_HEAD + merge_status=$? + set -e + + if [ "$merge_status" -ne 0 ]; then + conflicts="$(git diff --name-only --diff-filter=U)" + test "$conflicts" = '.github/workflows/recursive-install-windows.yml' + git checkout --ours -- .github/workflows/recursive-install-windows.yml + grep -F 'cancel-in-progress: true' .github/workflows/recursive-install-windows.yml + grep -F 'owner termination releases the lower-level store lock' .github/workflows/recursive-install-windows.yml + git add .github/workflows/recursive-install-windows.yml + fi + + test -z "$(git diff --name-only --diff-filter=U)" + git diff --check + git commit -m 'merge: synchronize cross-platform lock base' + git push \ + "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ + HEAD:agent/blocking-store-process-locks From a1009fbafff377c5f460a8330c3cf5cd7090fb0d Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 13:07:40 -0500 Subject: [PATCH 62/80] merge: align conflicted workflow with final cross-platform base --- .github/workflows/recursive-install-windows.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/recursive-install-windows.yml b/.github/workflows/recursive-install-windows.yml index eda09e0f..8fdb860c 100644 --- a/.github/workflows/recursive-install-windows.yml +++ b/.github/workflows/recursive-install-windows.yml @@ -79,9 +79,3 @@ jobs: - 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 From 19c962ec82b77ec6387cf0123959789cd0c7488b Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 13:08:17 -0500 Subject: [PATCH 63/80] ci: remove completed lock-stack synchronization workflow --- .github/workflows/sync-lock-stack.yml | 68 --------------------------- 1 file changed, 68 deletions(-) delete mode 100644 .github/workflows/sync-lock-stack.yml diff --git a/.github/workflows/sync-lock-stack.yml b/.github/workflows/sync-lock-stack.yml deleted file mode 100644 index 05fcbce9..00000000 --- a/.github/workflows/sync-lock-stack.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: synchronize blocking-lock stack - -on: - push: - branches: - - agent/blocking-store-process-locks - paths: - - .github/workflows/sync-lock-stack.yml - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: synchronize-blocking-lock-stack - cancel-in-progress: true - -jobs: - merge-final-cross-platform-base: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 - timeout-minutes: 10 - permissions: - contents: write - steps: - - name: Check out the blocking-lock branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: agent/blocking-store-process-locks - fetch-depth: 0 - persist-credentials: false - show-progress: false - - - name: Merge the final cross-platform base semantically - env: - GH_TOKEN: ${{ github.token }} - BASE_SHA: b7893ae396d20badeeb56ba36019edfa69e6d0e0 - run: | - set -euo pipefail - if git merge-base --is-ancestor "$BASE_SHA" HEAD; then - echo "cross-platform lock base already present" - exit 0 - fi - - git fetch --no-tags origin agent/recursive-lock-cross-platform - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - - set +e - git merge --no-ff --no-commit FETCH_HEAD - merge_status=$? - set -e - - if [ "$merge_status" -ne 0 ]; then - conflicts="$(git diff --name-only --diff-filter=U)" - test "$conflicts" = '.github/workflows/recursive-install-windows.yml' - git checkout --ours -- .github/workflows/recursive-install-windows.yml - grep -F 'cancel-in-progress: true' .github/workflows/recursive-install-windows.yml - grep -F 'owner termination releases the lower-level store lock' .github/workflows/recursive-install-windows.yml - git add .github/workflows/recursive-install-windows.yml - fi - - test -z "$(git diff --name-only --diff-filter=U)" - git diff --check - git commit -m 'merge: synchronize cross-platform lock base' - git push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ - HEAD:agent/blocking-store-process-locks From 1232fc19126b96f8e8cfa8585e92b247a1ce693c Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 13:08:38 -0500 Subject: [PATCH 64/80] test: restore expanded Windows store-lock regressions after base sync --- .github/workflows/recursive-install-windows.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/recursive-install-windows.yml b/.github/workflows/recursive-install-windows.yml index 8fdb860c..eda09e0f 100644 --- a/.github/workflows/recursive-install-windows.yml +++ b/.github/workflows/recursive-install-windows.yml @@ -79,3 +79,9 @@ jobs: - 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 From 09f2f6c3813721d145537334071ec33e96b82919 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 15:00:58 -0500 Subject: [PATCH 65/80] test: stage workspace adapter and frozen-install implementation --- .github/workspace_install_wiring.py | 355 ++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 .github/workspace_install_wiring.py diff --git a/.github/workspace_install_wiring.py b/.github/workspace_install_wiring.py new file mode 100644 index 00000000..31238bf8 --- /dev/null +++ b/.github/workspace_install_wiring.py @@ -0,0 +1,355 @@ +from pathlib import Path + + +path = Path("src/ops.rs") +text = path.read_text(encoding="utf-8") + + +def replace_once(old: str, new: str, label: str) -> None: + global text + if new in text: + return + count = text.count(old) + if count != 1: + raise RuntimeError(f"{label}: expected one match, found {count}") + text = text.replace(old, new, 1) + + +workspace_helper = r'''fn collect_workspace_links_for_frozen( + project: &Path, + manifest: &Manifest, + workspace: Option<&WorkspaceInfo>, +) -> Result> { + let Some(workspace) = workspace else { + return Ok(BTreeMap::new()); + }; + + let mut links = BTreeMap::new(); + let mut pending: VecDeque<(String, String)> = manifest + .dependencies + .iter() + .map(|(key, requirement)| (key.clone(), requirement.clone())) + .collect(); + + while let Some((raw_key, requirement_text)) = pending.pop_front() { + let (org, name) = split_key(&raw_key)?; + let key = format!("{org}/{name}"); + let Some(member_dir) = workspace.members.get(&key) else { + continue; + }; + let member_manifest = read_manifest(member_dir).with_context(|| { + format!( + "reading workspace member `{key}` from {}", + member_dir.display() + ) + })?; + let requirement = Requirement::parse(&requirement_text); + if !requirement.matches(&member_manifest.package.version) { + bail!( + "workspace member {key}@{} does not satisfy `{requirement_text}`", + member_manifest.package.version + ); + } + if member_dir == project || links.contains_key(&key) { + continue; + } + links.insert(key, member_dir.clone()); + pending.extend(member_manifest.dependencies); + } + + Ok(links) +} + +''' +replace_once( + "// ---------------------------------------------------------------------------\n// install\n", + workspace_helper + "// ---------------------------------------------------------------------------\n// install\n", + "insert frozen workspace resolver", +) + +replace_once( + r''' validate_frozen_manifest_requirements( + &manifest, + &lock, + workspace.as_ref(), + validate_manifest_requirements, + )?; + for locked in &lock.packages { +''', + r''' validate_frozen_manifest_requirements( + &manifest, + &lock, + workspace.as_ref(), + validate_manifest_requirements, + )?; + workspace_links = + collect_workspace_links_for_frozen(project, &manifest, workspace.as_ref())?; + for locked in &lock.packages { +''', + "restore workspace members during frozen installs", +) + +replace_once( + r''' if let Some(ws) = &workspace + && let Some(member_dir) = ws.members.get(&key) + { + if member_dir != project && !workspace_links.contains_key(&key) { + workspace_links.insert(key.clone(), member_dir.clone()); + if let Ok(member_manifest) = read_manifest(member_dir) { + for (sub_key, sub_req) in member_manifest.dependencies { + let (sub_org, sub_name) = split_key(&sub_key)?; + queue.push_back((sub_org, sub_name, sub_req)); + } + } + } + continue; + } +''', + r''' if let Some(ws) = &workspace + && let Some(member_dir) = ws.members.get(&key) + { + let member_manifest = read_manifest(member_dir).with_context(|| { + format!( + "reading workspace member `{key}` from {}", + member_dir.display() + ) + })?; + let requirement = Requirement::parse(&req_str); + if !requirement.matches(&member_manifest.package.version) { + bail!( + "workspace member {key}@{} does not satisfy `{req_str}`", + member_manifest.package.version + ); + } + if member_dir != project && !workspace_links.contains_key(&key) { + workspace_links.insert(key.clone(), member_dir.clone()); + for (sub_key, sub_req) in member_manifest.dependencies { + let (sub_org, sub_name) = split_key(&sub_key)?; + queue.push_back((sub_org, sub_name, sub_req)); + } + } + continue; + } +''', + "validate mutable workspace members", +) + +replace_once( + r''' for (key, member_dir) in &workspace_links { + interactive::confirm(cfg.interactive, &format!("link workspace package {key}"))?; + let (org, name) = split_key(key)?; + let dest = modules.join(&org).join(&name); + // Workspace dependencies obey the same ownership decision as registry + // packages. Copy mode must remain self-contained after the member source + // directory leaves the Docker/OCI build context. + link_or_copy(member_dir, &dest, mode)?; + if let Ok(member_manifest) = read_manifest(member_dir) { + for (bin_name, rel_target) in &member_manifest.bin { + bins.insert(bin_name.clone(), dest.join(rel_target)); + } + } + installed.push((key.clone(), "workspace".to_string())); + } +''', + r''' for (key, member_dir) in &workspace_links { + interactive::confirm(cfg.interactive, &format!("link workspace package {key}"))?; + let (org, name) = split_key(key)?; + let member_manifest = read_manifest(member_dir).with_context(|| { + format!( + "reading workspace member `{key}` from {}", + member_dir.display() + ) + })?; + if !allow_ecosystem_mismatch + && let Some(problem) = ecosystem_mismatch( + key, + &name, + member_manifest.package.language, + member_manifest.package.ecosystem(), + &project_ecos, + ) + { + bail!("{problem}"); + } + + let dest = modules.join(&org).join(&name); + // Workspace dependencies obey the same ownership decision as registry + // packages. Copy mode must remain self-contained after the member source + // directory leaves the Docker/OCI build context. + link_or_copy(member_dir, &dest, mode)?; + for (bin_name, rel_target) in &member_manifest.bin { + bins.insert(bin_name.clone(), dest.join(rel_target)); + } + + let dependency_adapter = if use_dependency_adapters { + member_manifest + .install + .adapter + .as_deref() + .map(named_adapter) + .transpose()? + .unwrap_or(adapter) + } else { + adapter + }; + match dependency_adapter { + Adapter::Node => { + used_node_adapter = true; + let node_dest = project + .join("node_modules") + .join(format!("@{org}")) + .join(&name); + transaction.backup(&node_dest)?; + link_or_copy(member_dir, &node_dest, mode)?; + } + Adapter::Java => { + used_java_adapter = true; + for entry in walkdir::WalkDir::new(&dest) + .follow_links(true) + .into_iter() + .filter_map(|entry| entry.ok()) + { + if entry.path().extension().is_some_and(|extension| extension == "jar") { + jars.push(entry.path().to_string_lossy().to_string()); + } + } + } + Adapter::Go => wired_roots + .entry(Adapter::Go) + .or_default() + .push(dest.clone()), + Adapter::Python => wired_roots + .entry(Adapter::Python) + .or_default() + .push(dest.clone()), + Adapter::Rust => wired_roots + .entry(Adapter::Rust) + .or_default() + .push(dest.clone()), + Adapter::Dart => wired_roots + .entry(Adapter::Dart) + .or_default() + .push(dest.clone()), + Adapter::Auto | Adapter::None => {} + } + wired_packages.push(WiredPackage { + key: key.clone(), + version: "workspace".to_string(), + language: member_manifest.package.language, + ecosystem: member_manifest.package.ecosystem(), + path: dest, + }); + installed.push((key.clone(), "workspace".to_string())); + } +''', + "wire workspace packages through native adapters", +) + +replace_once( + r''' #[test] + fn lock_only_frozen_restore_skips_only_the_missing_manifest_comparison() { +''', + r''' #[test] + fn frozen_workspace_resolution_expands_transitive_members_and_validates_versions() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path(); + let app = root.join("apps/cli"); + let utils = root.join("packages/utils"); + let core = root.join("packages/core"); + for directory in [&app, &utils, &core] { + fs::create_dir_all(directory).unwrap(); + } + + fs::write( + root.join(MANIFEST_FILE), + r#"[package] +org = "zedtest" +name = "workspace-root" +version = "1.0.0" + +[package.repository] +vcs = "git" +url = "https://example.invalid/workspace-root" + +[workspace] +members = ["packages/*", "apps/*"] +"#, + ) + .unwrap(); + fs::write( + core.join(MANIFEST_FILE), + r#"[package] +org = "zedtest" +name = "ws-core" +version = "1.2.0" + +[package.repository] +vcs = "git" +url = "https://example.invalid/ws-core" +"#, + ) + .unwrap(); + fs::write( + utils.join(MANIFEST_FILE), + r#"[package] +org = "zedtest" +name = "ws-utils" +version = "1.1.0" + +[package.repository] +vcs = "git" +url = "https://example.invalid/ws-utils" + +[dependencies] +"zedtest/ws-core" = "^1" +"#, + ) + .unwrap(); + fs::write( + app.join(MANIFEST_FILE), + r#"[package] +org = "zedtest" +name = "ws-cli" +version = "1.0.0" + +[package.repository] +vcs = "git" +url = "https://example.invalid/ws-cli" + +[dependencies] +"zedtest/ws-utils" = "^1" +"#, + ) + .unwrap(); + + let manifest = read_manifest(&app).unwrap(); + let workspace = find_workspace(&app).unwrap(); + let links = + collect_workspace_links_for_frozen(&app, &manifest, Some(&workspace)).unwrap(); + assert_eq!( + links.keys().cloned().collect::>(), + vec!["zedtest/ws-core".to_string(), "zedtest/ws-utils".to_string()] + ); + assert_eq!(links["zedtest/ws-core"], core); + assert_eq!(links["zedtest/ws-utils"], utils); + + let incompatible = manifest + .to_toml_string() + .unwrap() + .replace("\"^1\"", "\"^2\""); + fs::write(app.join(MANIFEST_FILE), incompatible).unwrap(); + let manifest = read_manifest(&app).unwrap(); + let error = collect_workspace_links_for_frozen(&app, &manifest, Some(&workspace)) + .unwrap_err() + .to_string(); + assert!(error.contains("ws-utils@1.1.0"), "{error}"); + assert!(error.contains("does not satisfy `^2`"), "{error}"); + } + + #[test] + fn lock_only_frozen_restore_skips_only_the_missing_manifest_comparison() { +''', + "add frozen workspace graph tests", +) + +path.write_text(text, encoding="utf-8") From 9de1d32fde7f3dfa24b8c21fd70908e5535d6030 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 15:01:19 -0500 Subject: [PATCH 66/80] ci: validate and publish workspace install wiring --- .../apply-workspace-install-wiring.yml | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/apply-workspace-install-wiring.yml diff --git a/.github/workflows/apply-workspace-install-wiring.yml b/.github/workflows/apply-workspace-install-wiring.yml new file mode 100644 index 00000000..9d3b05ee --- /dev/null +++ b/.github/workflows/apply-workspace-install-wiring.yml @@ -0,0 +1,95 @@ +name: apply workspace install wiring + +on: + push: + branches: + - agent/workspace-install-wiring + paths: + - .github/workspace_install_wiring.py + - .github/workflows/apply-workspace-install-wiring.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: apply-workspace-install-wiring + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: "1" + +jobs: + apply-and-validate: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + permissions: + contents: write + steps: + - name: Check out workspace-wiring branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: agent/workspace-install-wiring + path: zed-cli + fetch-depth: 0 + 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 and lint support + run: | + set -euo pipefail + rustup toolchain install stable --profile minimal --component rustfmt,clippy + rustup default stable + rustc --version --verbose + cargo --version --verbose + + - name: Apply exact workspace install patch + working-directory: zed-cli + run: | + set -euo pipefail + python3 .github/workspace_install_wiring.py + cargo fmt --all + git diff --check + + - name: Validate workspace and install contracts + run: | + set -euo pipefail + cargo fmt --all --check --manifest-path zed-cli/Cargo.toml + cargo test --locked --manifest-path zed-cli/Cargo.toml --lib \ + ops::tests::frozen_workspace_resolution_expands_transitive_members_and_validates_versions \ + -- --exact --nocapture + cargo test --locked --manifest-path zed-cli/Cargo.toml --lib ops::tests + cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::tests + cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::hardening_tests + cargo clippy --locked --manifest-path zed-cli/Cargo.toml --all-targets -- -D warnings + + - name: Publish validated workspace install implementation + working-directory: zed-cli + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git add src/ops.rs + git diff --cached --check + if git diff --cached --quiet; then + echo 'workspace install implementation is already published' + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git commit -m 'fix: wire and restore workspace packages' + git push \ + "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ + HEAD:agent/workspace-install-wiring From 62954bce2db32f939e229b06c03ed6a80c513e16 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 15:02:28 -0500 Subject: [PATCH 67/80] ci: trigger workspace patch validation from the stacked PR --- .github/workflows/apply-workspace-install-wiring.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/apply-workspace-install-wiring.yml b/.github/workflows/apply-workspace-install-wiring.yml index 9d3b05ee..4a42066b 100644 --- a/.github/workflows/apply-workspace-install-wiring.yml +++ b/.github/workflows/apply-workspace-install-wiring.yml @@ -7,13 +7,20 @@ on: paths: - .github/workspace_install_wiring.py - .github/workflows/apply-workspace-install-wiring.yml + pull_request: + branches: + - agent/recursive-resolver-input-hardening + paths: + - .github/workspace_install_wiring.py + - .github/workflows/apply-workspace-install-wiring.yml + - src/ops.rs workflow_dispatch: permissions: contents: read concurrency: - group: apply-workspace-install-wiring + group: apply-workspace-install-wiring-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: From 235ef28c24d9b2ae4760b1446b8de9cb26ab27c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:05:16 +0000 Subject: [PATCH 68/80] fix: wire and restore workspace packages --- src/ops.rs | 252 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 244 insertions(+), 8 deletions(-) diff --git a/src/ops.rs b/src/ops.rs index 8d4696bf..bab67e96 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -219,6 +219,51 @@ fn collect_members(root: &Path, globs: &[String]) -> WorkspaceInfo { info } +fn collect_workspace_links_for_frozen( + project: &Path, + manifest: &Manifest, + workspace: Option<&WorkspaceInfo>, +) -> Result> { + let Some(workspace) = workspace else { + return Ok(BTreeMap::new()); + }; + + let mut links = BTreeMap::new(); + let mut pending: VecDeque<(String, String)> = manifest + .dependencies + .iter() + .map(|(key, requirement)| (key.clone(), requirement.clone())) + .collect(); + + while let Some((raw_key, requirement_text)) = pending.pop_front() { + let (org, name) = split_key(&raw_key)?; + let key = format!("{org}/{name}"); + let Some(member_dir) = workspace.members.get(&key) else { + continue; + }; + let member_manifest = read_manifest(member_dir).with_context(|| { + format!( + "reading workspace member `{key}` from {}", + member_dir.display() + ) + })?; + let requirement = Requirement::parse(&requirement_text); + if !requirement.matches(&member_manifest.package.version) { + bail!( + "workspace member {key}@{} does not satisfy `{requirement_text}`", + member_manifest.package.version + ); + } + if member_dir == project || links.contains_key(&key) { + continue; + } + links.insert(key, member_dir.clone()); + pending.extend(member_manifest.dependencies); + } + + Ok(links) +} + // --------------------------------------------------------------------------- // install @@ -1023,6 +1068,8 @@ fn install_locked( workspace.as_ref(), validate_manifest_requirements, )?; + workspace_links = + collect_workspace_links_for_frozen(project, &manifest, workspace.as_ref())?; for locked in &lock.packages { if !is_slug(&locked.org) || !is_slug(&locked.name) { bail!( @@ -1058,13 +1105,24 @@ fn install_locked( if let Some(ws) = &workspace && let Some(member_dir) = ws.members.get(&key) { + let member_manifest = read_manifest(member_dir).with_context(|| { + format!( + "reading workspace member `{key}` from {}", + member_dir.display() + ) + })?; + let requirement = Requirement::parse(&req_str); + if !requirement.matches(&member_manifest.package.version) { + bail!( + "workspace member {key}@{} does not satisfy `{req_str}`", + member_manifest.package.version + ); + } if member_dir != project && !workspace_links.contains_key(&key) { workspace_links.insert(key.clone(), member_dir.clone()); - if let Ok(member_manifest) = read_manifest(member_dir) { - for (sub_key, sub_req) in member_manifest.dependencies { - let (sub_org, sub_name) = split_key(&sub_key)?; - queue.push_back((sub_org, sub_name, sub_req)); - } + for (sub_key, sub_req) in member_manifest.dependencies { + let (sub_org, sub_name) = split_key(&sub_key)?; + queue.push_back((sub_org, sub_name, sub_req)); } } continue; @@ -1316,16 +1374,95 @@ fn install_locked( for (key, member_dir) in &workspace_links { interactive::confirm(cfg.interactive, &format!("link workspace package {key}"))?; let (org, name) = split_key(key)?; + let member_manifest = read_manifest(member_dir).with_context(|| { + format!( + "reading workspace member `{key}` from {}", + member_dir.display() + ) + })?; + if !allow_ecosystem_mismatch + && let Some(problem) = ecosystem_mismatch( + key, + &name, + member_manifest.package.language, + member_manifest.package.ecosystem(), + &project_ecos, + ) + { + bail!("{problem}"); + } + let dest = modules.join(&org).join(&name); // Workspace dependencies obey the same ownership decision as registry // packages. Copy mode must remain self-contained after the member source // directory leaves the Docker/OCI build context. link_or_copy(member_dir, &dest, mode)?; - if let Ok(member_manifest) = read_manifest(member_dir) { - for (bin_name, rel_target) in &member_manifest.bin { - bins.insert(bin_name.clone(), dest.join(rel_target)); + for (bin_name, rel_target) in &member_manifest.bin { + bins.insert(bin_name.clone(), dest.join(rel_target)); + } + + let dependency_adapter = if use_dependency_adapters { + member_manifest + .install + .adapter + .as_deref() + .map(named_adapter) + .transpose()? + .unwrap_or(adapter) + } else { + adapter + }; + match dependency_adapter { + Adapter::Node => { + used_node_adapter = true; + let node_dest = project + .join("node_modules") + .join(format!("@{org}")) + .join(&name); + transaction.backup(&node_dest)?; + link_or_copy(member_dir, &node_dest, mode)?; } + Adapter::Java => { + used_java_adapter = true; + for entry in walkdir::WalkDir::new(&dest) + .follow_links(true) + .into_iter() + .filter_map(|entry| entry.ok()) + { + if entry + .path() + .extension() + .is_some_and(|extension| extension == "jar") + { + jars.push(entry.path().to_string_lossy().to_string()); + } + } + } + Adapter::Go => wired_roots + .entry(Adapter::Go) + .or_default() + .push(dest.clone()), + Adapter::Python => wired_roots + .entry(Adapter::Python) + .or_default() + .push(dest.clone()), + Adapter::Rust => wired_roots + .entry(Adapter::Rust) + .or_default() + .push(dest.clone()), + Adapter::Dart => wired_roots + .entry(Adapter::Dart) + .or_default() + .push(dest.clone()), + Adapter::Auto | Adapter::None => {} } + wired_packages.push(WiredPackage { + key: key.clone(), + version: "workspace".to_string(), + language: member_manifest.package.language, + ecosystem: member_manifest.package.ecosystem(), + path: dest, + }); installed.push((key.clone(), "workspace".to_string())); } hoist_bins(&modules, &bins, mode)?; @@ -2661,6 +2798,105 @@ version = "1.0.0" assert!(error.contains("two paths"), "{error}"); } + #[test] + fn frozen_workspace_resolution_expands_transitive_members_and_validates_versions() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path(); + let app = root.join("apps/cli"); + let utils = root.join("packages/utils"); + let core = root.join("packages/core"); + for directory in [&app, &utils, &core] { + fs::create_dir_all(directory).unwrap(); + } + + fs::write( + root.join(MANIFEST_FILE), + r#"[package] +org = "zedtest" +name = "workspace-root" +version = "1.0.0" + +[package.repository] +vcs = "git" +url = "https://example.invalid/workspace-root" + +[workspace] +members = ["packages/*", "apps/*"] +"#, + ) + .unwrap(); + fs::write( + core.join(MANIFEST_FILE), + r#"[package] +org = "zedtest" +name = "ws-core" +version = "1.2.0" + +[package.repository] +vcs = "git" +url = "https://example.invalid/ws-core" +"#, + ) + .unwrap(); + fs::write( + utils.join(MANIFEST_FILE), + r#"[package] +org = "zedtest" +name = "ws-utils" +version = "1.1.0" + +[package.repository] +vcs = "git" +url = "https://example.invalid/ws-utils" + +[dependencies] +"zedtest/ws-core" = "^1" +"#, + ) + .unwrap(); + fs::write( + app.join(MANIFEST_FILE), + r#"[package] +org = "zedtest" +name = "ws-cli" +version = "1.0.0" + +[package.repository] +vcs = "git" +url = "https://example.invalid/ws-cli" + +[dependencies] +"zedtest/ws-utils" = "^1" +"#, + ) + .unwrap(); + + let manifest = read_manifest(&app).unwrap(); + let workspace = find_workspace(&app).unwrap(); + let links = collect_workspace_links_for_frozen(&app, &manifest, Some(&workspace)).unwrap(); + assert_eq!( + links.keys().cloned().collect::>(), + vec![ + "zedtest/ws-core".to_string(), + "zedtest/ws-utils".to_string() + ] + ); + assert_eq!(links["zedtest/ws-core"], core); + assert_eq!(links["zedtest/ws-utils"], utils); + + let incompatible = manifest + .to_toml_string() + .unwrap() + .replace("\"^1\"", "\"^2\""); + fs::write(app.join(MANIFEST_FILE), incompatible).unwrap(); + let manifest = read_manifest(&app).unwrap(); + let error = collect_workspace_links_for_frozen(&app, &manifest, Some(&workspace)) + .unwrap_err() + .to_string(); + assert!(error.contains("ws-utils@1.1.0"), "{error}"); + assert!(error.contains("does not satisfy `^2`"), "{error}"); + } + #[test] fn lock_only_frozen_restore_skips_only_the_missing_manifest_comparison() { let manifest = Manifest::parse( From 93e26a64f9b8060b9d104a33f401ebad4e2d5428 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 15:06:20 -0500 Subject: [PATCH 69/80] ci: remove completed workspace install patch bootstrap --- .github/workspace_install_wiring.py | 355 ---------------------------- 1 file changed, 355 deletions(-) delete mode 100644 .github/workspace_install_wiring.py diff --git a/.github/workspace_install_wiring.py b/.github/workspace_install_wiring.py deleted file mode 100644 index 31238bf8..00000000 --- a/.github/workspace_install_wiring.py +++ /dev/null @@ -1,355 +0,0 @@ -from pathlib import Path - - -path = Path("src/ops.rs") -text = path.read_text(encoding="utf-8") - - -def replace_once(old: str, new: str, label: str) -> None: - global text - if new in text: - return - count = text.count(old) - if count != 1: - raise RuntimeError(f"{label}: expected one match, found {count}") - text = text.replace(old, new, 1) - - -workspace_helper = r'''fn collect_workspace_links_for_frozen( - project: &Path, - manifest: &Manifest, - workspace: Option<&WorkspaceInfo>, -) -> Result> { - let Some(workspace) = workspace else { - return Ok(BTreeMap::new()); - }; - - let mut links = BTreeMap::new(); - let mut pending: VecDeque<(String, String)> = manifest - .dependencies - .iter() - .map(|(key, requirement)| (key.clone(), requirement.clone())) - .collect(); - - while let Some((raw_key, requirement_text)) = pending.pop_front() { - let (org, name) = split_key(&raw_key)?; - let key = format!("{org}/{name}"); - let Some(member_dir) = workspace.members.get(&key) else { - continue; - }; - let member_manifest = read_manifest(member_dir).with_context(|| { - format!( - "reading workspace member `{key}` from {}", - member_dir.display() - ) - })?; - let requirement = Requirement::parse(&requirement_text); - if !requirement.matches(&member_manifest.package.version) { - bail!( - "workspace member {key}@{} does not satisfy `{requirement_text}`", - member_manifest.package.version - ); - } - if member_dir == project || links.contains_key(&key) { - continue; - } - links.insert(key, member_dir.clone()); - pending.extend(member_manifest.dependencies); - } - - Ok(links) -} - -''' -replace_once( - "// ---------------------------------------------------------------------------\n// install\n", - workspace_helper + "// ---------------------------------------------------------------------------\n// install\n", - "insert frozen workspace resolver", -) - -replace_once( - r''' validate_frozen_manifest_requirements( - &manifest, - &lock, - workspace.as_ref(), - validate_manifest_requirements, - )?; - for locked in &lock.packages { -''', - r''' validate_frozen_manifest_requirements( - &manifest, - &lock, - workspace.as_ref(), - validate_manifest_requirements, - )?; - workspace_links = - collect_workspace_links_for_frozen(project, &manifest, workspace.as_ref())?; - for locked in &lock.packages { -''', - "restore workspace members during frozen installs", -) - -replace_once( - r''' if let Some(ws) = &workspace - && let Some(member_dir) = ws.members.get(&key) - { - if member_dir != project && !workspace_links.contains_key(&key) { - workspace_links.insert(key.clone(), member_dir.clone()); - if let Ok(member_manifest) = read_manifest(member_dir) { - for (sub_key, sub_req) in member_manifest.dependencies { - let (sub_org, sub_name) = split_key(&sub_key)?; - queue.push_back((sub_org, sub_name, sub_req)); - } - } - } - continue; - } -''', - r''' if let Some(ws) = &workspace - && let Some(member_dir) = ws.members.get(&key) - { - let member_manifest = read_manifest(member_dir).with_context(|| { - format!( - "reading workspace member `{key}` from {}", - member_dir.display() - ) - })?; - let requirement = Requirement::parse(&req_str); - if !requirement.matches(&member_manifest.package.version) { - bail!( - "workspace member {key}@{} does not satisfy `{req_str}`", - member_manifest.package.version - ); - } - if member_dir != project && !workspace_links.contains_key(&key) { - workspace_links.insert(key.clone(), member_dir.clone()); - for (sub_key, sub_req) in member_manifest.dependencies { - let (sub_org, sub_name) = split_key(&sub_key)?; - queue.push_back((sub_org, sub_name, sub_req)); - } - } - continue; - } -''', - "validate mutable workspace members", -) - -replace_once( - r''' for (key, member_dir) in &workspace_links { - interactive::confirm(cfg.interactive, &format!("link workspace package {key}"))?; - let (org, name) = split_key(key)?; - let dest = modules.join(&org).join(&name); - // Workspace dependencies obey the same ownership decision as registry - // packages. Copy mode must remain self-contained after the member source - // directory leaves the Docker/OCI build context. - link_or_copy(member_dir, &dest, mode)?; - if let Ok(member_manifest) = read_manifest(member_dir) { - for (bin_name, rel_target) in &member_manifest.bin { - bins.insert(bin_name.clone(), dest.join(rel_target)); - } - } - installed.push((key.clone(), "workspace".to_string())); - } -''', - r''' for (key, member_dir) in &workspace_links { - interactive::confirm(cfg.interactive, &format!("link workspace package {key}"))?; - let (org, name) = split_key(key)?; - let member_manifest = read_manifest(member_dir).with_context(|| { - format!( - "reading workspace member `{key}` from {}", - member_dir.display() - ) - })?; - if !allow_ecosystem_mismatch - && let Some(problem) = ecosystem_mismatch( - key, - &name, - member_manifest.package.language, - member_manifest.package.ecosystem(), - &project_ecos, - ) - { - bail!("{problem}"); - } - - let dest = modules.join(&org).join(&name); - // Workspace dependencies obey the same ownership decision as registry - // packages. Copy mode must remain self-contained after the member source - // directory leaves the Docker/OCI build context. - link_or_copy(member_dir, &dest, mode)?; - for (bin_name, rel_target) in &member_manifest.bin { - bins.insert(bin_name.clone(), dest.join(rel_target)); - } - - let dependency_adapter = if use_dependency_adapters { - member_manifest - .install - .adapter - .as_deref() - .map(named_adapter) - .transpose()? - .unwrap_or(adapter) - } else { - adapter - }; - match dependency_adapter { - Adapter::Node => { - used_node_adapter = true; - let node_dest = project - .join("node_modules") - .join(format!("@{org}")) - .join(&name); - transaction.backup(&node_dest)?; - link_or_copy(member_dir, &node_dest, mode)?; - } - Adapter::Java => { - used_java_adapter = true; - for entry in walkdir::WalkDir::new(&dest) - .follow_links(true) - .into_iter() - .filter_map(|entry| entry.ok()) - { - if entry.path().extension().is_some_and(|extension| extension == "jar") { - jars.push(entry.path().to_string_lossy().to_string()); - } - } - } - Adapter::Go => wired_roots - .entry(Adapter::Go) - .or_default() - .push(dest.clone()), - Adapter::Python => wired_roots - .entry(Adapter::Python) - .or_default() - .push(dest.clone()), - Adapter::Rust => wired_roots - .entry(Adapter::Rust) - .or_default() - .push(dest.clone()), - Adapter::Dart => wired_roots - .entry(Adapter::Dart) - .or_default() - .push(dest.clone()), - Adapter::Auto | Adapter::None => {} - } - wired_packages.push(WiredPackage { - key: key.clone(), - version: "workspace".to_string(), - language: member_manifest.package.language, - ecosystem: member_manifest.package.ecosystem(), - path: dest, - }); - installed.push((key.clone(), "workspace".to_string())); - } -''', - "wire workspace packages through native adapters", -) - -replace_once( - r''' #[test] - fn lock_only_frozen_restore_skips_only_the_missing_manifest_comparison() { -''', - r''' #[test] - fn frozen_workspace_resolution_expands_transitive_members_and_validates_versions() { - let temp = tempfile::tempdir().unwrap(); - let root = temp.path(); - let app = root.join("apps/cli"); - let utils = root.join("packages/utils"); - let core = root.join("packages/core"); - for directory in [&app, &utils, &core] { - fs::create_dir_all(directory).unwrap(); - } - - fs::write( - root.join(MANIFEST_FILE), - r#"[package] -org = "zedtest" -name = "workspace-root" -version = "1.0.0" - -[package.repository] -vcs = "git" -url = "https://example.invalid/workspace-root" - -[workspace] -members = ["packages/*", "apps/*"] -"#, - ) - .unwrap(); - fs::write( - core.join(MANIFEST_FILE), - r#"[package] -org = "zedtest" -name = "ws-core" -version = "1.2.0" - -[package.repository] -vcs = "git" -url = "https://example.invalid/ws-core" -"#, - ) - .unwrap(); - fs::write( - utils.join(MANIFEST_FILE), - r#"[package] -org = "zedtest" -name = "ws-utils" -version = "1.1.0" - -[package.repository] -vcs = "git" -url = "https://example.invalid/ws-utils" - -[dependencies] -"zedtest/ws-core" = "^1" -"#, - ) - .unwrap(); - fs::write( - app.join(MANIFEST_FILE), - r#"[package] -org = "zedtest" -name = "ws-cli" -version = "1.0.0" - -[package.repository] -vcs = "git" -url = "https://example.invalid/ws-cli" - -[dependencies] -"zedtest/ws-utils" = "^1" -"#, - ) - .unwrap(); - - let manifest = read_manifest(&app).unwrap(); - let workspace = find_workspace(&app).unwrap(); - let links = - collect_workspace_links_for_frozen(&app, &manifest, Some(&workspace)).unwrap(); - assert_eq!( - links.keys().cloned().collect::>(), - vec!["zedtest/ws-core".to_string(), "zedtest/ws-utils".to_string()] - ); - assert_eq!(links["zedtest/ws-core"], core); - assert_eq!(links["zedtest/ws-utils"], utils); - - let incompatible = manifest - .to_toml_string() - .unwrap() - .replace("\"^1\"", "\"^2\""); - fs::write(app.join(MANIFEST_FILE), incompatible).unwrap(); - let manifest = read_manifest(&app).unwrap(); - let error = collect_workspace_links_for_frozen(&app, &manifest, Some(&workspace)) - .unwrap_err() - .to_string(); - assert!(error.contains("ws-utils@1.1.0"), "{error}"); - assert!(error.contains("does not satisfy `^2`"), "{error}"); - } - - #[test] - fn lock_only_frozen_restore_skips_only_the_missing_manifest_comparison() { -''', - "add frozen workspace graph tests", -) - -path.write_text(text, encoding="utf-8") From 56e597b35deccc82bff7deee807f459a1def537d Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 15:06:26 -0500 Subject: [PATCH 70/80] ci: remove completed workspace install publishing workflow --- .../apply-workspace-install-wiring.yml | 102 ------------------ 1 file changed, 102 deletions(-) delete mode 100644 .github/workflows/apply-workspace-install-wiring.yml diff --git a/.github/workflows/apply-workspace-install-wiring.yml b/.github/workflows/apply-workspace-install-wiring.yml deleted file mode 100644 index 4a42066b..00000000 --- a/.github/workflows/apply-workspace-install-wiring.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: apply workspace install wiring - -on: - push: - branches: - - agent/workspace-install-wiring - paths: - - .github/workspace_install_wiring.py - - .github/workflows/apply-workspace-install-wiring.yml - pull_request: - branches: - - agent/recursive-resolver-input-hardening - paths: - - .github/workspace_install_wiring.py - - .github/workflows/apply-workspace-install-wiring.yml - - src/ops.rs - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: apply-workspace-install-wiring-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: "1" - -jobs: - apply-and-validate: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 - timeout-minutes: 45 - permissions: - contents: write - steps: - - name: Check out workspace-wiring branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: agent/workspace-install-wiring - path: zed-cli - fetch-depth: 0 - 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 and lint support - run: | - set -euo pipefail - rustup toolchain install stable --profile minimal --component rustfmt,clippy - rustup default stable - rustc --version --verbose - cargo --version --verbose - - - name: Apply exact workspace install patch - working-directory: zed-cli - run: | - set -euo pipefail - python3 .github/workspace_install_wiring.py - cargo fmt --all - git diff --check - - - name: Validate workspace and install contracts - run: | - set -euo pipefail - cargo fmt --all --check --manifest-path zed-cli/Cargo.toml - cargo test --locked --manifest-path zed-cli/Cargo.toml --lib \ - ops::tests::frozen_workspace_resolution_expands_transitive_members_and_validates_versions \ - -- --exact --nocapture - cargo test --locked --manifest-path zed-cli/Cargo.toml --lib ops::tests - cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::tests - cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::hardening_tests - cargo clippy --locked --manifest-path zed-cli/Cargo.toml --all-targets -- -D warnings - - - name: Publish validated workspace install implementation - working-directory: zed-cli - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - git add src/ops.rs - git diff --cached --check - if git diff --cached --quiet; then - echo 'workspace install implementation is already published' - exit 0 - fi - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m 'fix: wire and restore workspace packages' - git push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ - HEAD:agent/workspace-install-wiring From 3bbf82d79d25f0fcffc2b82a7ac45169605d8969 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 15:26:54 -0500 Subject: [PATCH 71/80] test: stage workspace-aware uninstall and wiring cleanup --- .github/workspace_uninstall_fix.py | 231 +++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 .github/workspace_uninstall_fix.py diff --git a/.github/workspace_uninstall_fix.py b/.github/workspace_uninstall_fix.py new file mode 100644 index 00000000..9d77e47b --- /dev/null +++ b/.github/workspace_uninstall_fix.py @@ -0,0 +1,231 @@ +from pathlib import Path + + +path = Path("src/ops.rs") +text = path.read_text(encoding="utf-8") +start = text.index("pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> {") +end = text.index("\n// ---------------------------------------------------------------------------\n// build hooks", start) + +replacement = r'''pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { + let lock_path = project.join(LOCKFILE_FILE); + let text = fs::read_to_string(&lock_path) + .with_context(|| format!("zed uninstall requires {LOCKFILE_FILE}"))?; + let lock = Lockfile::parse(&text).with_context(|| format!("invalid {LOCKFILE_FILE}"))?; + + // Workspace packages deliberately do not appear in the artifact lock: the + // lock records immutable registry hashes, while workspace members are live + // source projections. Reconstruct the active workspace graph from the same + // manifest boundary used by frozen install so a workspace-only project can + // still uninstall and later restore its exact materialized graph. + let manifest = read_manifest(project).ok(); + let workspace = find_workspace(project); + let workspace_links = match manifest.as_ref() { + Some(manifest) => { + collect_workspace_links_for_frozen(project, manifest, workspace.as_ref())? + } + None => BTreeMap::new(), + }; + let total_materialized = lock.packages.len() + workspace_links.len(); + if total_materialized == 0 { + println!("nothing to uninstall"); + return Ok(()); + } + + let mut targets = BTreeSet::new(); + if specs.is_empty() { + targets.extend(lock.packages.iter().map(LockedPackage::full_name)); + targets.extend(workspace_links.keys().cloned()); + } else { + for spec in specs { + if spec.contains('@') { + bail!("uninstall accepts package identities without versions (expected org/name)"); + } + let (org, name) = split_key(spec)?; + let key = format!("{org}/{name}"); + if workspace_links.contains_key(&key) { + bail!( + "selective uninstall of workspace package `{key}` is not supported; run `zed uninstall` without package arguments to remove the complete materialized graph while retaining {LOCKFILE_FILE}" + ); + } + if lock.find(&org, &name).is_none() { + bail!("{key} is neither pinned by {LOCKFILE_FILE} nor an active workspace package"); + } + targets.insert(key); + } + } + + interactive::confirm( + cfg.interactive, + &format!( + "uninstall {} package(s) from {} while retaining {LOCKFILE_FILE}", + targets.len(), + project.display() + ), + )?; + + let store = Store::new(&cfg.home); + let _install_lock = store.install_lock()?; + let modules_dir = manifest + .as_ref() + .map(|manifest| manifest.modules_dir().to_string()) + .unwrap_or_else(|| MODULES_DIR.to_string()); + let modules = project.join(&modules_dir); + let had_node_adapter = project.join(".zed").join("node_path").is_file(); + let had_java_adapter = project.join(".zed").join("classpath").is_file(); + let uninstall_all = targets.len() == total_materialized; + + let mut transaction = ProjectTransaction::begin(project)?; + eprintln!( + "transaction {}: staging uninstall rollback data", + transaction.id() + ); + if uninstall_all { + transaction.backup(&modules)?; + // These files are all generated projections of the materialized graph. + // Remove them transactionally on a full uninstall so no toolchain sees + // stale package paths while the retained lock waits for frozen restore. + for generated in [ + "paths.json", + "node_path", + "classpath", + "go.work", + "pythonpath", + "cargo-paths.toml", + "pub-deps.yaml", + ] { + transaction.backup(&project.join(".zed").join(generated))?; + } + } else { + transaction.backup(&modules.join(BIN_DIR))?; + if had_java_adapter { + transaction.backup(&project.join(".zed").join("classpath"))?; + } + } + + for key in &targets { + interactive::confirm(cfg.interactive, &format!("unmaterialize {key}"))?; + let (org, name) = split_key(key)?; + if !uninstall_all { + transaction.backup(&modules.join(&org).join(&name))?; + } + if had_node_adapter { + transaction.backup( + &project + .join("node_modules") + .join(format!("@{org}")) + .join(&name), + )?; + } + } + + let remaining: Vec<&LockedPackage> = lock + .packages + .iter() + .filter(|package| !targets.contains(&package.full_name())) + .collect(); + let remaining_workspace = workspace_links + .keys() + .filter(|key| !targets.contains(*key)) + .count(); + if !uninstall_all { + let mut bins: BTreeMap = BTreeMap::new(); + let mut mode = InstallMode::Copy; + let mut jars = Vec::new(); + for package in &remaining { + let installed = modules.join(&package.org).join(&package.name); + if fs::symlink_metadata(&installed) + .is_ok_and(|metadata| metadata.file_type().is_symlink()) + { + mode = InstallMode::Symlink; + } + if let Ok(package_manifest) = read_manifest(&installed) { + for (name, target) in package_manifest.bin { + bins.insert(name, installed.join(target)); + } + } + if had_java_adapter && installed.exists() { + for entry in walkdir::WalkDir::new(&installed) + .follow_links(true) + .into_iter() + .filter_map(|entry| entry.ok()) + { + if entry + .path() + .extension() + .is_some_and(|extension| extension == "jar") + { + jars.push(entry.path().to_string_lossy().to_string()); + } + } + } + } + // Selective uninstall currently targets registry packages only. Keep + // workspace bins and Java entries in the rebuilt aggregate projections. + for key in workspace_links.keys().filter(|key| !targets.contains(*key)) { + let (org, name) = split_key(key)?; + let installed = modules.join(&org).join(&name); + if fs::symlink_metadata(&installed) + .is_ok_and(|metadata| metadata.file_type().is_symlink()) + { + mode = InstallMode::Symlink; + } + if let Ok(package_manifest) = read_manifest(&installed) { + for (bin_name, target) in package_manifest.bin { + bins.insert(bin_name, installed.join(target)); + } + } + if had_java_adapter && installed.exists() { + for entry in walkdir::WalkDir::new(&installed) + .follow_links(true) + .into_iter() + .filter_map(|entry| entry.ok()) + { + if entry + .path() + .extension() + .is_some_and(|extension| extension == "jar") + { + jars.push(entry.path().to_string_lossy().to_string()); + } + } + } + } + hoist_bins(&modules, &bins, mode)?; + if had_java_adapter && !jars.is_empty() { + jars.sort(); + jars.dedup(); + let classpath = project.join(".zed").join("classpath"); + fs::create_dir_all(classpath.parent().context("classpath parent")?)?; + fs::write(classpath, jars.join(":") + "\n")?; + } + } + + let remaining_total = remaining.len() + remaining_workspace; + interactive::confirm( + cfg.interactive, + &format!( + "record {remaining_total} remaining installed package(s) and commit transaction {}", + transaction.id() + ), + )?; + store.record_project( + project, + remaining + .iter() + .map(|package| package.sha256.clone()) + .collect(), + )?; + transaction.commit()?; + + for key in &targets { + println!("uninstalled {key}"); + } + println!( + "{remaining_total} package(s) remain materialized; {LOCKFILE_FILE} retained for frozen reinstall" + ); + Ok(()) +} +''' + +text = text[:start] + replacement + text[end:] +path.write_text(text, encoding="utf-8") From c96abd018413f4a338346fccf506545fab6b1f44 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 15:27:11 -0500 Subject: [PATCH 72/80] ci: validate workspace uninstall and adapter cleanup --- .../workflows/apply-workspace-uninstall.yml | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .github/workflows/apply-workspace-uninstall.yml diff --git a/.github/workflows/apply-workspace-uninstall.yml b/.github/workflows/apply-workspace-uninstall.yml new file mode 100644 index 00000000..d5caf088 --- /dev/null +++ b/.github/workflows/apply-workspace-uninstall.yml @@ -0,0 +1,99 @@ +name: apply workspace uninstall lifecycle + +on: + pull_request: + branches: + - agent/recursive-resolver-input-hardening + paths: + - .github/workspace_uninstall_fix.py + - .github/workflows/apply-workspace-uninstall.yml + - src/ops.rs + push: + branches: + - agent/workspace-install-wiring + paths: + - .github/workspace_uninstall_fix.py + - .github/workflows/apply-workspace-uninstall.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: apply-workspace-uninstall-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: "1" + +jobs: + apply-and-validate: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + permissions: + contents: write + steps: + - name: Check out workspace install branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: agent/workspace-install-wiring + path: zed-cli + fetch-depth: 0 + 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 and lint support + run: | + set -euo pipefail + rustup toolchain install stable --profile minimal --component rustfmt,clippy + rustup default stable + rustc --version --verbose + cargo --version --verbose + + - name: Apply exact workspace uninstall patch + working-directory: zed-cli + run: | + set -euo pipefail + python3 .github/workspace_uninstall_fix.py + cargo fmt --all + git diff --check + + - name: Validate uninstall and install contracts + run: | + set -euo pipefail + cargo fmt --all --check --manifest-path zed-cli/Cargo.toml + cargo test --locked --manifest-path zed-cli/Cargo.toml --lib ops::tests + cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::tests + cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::hardening_tests + cargo clippy --locked --manifest-path zed-cli/Cargo.toml --all-targets -- -D warnings + + - name: Publish validated workspace uninstall implementation + working-directory: zed-cli + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git add src/ops.rs + git diff --cached --check + if git diff --cached --quiet; then + echo 'workspace uninstall implementation is already published' + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git commit -m 'fix: uninstall workspace projections transactionally' + git push \ + "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ + HEAD:agent/workspace-install-wiring From 29df5905a151d797872b9a3731b5a8a48de65e67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:28:51 +0000 Subject: [PATCH 73/80] fix: uninstall workspace projections transactionally --- src/ops.rs | 94 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 14 deletions(-) diff --git a/src/ops.rs b/src/ops.rs index bab67e96..acb8b105 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -1555,7 +1555,22 @@ pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { let text = fs::read_to_string(&lock_path) .with_context(|| format!("zed uninstall requires {LOCKFILE_FILE}"))?; let lock = Lockfile::parse(&text).with_context(|| format!("invalid {LOCKFILE_FILE}"))?; - if lock.packages.is_empty() { + + // Workspace packages deliberately do not appear in the artifact lock: the + // lock records immutable registry hashes, while workspace members are live + // source projections. Reconstruct the active workspace graph from the same + // manifest boundary used by frozen install so a workspace-only project can + // still uninstall and later restore its exact materialized graph. + let manifest = read_manifest(project).ok(); + let workspace = find_workspace(project); + let workspace_links = match manifest.as_ref() { + Some(manifest) => { + collect_workspace_links_for_frozen(project, manifest, workspace.as_ref())? + } + None => BTreeMap::new(), + }; + let total_materialized = lock.packages.len() + workspace_links.len(); + if total_materialized == 0 { println!("nothing to uninstall"); return Ok(()); } @@ -1563,6 +1578,7 @@ pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { let mut targets = BTreeSet::new(); if specs.is_empty() { targets.extend(lock.packages.iter().map(LockedPackage::full_name)); + targets.extend(workspace_links.keys().cloned()); } else { for spec in specs { if spec.contains('@') { @@ -1570,8 +1586,13 @@ pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { } let (org, name) = split_key(spec)?; let key = format!("{org}/{name}"); + if workspace_links.contains_key(&key) { + bail!( + "selective uninstall of workspace package `{key}` is not supported; run `zed uninstall` without package arguments to remove the complete materialized graph while retaining {LOCKFILE_FILE}" + ); + } if lock.find(&org, &name).is_none() { - bail!("{key} is not pinned by {LOCKFILE_FILE}"); + bail!("{key} is neither pinned by {LOCKFILE_FILE} nor an active workspace package"); } targets.insert(key); } @@ -1588,7 +1609,6 @@ pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { let store = Store::new(&cfg.home); let _install_lock = store.install_lock()?; - let manifest = read_manifest(project).ok(); let modules_dir = manifest .as_ref() .map(|manifest| manifest.modules_dir().to_string()) @@ -1596,7 +1616,7 @@ pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { let modules = project.join(&modules_dir); let had_node_adapter = project.join(".zed").join("node_path").is_file(); let had_java_adapter = project.join(".zed").join("classpath").is_file(); - let uninstall_all = targets.len() == lock.packages.len(); + let uninstall_all = targets.len() == total_materialized; let mut transaction = ProjectTransaction::begin(project)?; eprintln!( @@ -1605,14 +1625,25 @@ pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { ); if uninstall_all { transaction.backup(&modules)?; + // These files are all generated projections of the materialized graph. + // Remove them transactionally on a full uninstall so no toolchain sees + // stale package paths while the retained lock waits for frozen restore. + for generated in [ + "paths.json", + "node_path", + "classpath", + "go.work", + "pythonpath", + "cargo-paths.toml", + "pub-deps.yaml", + ] { + transaction.backup(&project.join(".zed").join(generated))?; + } } else { transaction.backup(&modules.join(BIN_DIR))?; - } - if had_java_adapter { - transaction.backup(&project.join(".zed").join("classpath"))?; - } - if uninstall_all && had_node_adapter { - transaction.backup(&project.join(".zed").join("node_path"))?; + if had_java_adapter { + transaction.backup(&project.join(".zed").join("classpath"))?; + } } for key in &targets { @@ -1636,6 +1667,10 @@ pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { .iter() .filter(|package| !targets.contains(&package.full_name())) .collect(); + let remaining_workspace = workspace_links + .keys() + .filter(|key| !targets.contains(*key)) + .count(); if !uninstall_all { let mut bins: BTreeMap = BTreeMap::new(); let mut mode = InstallMode::Copy; @@ -1668,20 +1703,52 @@ pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { } } } + // Selective uninstall currently targets registry packages only. Keep + // workspace bins and Java entries in the rebuilt aggregate projections. + for key in workspace_links.keys().filter(|key| !targets.contains(*key)) { + let (org, name) = split_key(key)?; + let installed = modules.join(&org).join(&name); + if fs::symlink_metadata(&installed) + .is_ok_and(|metadata| metadata.file_type().is_symlink()) + { + mode = InstallMode::Symlink; + } + if let Ok(package_manifest) = read_manifest(&installed) { + for (bin_name, target) in package_manifest.bin { + bins.insert(bin_name, installed.join(target)); + } + } + if had_java_adapter && installed.exists() { + for entry in walkdir::WalkDir::new(&installed) + .follow_links(true) + .into_iter() + .filter_map(|entry| entry.ok()) + { + if entry + .path() + .extension() + .is_some_and(|extension| extension == "jar") + { + jars.push(entry.path().to_string_lossy().to_string()); + } + } + } + } hoist_bins(&modules, &bins, mode)?; if had_java_adapter && !jars.is_empty() { jars.sort(); + jars.dedup(); let classpath = project.join(".zed").join("classpath"); fs::create_dir_all(classpath.parent().context("classpath parent")?)?; fs::write(classpath, jars.join(":") + "\n")?; } } + let remaining_total = remaining.len() + remaining_workspace; interactive::confirm( cfg.interactive, &format!( - "record {} remaining installed package(s) and commit transaction {}", - remaining.len(), + "record {remaining_total} remaining installed package(s) and commit transaction {}", transaction.id() ), )?; @@ -1698,8 +1765,7 @@ pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { println!("uninstalled {key}"); } println!( - "{} package(s) remain materialized; {LOCKFILE_FILE} retained for frozen reinstall", - remaining.len() + "{remaining_total} package(s) remain materialized; {LOCKFILE_FILE} retained for frozen reinstall" ); Ok(()) } From 678dbb521c489027f86d1747c8094b09c76d7ce6 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 15:31:18 -0500 Subject: [PATCH 74/80] ci: remove completed workspace uninstall patch helper --- .github/workspace_uninstall_fix.py | 231 ----------------------------- 1 file changed, 231 deletions(-) delete mode 100644 .github/workspace_uninstall_fix.py diff --git a/.github/workspace_uninstall_fix.py b/.github/workspace_uninstall_fix.py deleted file mode 100644 index 9d77e47b..00000000 --- a/.github/workspace_uninstall_fix.py +++ /dev/null @@ -1,231 +0,0 @@ -from pathlib import Path - - -path = Path("src/ops.rs") -text = path.read_text(encoding="utf-8") -start = text.index("pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> {") -end = text.index("\n// ---------------------------------------------------------------------------\n// build hooks", start) - -replacement = r'''pub fn uninstall(project: &Path, cfg: &Config, specs: &[String]) -> Result<()> { - let lock_path = project.join(LOCKFILE_FILE); - let text = fs::read_to_string(&lock_path) - .with_context(|| format!("zed uninstall requires {LOCKFILE_FILE}"))?; - let lock = Lockfile::parse(&text).with_context(|| format!("invalid {LOCKFILE_FILE}"))?; - - // Workspace packages deliberately do not appear in the artifact lock: the - // lock records immutable registry hashes, while workspace members are live - // source projections. Reconstruct the active workspace graph from the same - // manifest boundary used by frozen install so a workspace-only project can - // still uninstall and later restore its exact materialized graph. - let manifest = read_manifest(project).ok(); - let workspace = find_workspace(project); - let workspace_links = match manifest.as_ref() { - Some(manifest) => { - collect_workspace_links_for_frozen(project, manifest, workspace.as_ref())? - } - None => BTreeMap::new(), - }; - let total_materialized = lock.packages.len() + workspace_links.len(); - if total_materialized == 0 { - println!("nothing to uninstall"); - return Ok(()); - } - - let mut targets = BTreeSet::new(); - if specs.is_empty() { - targets.extend(lock.packages.iter().map(LockedPackage::full_name)); - targets.extend(workspace_links.keys().cloned()); - } else { - for spec in specs { - if spec.contains('@') { - bail!("uninstall accepts package identities without versions (expected org/name)"); - } - let (org, name) = split_key(spec)?; - let key = format!("{org}/{name}"); - if workspace_links.contains_key(&key) { - bail!( - "selective uninstall of workspace package `{key}` is not supported; run `zed uninstall` without package arguments to remove the complete materialized graph while retaining {LOCKFILE_FILE}" - ); - } - if lock.find(&org, &name).is_none() { - bail!("{key} is neither pinned by {LOCKFILE_FILE} nor an active workspace package"); - } - targets.insert(key); - } - } - - interactive::confirm( - cfg.interactive, - &format!( - "uninstall {} package(s) from {} while retaining {LOCKFILE_FILE}", - targets.len(), - project.display() - ), - )?; - - let store = Store::new(&cfg.home); - let _install_lock = store.install_lock()?; - let modules_dir = manifest - .as_ref() - .map(|manifest| manifest.modules_dir().to_string()) - .unwrap_or_else(|| MODULES_DIR.to_string()); - let modules = project.join(&modules_dir); - let had_node_adapter = project.join(".zed").join("node_path").is_file(); - let had_java_adapter = project.join(".zed").join("classpath").is_file(); - let uninstall_all = targets.len() == total_materialized; - - let mut transaction = ProjectTransaction::begin(project)?; - eprintln!( - "transaction {}: staging uninstall rollback data", - transaction.id() - ); - if uninstall_all { - transaction.backup(&modules)?; - // These files are all generated projections of the materialized graph. - // Remove them transactionally on a full uninstall so no toolchain sees - // stale package paths while the retained lock waits for frozen restore. - for generated in [ - "paths.json", - "node_path", - "classpath", - "go.work", - "pythonpath", - "cargo-paths.toml", - "pub-deps.yaml", - ] { - transaction.backup(&project.join(".zed").join(generated))?; - } - } else { - transaction.backup(&modules.join(BIN_DIR))?; - if had_java_adapter { - transaction.backup(&project.join(".zed").join("classpath"))?; - } - } - - for key in &targets { - interactive::confirm(cfg.interactive, &format!("unmaterialize {key}"))?; - let (org, name) = split_key(key)?; - if !uninstall_all { - transaction.backup(&modules.join(&org).join(&name))?; - } - if had_node_adapter { - transaction.backup( - &project - .join("node_modules") - .join(format!("@{org}")) - .join(&name), - )?; - } - } - - let remaining: Vec<&LockedPackage> = lock - .packages - .iter() - .filter(|package| !targets.contains(&package.full_name())) - .collect(); - let remaining_workspace = workspace_links - .keys() - .filter(|key| !targets.contains(*key)) - .count(); - if !uninstall_all { - let mut bins: BTreeMap = BTreeMap::new(); - let mut mode = InstallMode::Copy; - let mut jars = Vec::new(); - for package in &remaining { - let installed = modules.join(&package.org).join(&package.name); - if fs::symlink_metadata(&installed) - .is_ok_and(|metadata| metadata.file_type().is_symlink()) - { - mode = InstallMode::Symlink; - } - if let Ok(package_manifest) = read_manifest(&installed) { - for (name, target) in package_manifest.bin { - bins.insert(name, installed.join(target)); - } - } - if had_java_adapter && installed.exists() { - for entry in walkdir::WalkDir::new(&installed) - .follow_links(true) - .into_iter() - .filter_map(|entry| entry.ok()) - { - if entry - .path() - .extension() - .is_some_and(|extension| extension == "jar") - { - jars.push(entry.path().to_string_lossy().to_string()); - } - } - } - } - // Selective uninstall currently targets registry packages only. Keep - // workspace bins and Java entries in the rebuilt aggregate projections. - for key in workspace_links.keys().filter(|key| !targets.contains(*key)) { - let (org, name) = split_key(key)?; - let installed = modules.join(&org).join(&name); - if fs::symlink_metadata(&installed) - .is_ok_and(|metadata| metadata.file_type().is_symlink()) - { - mode = InstallMode::Symlink; - } - if let Ok(package_manifest) = read_manifest(&installed) { - for (bin_name, target) in package_manifest.bin { - bins.insert(bin_name, installed.join(target)); - } - } - if had_java_adapter && installed.exists() { - for entry in walkdir::WalkDir::new(&installed) - .follow_links(true) - .into_iter() - .filter_map(|entry| entry.ok()) - { - if entry - .path() - .extension() - .is_some_and(|extension| extension == "jar") - { - jars.push(entry.path().to_string_lossy().to_string()); - } - } - } - } - hoist_bins(&modules, &bins, mode)?; - if had_java_adapter && !jars.is_empty() { - jars.sort(); - jars.dedup(); - let classpath = project.join(".zed").join("classpath"); - fs::create_dir_all(classpath.parent().context("classpath parent")?)?; - fs::write(classpath, jars.join(":") + "\n")?; - } - } - - let remaining_total = remaining.len() + remaining_workspace; - interactive::confirm( - cfg.interactive, - &format!( - "record {remaining_total} remaining installed package(s) and commit transaction {}", - transaction.id() - ), - )?; - store.record_project( - project, - remaining - .iter() - .map(|package| package.sha256.clone()) - .collect(), - )?; - transaction.commit()?; - - for key in &targets { - println!("uninstalled {key}"); - } - println!( - "{remaining_total} package(s) remain materialized; {LOCKFILE_FILE} retained for frozen reinstall" - ); - Ok(()) -} -''' - -text = text[:start] + replacement + text[end:] -path.write_text(text, encoding="utf-8") From 8cc6745c75fd46dabf2e383cbcfc14db5ff933da Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 15:31:24 -0500 Subject: [PATCH 75/80] ci: remove completed workspace uninstall publishing workflow --- .../workflows/apply-workspace-uninstall.yml | 99 ------------------- 1 file changed, 99 deletions(-) delete mode 100644 .github/workflows/apply-workspace-uninstall.yml diff --git a/.github/workflows/apply-workspace-uninstall.yml b/.github/workflows/apply-workspace-uninstall.yml deleted file mode 100644 index d5caf088..00000000 --- a/.github/workflows/apply-workspace-uninstall.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: apply workspace uninstall lifecycle - -on: - pull_request: - branches: - - agent/recursive-resolver-input-hardening - paths: - - .github/workspace_uninstall_fix.py - - .github/workflows/apply-workspace-uninstall.yml - - src/ops.rs - push: - branches: - - agent/workspace-install-wiring - paths: - - .github/workspace_uninstall_fix.py - - .github/workflows/apply-workspace-uninstall.yml - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: apply-workspace-uninstall-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: "1" - -jobs: - apply-and-validate: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 - timeout-minutes: 45 - permissions: - contents: write - steps: - - name: Check out workspace install branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: agent/workspace-install-wiring - path: zed-cli - fetch-depth: 0 - 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 and lint support - run: | - set -euo pipefail - rustup toolchain install stable --profile minimal --component rustfmt,clippy - rustup default stable - rustc --version --verbose - cargo --version --verbose - - - name: Apply exact workspace uninstall patch - working-directory: zed-cli - run: | - set -euo pipefail - python3 .github/workspace_uninstall_fix.py - cargo fmt --all - git diff --check - - - name: Validate uninstall and install contracts - run: | - set -euo pipefail - cargo fmt --all --check --manifest-path zed-cli/Cargo.toml - cargo test --locked --manifest-path zed-cli/Cargo.toml --lib ops::tests - cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::tests - cargo test --locked --manifest-path zed-cli/Cargo.toml --lib install_graph::hardening_tests - cargo clippy --locked --manifest-path zed-cli/Cargo.toml --all-targets -- -D warnings - - - name: Publish validated workspace uninstall implementation - working-directory: zed-cli - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - git add src/ops.rs - git diff --cached --check - if git diff --cached --quiet; then - echo 'workspace uninstall implementation is already published' - exit 0 - fi - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m 'fix: uninstall workspace projections transactionally' - git push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ - HEAD:agent/workspace-install-wiring From 85895878c5ffe55216adbe9c73b895c502683fc8 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 16:47:48 -0500 Subject: [PATCH 76/80] ci(DEN-1505): map current-main recursive-stack conflicts --- ...den-1505-current-main-merge-diagnostic.yml | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .github/workflows/den-1505-current-main-merge-diagnostic.yml diff --git a/.github/workflows/den-1505-current-main-merge-diagnostic.yml b/.github/workflows/den-1505-current-main-merge-diagnostic.yml new file mode 100644 index 00000000..9bd6cd83 --- /dev/null +++ b/.github/workflows/den-1505-current-main-merge-diagnostic.yml @@ -0,0 +1,107 @@ +name: DEN-1505 current-main merge diagnostic + +on: + push: + branches: + - agent/den-1505-current-main-integration + paths: + - '.github/workflows/den-1505-current-main-merge-diagnostic.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CURRENT_MAIN_SHA: b82da352472e301425c175b78d9d2c1c0cd0f1db + +jobs: + conflict-map: + name: Map the exact semantic conflict blocks + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out the cumulative installer stack + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Attempt the exact current-main merge without committing + id: merge + shell: bash + run: | + set -euo pipefail + git fetch --no-tags origin "$CURRENT_MAIN_SHA" + set +e + git -c merge.conflictStyle=diff3 merge --no-commit --no-ff "$CURRENT_MAIN_SHA" + status=$? + set -e + echo "status=$status" >> "$GITHUB_OUTPUT" + git status --short + git diff --name-only --diff-filter=U | sort > "$RUNNER_TEMP/conflicts.txt" + cat "$RUNNER_TEMP/conflicts.txt" + + - name: Require the bounded two-file conflict surface + shell: bash + run: | + set -euo pipefail + printf '%s\n' src/lib.rs src/ops.rs > "$RUNNER_TEMP/expected.txt" + cmp "$RUNNER_TEMP/expected.txt" "$RUNNER_TEMP/conflicts.txt" + + - name: Render every conflict block with bounded context + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + + for raw_path in Path('/tmp').glob('this-path-never-matches'): + raise AssertionError(raw_path) + + for name in ('src/lib.rs', 'src/ops.rs'): + lines = Path(name).read_text(encoding='utf-8').splitlines() + print(f'::group::{name} conflict blocks') + index = 0 + block = 0 + while index < len(lines): + if not lines[index].startswith('<<<<<<< '): + index += 1 + continue + start = index + depth = 0 + while index < len(lines): + line = lines[index] + if line.startswith('<<<<<<< '): + depth += 1 + elif line.startswith('>>>>>>> '): + depth -= 1 + if depth == 0: + break + index += 1 + if index >= len(lines): + raise SystemExit(f'unterminated conflict in {name}') + block += 1 + lo = max(0, start - 12) + hi = min(len(lines), index + 13) + print(f'--- block {block}: lines {start + 1}-{index + 1} ---') + for line_number in range(lo, hi): + print(f'{line_number + 1:05d}: {lines[line_number]}') + index += 1 + print(f'conflict_blocks={block}') + print('::endgroup::') + PY + + - name: Upload the bounded conflicted files + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: den-1505-current-main-conflicts + path: | + src/lib.rs + src/ops.rs + ${{ runner.temp }}/conflicts.txt + if-no-files-found: error + retention-days: 1 From 1bef5b806b93e38e9308890c5eaa97f4626e2215 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 16:49:05 -0500 Subject: [PATCH 77/80] ci(DEN-1505): expose merge diagnostics on the PR head --- .../workflows/den-1505-current-main-merge-diagnostic.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/den-1505-current-main-merge-diagnostic.yml b/.github/workflows/den-1505-current-main-merge-diagnostic.yml index 9bd6cd83..f1c781a0 100644 --- a/.github/workflows/den-1505-current-main-merge-diagnostic.yml +++ b/.github/workflows/den-1505-current-main-merge-diagnostic.yml @@ -1,6 +1,9 @@ name: DEN-1505 current-main merge diagnostic on: + pull_request: + paths: + - '.github/workflows/den-1505-current-main-merge-diagnostic.yml' push: branches: - agent/den-1505-current-main-integration @@ -59,9 +62,6 @@ jobs: python3 - <<'PY' from pathlib import Path - for raw_path in Path('/tmp').glob('this-path-never-matches'): - raise AssertionError(raw_path) - for name in ('src/lib.rs', 'src/ops.rs'): lines = Path(name).read_text(encoding='utf-8').splitlines() print(f'::group::{name} conflict blocks') From bed188e0915a22124d627f85af1453ca8c85dbf1 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 16:50:37 -0500 Subject: [PATCH 78/80] ci(DEN-1505): commit bounded merge diagnostics from a disposable clone --- ...den-1505-current-main-merge-diagnostic.yml | 123 +++++++++++------- 1 file changed, 78 insertions(+), 45 deletions(-) diff --git a/.github/workflows/den-1505-current-main-merge-diagnostic.yml b/.github/workflows/den-1505-current-main-merge-diagnostic.yml index f1c781a0..c0a86f82 100644 --- a/.github/workflows/den-1505-current-main-merge-diagnostic.yml +++ b/.github/workflows/den-1505-current-main-merge-diagnostic.yml @@ -1,9 +1,6 @@ name: DEN-1505 current-main merge diagnostic on: - pull_request: - paths: - - '.github/workflows/den-1505-current-main-merge-diagnostic.yml' push: branches: - agent/den-1505-current-main-integration @@ -12,7 +9,7 @@ on: workflow_dispatch: permissions: - contents: read + contents: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -20,10 +17,11 @@ concurrency: env: CURRENT_MAIN_SHA: b82da352472e301425c175b78d9d2c1c0cd0f1db + INTEGRATION_BRANCH: agent/den-1505-current-main-integration jobs: conflict-map: - name: Map the exact semantic conflict blocks + name: Commit the bounded semantic conflict report runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -31,42 +29,55 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - persist-credentials: false - - name: Attempt the exact current-main merge without committing - id: merge + - name: Map conflicts in a disposable local clone shell: bash run: | set -euo pipefail git fetch --no-tags origin "$CURRENT_MAIN_SHA" + git clone --quiet --no-hardlinks . "$RUNNER_TEMP/merge-work" + git -C "$RUNNER_TEMP/merge-work" checkout --quiet --detach "$GITHUB_SHA" + set +e - git -c merge.conflictStyle=diff3 merge --no-commit --no-ff "$CURRENT_MAIN_SHA" - status=$? + git -C "$RUNNER_TEMP/merge-work" \ + -c merge.conflictStyle=diff3 \ + merge --no-commit --no-ff "$CURRENT_MAIN_SHA" + merge_status=$? set -e - echo "status=$status" >> "$GITHUB_OUTPUT" - git status --short - git diff --name-only --diff-filter=U | sort > "$RUNNER_TEMP/conflicts.txt" - cat "$RUNNER_TEMP/conflicts.txt" + test "$merge_status" -ne 0 - - name: Require the bounded two-file conflict surface - shell: bash - run: | - set -euo pipefail + git -C "$RUNNER_TEMP/merge-work" \ + diff --name-only --diff-filter=U | sort \ + > "$RUNNER_TEMP/conflicts.txt" printf '%s\n' src/lib.rs src/ops.rs > "$RUNNER_TEMP/expected.txt" cmp "$RUNNER_TEMP/expected.txt" "$RUNNER_TEMP/conflicts.txt" - - name: Render every conflict block with bounded context - shell: bash - run: | - set -euo pipefail - python3 - <<'PY' + mkdir -p merge-diagnostics + python3 - "$RUNNER_TEMP/merge-work" "$CURRENT_MAIN_SHA" <<'PY' from pathlib import Path + import json + import sys + + root = Path(sys.argv[1]) + main_sha = sys.argv[2] + paths = ('src/lib.rs', 'src/ops.rs') + markdown = [ + '# DEN-1505 current-main semantic conflict map', + '', + f'- installer-stack head: `{Path.cwd()}`', + f'- exact current main: `{main_sha}`', + '- expected conflicted paths: `src/lib.rs`, `src/ops.rs`', + '- generated from a disposable local clone; source files were not resolved or committed', + '', + ] + machine = {'currentMain': main_sha, 'conflicts': []} - for name in ('src/lib.rs', 'src/ops.rs'): - lines = Path(name).read_text(encoding='utf-8').splitlines() - print(f'::group::{name} conflict blocks') + for name in paths: + lines = (root / name).read_text(encoding='utf-8').splitlines() index = 0 - block = 0 + block_number = 0 + blocks = [] + markdown.extend([f'## `{name}`', '']) while index < len(lines): if not lines[index].startswith('<<<<<<< '): index += 1 @@ -84,24 +95,46 @@ jobs: index += 1 if index >= len(lines): raise SystemExit(f'unterminated conflict in {name}') - block += 1 - lo = max(0, start - 12) - hi = min(len(lines), index + 13) - print(f'--- block {block}: lines {start + 1}-{index + 1} ---') - for line_number in range(lo, hi): - print(f'{line_number + 1:05d}: {lines[line_number]}') + block_number += 1 + block_lines = lines[start:index + 1] + if len(block_lines) > 400: + raise SystemExit(f'conflict block too large in {name}: {len(block_lines)} lines') + blocks.append({ + 'number': block_number, + 'startLine': start + 1, + 'endLine': index + 1, + 'lines': block_lines, + }) + markdown.extend([ + f'### Block {block_number} — lines {start + 1}–{index + 1}', + '', + '```diff', + *block_lines, + '```', + '', + ]) index += 1 - print(f'conflict_blocks={block}') - print('::endgroup::') + if not blocks: + raise SystemExit(f'expected at least one conflict block in {name}') + machine['conflicts'].append({'path': name, 'blocks': blocks}) + + Path('merge-diagnostics/den-1505-current-main-conflicts.md').write_text( + '\n'.join(markdown) + '\n', encoding='utf-8' + ) + Path('merge-diagnostics/den-1505-current-main-conflicts.json').write_text( + json.dumps(machine, sort_keys=True, indent=2) + '\n', encoding='utf-8' + ) PY - - name: Upload the bounded conflicted files - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: den-1505-current-main-conflicts - path: | - src/lib.rs - src/ops.rs - ${{ runner.temp }}/conflicts.txt - if-no-files-found: error - retention-days: 1 + - name: Commit only the bounded diagnostic report + shell: bash + run: | + set -euo pipefail + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add \ + merge-diagnostics/den-1505-current-main-conflicts.md \ + merge-diagnostics/den-1505-current-main-conflicts.json + git diff --cached --check + git commit -m 'docs(DEN-1505): record exact current-main conflict blocks' + git push origin "HEAD:$INTEGRATION_BRANCH" From 5685a58e7de96dfe09955dd29f5be34676800f48 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 17:01:54 -0500 Subject: [PATCH 79/80] docs(DEN-1505/DEN-1585): document strict frozen and Nix composition --- docs/recursive-installs.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/recursive-installs.md b/docs/recursive-installs.md index be5dcd21..56376fa6 100644 --- a/docs/recursive-installs.md +++ b/docs/recursive-installs.md @@ -51,6 +51,25 @@ 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 From 24cc131dc206dd54884ec3bff6631821fdbbe87c Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Mon, 3 Aug 2026 17:03:36 -0500 Subject: [PATCH 80/80] ci(DEN-1505): rename blocking-lock suite as permanent contract Retain the complete Linux process-lock and recursive-cache test coverage while removing the legacy apply/bootstrap filename and updating its path filters. --- ...ore-process-locks.yml => blocking-store-process-locks.yml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{apply-blocking-store-process-locks.yml => blocking-store-process-locks.yml} (94%) diff --git a/.github/workflows/apply-blocking-store-process-locks.yml b/.github/workflows/blocking-store-process-locks.yml similarity index 94% rename from .github/workflows/apply-blocking-store-process-locks.yml rename to .github/workflows/blocking-store-process-locks.yml index 85a39020..aac5f964 100644 --- a/.github/workflows/apply-blocking-store-process-locks.yml +++ b/.github/workflows/blocking-store-process-locks.yml @@ -8,7 +8,7 @@ on: - src/install_graph/** - Cargo.toml - Cargo.lock - - .github/workflows/apply-blocking-store-process-locks.yml + - .github/workflows/blocking-store-process-locks.yml push: branches: [main] paths: @@ -17,7 +17,7 @@ on: - src/install_graph/** - Cargo.toml - Cargo.lock - - .github/workflows/apply-blocking-store-process-locks.yml + - .github/workflows/blocking-store-process-locks.yml workflow_dispatch: permissions: