Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions crates/fbuild-daemon/src/handlers/emulator/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,16 @@ pub(crate) fn qemu_session_dir(project_dir: &Path, env_name: &str) -> PathBuf {

pub(crate) fn build_linux_macos_qemu_hint(err: &str) -> String {
if cfg!(any(target_os = "linux", target_os = "macos")) {
let prefix = if err.is_empty() {
String::new()
} else {
format!("{}. ", err)
};
format!(
"{}. On Linux/macOS, ensure QEMU runtime deps are installed: libgcrypt, glib2, pixman, SDL2, and libslirp.",
err
"{}The cached QEMU toolchain may be incomplete or corrupt. \
On Linux/macOS, also ensure runtime deps are installed: \
libgcrypt, glib2, pixman, SDL2, and libslirp.",
prefix
)
} else {
err.to_string()
Expand Down Expand Up @@ -351,6 +358,16 @@ pub(crate) async fn run_qemu_process(
} else {
MonitorOutcome::Success(format!("{} exited normally", label))
}
} else if status.code() == Some(127) {
MonitorOutcome::Error(format!(
"{} failed to start (exit code 127): a required shared library is missing.\n\
{}\n\
The cached QEMU toolchain may be incomplete or corrupt.\n\
Delete the cached toolchain and retry:\n rm -rf $(dirname {})",
label,
build_linux_macos_qemu_hint(""),
qemu_path.display()
))
} else {
MonitorOutcome::Error(format!(
"{} exited with code {}",
Expand Down
53 changes: 49 additions & 4 deletions crates/fbuild-packages-fetch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,14 +270,26 @@ impl PackageBase {
}

/// Check if already installed in cache.
///
/// Requires both the install directory AND the `.install_complete` sentinel
/// to be present. A directory without the sentinel is an incomplete or
/// partial install (e.g. a CI cache restored from a crashed job before the
/// atomic rename was committed, or an extract interrupted mid-flight). The
/// caller should treat this as "not installed" so the package is
/// re-extracted rather than invoked with a corrupt tree.
///
/// On a cache hit, bumps the LRU timestamp in the DiskCache index.
pub fn is_cached(&self) -> bool {
let path = self.install_path();
let cached = path.exists() && path.is_dir();
if cached {
self.touch_disk_cache();
if !path.exists() || !path.is_dir() {
return false;
}
cached
let sentinel = disk_cache::paths::install_complete_sentinel(&path);
if !sentinel.exists() {
return false;
}
self.touch_disk_cache();
true
Comment on lines +273 to +292

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Repair incomplete cache directories before returning them.

At Line 344, staged_install returns an existing install directory before it validates or replaces it. The new predicate reports a missing sentinel as a cache miss, but the retry returns the same incomplete directory.

Write the sentinel in staging before the atomic rename. Treat a sentinel write failure as an install failure. Under the install lock, remove and reinstall an existing directory that does not meet the completeness requirement. Add a regression test that calls staged_install against an existing directory without the sentinel.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-packages-fetch/src/lib.rs` around lines 273 - 292, The
staged_install flow must repair incomplete existing installs instead of
returning them. Before atomically renaming the staging directory, write the
install-complete sentinel and propagate any write failure as an installation
error; under the install lock, remove an existing install directory when
is_cached reports it incomplete, then reinstall it. Add a regression test
covering staged_install with a pre-existing directory missing the sentinel.

Source: Coding guidelines

}

/// Best-effort LRU touch in the DiskCache index.
Expand Down Expand Up @@ -619,6 +631,9 @@ mod toolchain_gcc_ar_tests {
);
let install_path = base.install_path();
std::fs::create_dir_all(&install_path).unwrap();
// Write the sentinel so is_cached() passes the completeness check.
let sentinel = disk_cache::paths::install_complete_sentinel(&install_path);
std::fs::write(&sentinel, b"").unwrap();

let disk_cache = DiskCache::open_at(&cache_root).unwrap();
let rel_path = install_path.strip_prefix(disk_cache.cache_root()).unwrap();
Expand Down Expand Up @@ -697,6 +712,36 @@ mod toolchain_gcc_ar_tests {
);
assert!(disk_cache::paths::install_complete_sentinel(&installed).exists());
}

#[test]
fn is_cached_returns_false_when_sentinel_is_missing() {
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let cache_key = "missing-sentinel-tool";
let base = PackageBase::with_cache_root(
"tool",
"1.0",
cache_key,
cache_key,
None,
CacheSubdir::Toolchains,
tmp.path(),
&cache_root,
);
let install_path = base.install_path();
std::fs::create_dir_all(&install_path).unwrap();

// Directory exists but sentinel is missing → should NOT be considered cached.
assert!(
!base.is_cached(),
"directory without .install_complete sentinel must not be cached"
);

// Write the sentinel → now it should be cached.
let sentinel = disk_cache::paths::install_complete_sentinel(&install_path);
std::fs::write(&sentinel, b"").unwrap();
assert!(base.is_cached(), "directory with sentinel should be cached");
}
}

#[cfg(test)]
Expand Down
Loading
Loading