diff --git a/cli/src/cargo.rs b/cli/src/cargo.rs index 30f9355..c13d6b2 100644 --- a/cli/src/cargo.rs +++ b/cli/src/cargo.rs @@ -61,6 +61,7 @@ pub struct Cargo { runner: PathBuf, target: PathBuf, sdk: PathBuf, + sdk_files: Arc>>, } impl Cargo { /// Installs changed SDK files and selects the current sausage executable as runner. @@ -126,25 +127,9 @@ impl Cargo { let cached = std::path::absolute(cache)? .join("sdk") .join(format!("{:x}", hash.finalize())); - if !cached.is_dir() { - let parent = cached.parent().unwrap(); - std::fs::create_dir_all(parent) - .with_context(|| format!("create {}", parent.display()))?; - let staging = tempfile::Builder::new() - .prefix("install-") - .tempdir_in(parent)?; - for (path, bytes) in &files { - write_changed(&staging.path().join(path), bytes)?; - } - // Another builder can publish the same complete, content-addressed SDK first. - if let Err(error) = std::fs::rename(staging.path(), &cached) - && !cached.is_dir() - { - return Err(error).with_context(|| format!("install {}", cached.display())); - } - } Ok(Self { sdk: cached, + sdk_files: Arc::new(files), target: std::path::absolute(cache)?.join("cargo-target"), world, runner, @@ -177,7 +162,27 @@ impl Cargo { let source = source.to_path_buf(); let copy = work.clone(); let sdk = self.sdk.clone(); + let sdk_files = self.sdk_files.clone(); let build_guard = tokio::task::spawn_blocking(move || -> Result<_> { + // Host startup installs only the readable world copy. Builds populate the cache. + if !sdk.is_dir() { + let parent = sdk.parent().unwrap(); + std::fs::create_dir_all(parent) + .with_context(|| format!("create {}", parent.display()))?; + let staging = tempfile::Builder::new() + .prefix("install-") + .tempdir_in(parent) + .with_context(|| format!("stage SDK in {}", parent.display()))?; + for (path, bytes) in sdk_files.iter() { + write_changed(&staging.path().join(path), bytes)?; + } + // Another builder can publish the same complete SDK first. + if let Err(error) = std::fs::rename(staging.path(), &sdk) + && !sdk.is_dir() + { + return Err(error).with_context(|| format!("install {}", sdk.display())); + } + } if cleanup.exists() { std::fs::remove_dir_all(&cleanup) .with_context(|| format!("remove {}", cleanup.display()))?; diff --git a/cli/tests/cargo.rs b/cli/tests/cargo.rs index 58f8dd3..6711b18 100644 --- a/cli/tests/cargo.rs +++ b/cli/tests/cargo.rs @@ -268,6 +268,42 @@ async fn native_discovery_and_error_paths() { ); host.stop().await; } +/// Rework 3: an unavailable cache prevents builds, not host startup or SDK installation. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn cache_failure_is_deferred_until_build() { + let world = TestWorld::new(); + world.source("simple", &fixture("cargo-test-fail")); + // A file blocks directory creation even when the test runs as root. + let cache = world.0.join("blocked-cache"); + std::fs::write(&cache, "not a directory").unwrap(); + let host = Host::start_with_cache(&world.0, &cache).await.unwrap(); + assert!(world.0.join("plugins/cargo/sdk/src/lib.rs").is_file()); + let error = host + .call("cargo.build", json!({"plugin":"simple"})) + .await + .unwrap_err(); + assert_eq!(error["code"], -32004); + let output = error["data"]["output"].as_str().unwrap(); + assert!(output.contains(cache.to_str().unwrap()), "{output}"); + assert!(output.contains("create"), "{output}"); + host.stop().await; + + let standalone = TestWorld::new(); + standalone.source("simple", &fixture("cargo-test-fail")); + let builder = sausage::cargo::Cargo::with_runner( + World::open(&standalone.0).unwrap(), + PathBuf::from(env!("CARGO_BIN_EXE_sausage")), + &cache, + ) + .unwrap(); + assert!(standalone.0.join("plugins/cargo/sdk/src/lib.rs").is_file()); + let error = builder + .build(&standalone.0.join("plugins/simple/source")) + .await + .unwrap_err(); + assert!(error.output.contains(cache.to_str().unwrap()), "{error}"); + assert!(cache.is_file()); +} /// T23: release-only failures use data.output and retain successful WASI totals. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn release_only_failure() {