fix: harden build cache against 7 correctness issues
C1 (Critical): Binary store restore now iterates ALL stage directories instead of only the first, matching how cook_creates handles multi-stage. C2 (Critical): All binary store restore errors are now logged via log_to_pty! instead of being silently discarded with let _ =. H1 (High): dep_hashes.toml keys now use full PackageName (including host: prefix) via name.to_string() instead of name.without_prefix(), preventing host/target key collisions. H2 (High): Patch file mtimes are now included in source_modified calculation, so editing a patch correctly triggers a rebuild. H4 (High): All to_str().unwrap() calls replaced with safe alternatives (to_string_lossy, direct PathBuf refs) to prevent panics on non-UTF8 paths. H5 (High): auto_deps.toml reconstruction now logs a warning that it may be incomplete (does not include ELF-discovered dynamic linking deps). M1 (Medium): dep_hashes.toml is now written atomically via write-to-tmp + fs::rename, preventing corrupted/partial files on crash. M3 (Medium): Missing source dir now triggers rebuild (SystemTime::now() fallback) instead of being masked as no-change via UNIX_EPOCH.
This commit is contained in:
+1
-1
Submodule local/sources/installer updated: e45ce4d57a...8d1bad9eb4
+99
-32
@@ -6,7 +6,7 @@ use crate::cook::package::{package_source_paths, package_target};
|
||||
use crate::cook::pty::PtyOut;
|
||||
use crate::cook::script::*;
|
||||
use crate::cook::{fetch, fs::*};
|
||||
use crate::recipe::Recipe;
|
||||
use crate::recipe::{Recipe, SourceRecipe};
|
||||
use crate::recipe::{AutoDeps, CookRecipe};
|
||||
use crate::recipe::{BuildKind, OptionalPackageRecipe};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -38,8 +38,12 @@ impl DepHashes {
|
||||
pub fn write(&self, path: &Path) -> Result<(), String> {
|
||||
let content = toml::to_string(self)
|
||||
.map_err(|e| format!("failed to serialize dep_hashes.toml: {e}"))?;
|
||||
fs::write(path, content)
|
||||
.map_err(|e| format!("failed to write dep_hashes.toml: {e:?}"))?;
|
||||
// Atomic write: write to temp file, then rename
|
||||
let tmp_path = path.with_extension("tmp");
|
||||
fs::write(&tmp_path, &content)
|
||||
.map_err(|e| format!("failed to write dep_hashes.toml tmp: {e:?}"))?;
|
||||
fs::rename(&tmp_path, path)
|
||||
.map_err(|e| format!("failed to rename dep_hashes.toml: {e:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -54,11 +58,16 @@ fn collect_current_dep_hashes(
|
||||
if let Ok(toml_content) = fs::read_to_string(&toml_path) {
|
||||
if let Ok(pkg) = toml::from_str::<Package>(&toml_content) {
|
||||
if !pkg.blake3.is_empty() {
|
||||
hashes.insert(name.without_prefix().to_string(), pkg.blake3);
|
||||
// Key by full package name (including host: prefix) to avoid
|
||||
// collisions between host and target deps with the same base name.
|
||||
hashes.insert(name.to_string(), pkg.blake3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Missing .toml or parse failure: dep is excluded from current hashes.
|
||||
// If it was previously tracked, dep_hashes_changed() will detect the
|
||||
// length mismatch and trigger a rebuild (conservative behavior).
|
||||
}
|
||||
hashes
|
||||
}
|
||||
@@ -328,51 +337,109 @@ pub fn build(
|
||||
.map(|t| PathBuf::from("repo").join(t))
|
||||
.unwrap_or_else(|| PathBuf::from("repo").join(redoxer::target()));
|
||||
let recipe_name = name.name();
|
||||
let main_pkgar = repo_target.join(format!("{}.pkgar", recipe_name));
|
||||
let main_toml = repo_target.join(format!("{}.toml", recipe_name));
|
||||
if main_pkgar.is_file() && main_toml.is_file() {
|
||||
if cli_verbose {
|
||||
log_to_pty!(logger, "DEBUG: restoring stage artifacts from repo/ binary store");
|
||||
let pkey_path = "build/id_ed25519.pub.toml";
|
||||
let pkey_exists = Path::new(pkey_path).is_file();
|
||||
|
||||
let mut all_restored = true;
|
||||
for (stage_dir, stage_pkgar) in stage_dirs.iter().zip(stage_pkgars.iter()) {
|
||||
if stage_pkgar.is_file() && stage_dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
if let Some(last_stage) = stage_dirs.last() {
|
||||
if !last_stage.is_dir() {
|
||||
let pkey_path = "build/id_ed25519.pub.toml";
|
||||
if Path::new(pkey_path).is_file() {
|
||||
let _ = pkgar::extract(pkey_path, &main_pkgar, last_stage.to_str().unwrap());
|
||||
|
||||
// Derive the repo pkgar name from the stage dir name.
|
||||
// "stage" → recipe_name, "stage.feature" → recipe_name.feature
|
||||
let stage_suffix = stage_dir
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.and_then(|s| s.strip_prefix("stage"))
|
||||
.unwrap_or("");
|
||||
let repo_pkgar_name = format!("{}{}.pkgar", recipe_name, stage_suffix);
|
||||
let repo_toml_name = format!("{}{}.toml", recipe_name, stage_suffix);
|
||||
let repo_pkgar = repo_target.join(&repo_pkgar_name);
|
||||
let repo_toml = repo_target.join(&repo_toml_name);
|
||||
|
||||
if !repo_pkgar.is_file() || !repo_toml.is_file() {
|
||||
if stage_suffix.is_empty() {
|
||||
log_to_pty!(logger, "WARN: binary store missing {} — will rebuild", repo_pkgar.display());
|
||||
}
|
||||
all_restored = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if !stage_dir.is_dir() {
|
||||
if pkey_exists {
|
||||
if let Err(e) = pkgar::extract(pkey_path, &repo_pkgar, stage_dir) {
|
||||
log_to_pty!(logger, "WARN: pkgar extract failed for {}: {e}", repo_pkgar.display());
|
||||
all_restored = false;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
log_to_pty!(logger, "WARN: signing key {} missing — cannot extract {}", pkey_path, repo_pkgar.display());
|
||||
all_restored = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(last_pkgar) = stage_pkgars.last() {
|
||||
if !last_pkgar.is_file() {
|
||||
let _ = fs::copy(&main_pkgar, last_pkgar);
|
||||
|
||||
if !stage_pkgar.is_file() {
|
||||
if let Err(e) = fs::copy(&repo_pkgar, stage_pkgar) {
|
||||
log_to_pty!(logger, "WARN: failed to copy {}: {e}", repo_pkgar.display());
|
||||
all_restored = false;
|
||||
}
|
||||
}
|
||||
if let Some(last_toml) = stage_pkgars.last().map(|p| p.with_extension("toml")) {
|
||||
if !last_toml.is_file() {
|
||||
let _ = fs::copy(&main_toml, &last_toml);
|
||||
|
||||
let stage_toml = stage_pkgar.with_extension("toml");
|
||||
if !stage_toml.is_file() {
|
||||
if let Err(e) = fs::copy(&repo_toml, &stage_toml) {
|
||||
log_to_pty!(logger, "WARN: failed to copy {}: {e}", repo_toml.display());
|
||||
}
|
||||
}
|
||||
let repo_dep_hashes = repo_target.join(format!("{}.dep_hashes.toml", recipe_name));
|
||||
let local_dep_hashes = get_sub_target_dir(target_dir, "dep_hashes.toml");
|
||||
if repo_dep_hashes.is_file() && !local_dep_hashes.is_file() {
|
||||
let _ = fs::copy(&repo_dep_hashes, &local_dep_hashes);
|
||||
}
|
||||
|
||||
let repo_dep_hashes = repo_target.join(format!("{}.dep_hashes.toml", recipe_name));
|
||||
let local_dep_hashes = get_sub_target_dir(target_dir, "dep_hashes.toml");
|
||||
if repo_dep_hashes.is_file() && !local_dep_hashes.is_file() && all_restored {
|
||||
if let Err(e) = fs::copy(&repo_dep_hashes, &local_dep_hashes) {
|
||||
log_to_pty!(logger, "WARN: failed to copy dep_hashes.toml: {e}");
|
||||
}
|
||||
if !auto_deps_file.is_file() {
|
||||
if let Ok(toml_content) = fs::read_to_string(&main_toml) {
|
||||
if let Ok(pkg_meta) = toml::from_str::<Package>(&toml_content) {
|
||||
let auto: BTreeSet<PackageName> = pkg_meta.depends.into_iter().collect();
|
||||
let wrapper = AutoDeps { packages: auto };
|
||||
let _ = serialize_and_write(&auto_deps_file, &wrapper);
|
||||
}
|
||||
|
||||
// Reconstruct auto_deps.toml from repo metadata (may be incomplete —
|
||||
// does not include ELF-discovered dynamic linking deps)
|
||||
if !auto_deps_file.is_file() && all_restored {
|
||||
let main_toml = repo_target.join(format!("{}.toml", recipe_name));
|
||||
if let Ok(toml_content) = fs::read_to_string(&main_toml) {
|
||||
if let Ok(pkg_meta) = toml::from_str::<Package>(&toml_content) {
|
||||
let auto: BTreeSet<PackageName> = pkg_meta.depends.into_iter().collect();
|
||||
let wrapper = AutoDeps { packages: auto };
|
||||
if let Err(e) = serialize_and_write(&auto_deps_file, &wrapper) {
|
||||
log_to_pty!(logger, "WARN: failed to write auto_deps.toml: {e}");
|
||||
}
|
||||
if cli_verbose {
|
||||
log_to_pty!(logger, "DEBUG: auto_deps.toml reconstructed from repo depends (may miss ELF dynamic deps)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut source_modified = modified_dir_ignore_git(source_dir).unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
let mut source_modified = modified_dir_ignore_git(source_dir).unwrap_or_else(|_| {
|
||||
log_to_pty!(logger, "WARN: source dir {} inaccessible — treating as modified", source_dir.display());
|
||||
SystemTime::now()
|
||||
});
|
||||
if let Ok(recipe_modified) = modified(&recipe_dir.join("recipe.toml")) {
|
||||
source_modified = source_modified.max(recipe_modified);
|
||||
}
|
||||
let recipe_patches: &[String] = match &recipe.source {
|
||||
Some(SourceRecipe::Git { patches, .. }) => patches,
|
||||
Some(SourceRecipe::Tar { patches, .. }) => patches,
|
||||
_ => &[],
|
||||
};
|
||||
for patch in recipe_patches {
|
||||
let patch_path = recipe_dir.join(patch);
|
||||
if let Ok(patch_modified) = modified(&patch_path) {
|
||||
source_modified = source_modified.max(patch_modified);
|
||||
}
|
||||
}
|
||||
|
||||
let deps_modified = modified_all_btree(
|
||||
dep_pkgars.iter().map(|(_dep, pkgar)| pkgar.as_path()),
|
||||
@@ -778,7 +845,7 @@ fn build_deps_dir(
|
||||
let tag_file = tags_dir.join(name.without_prefix());
|
||||
fs::write(&tag_file, "")
|
||||
.map_err(|e| format!("failed to write tag file {}: {:?}", tag_file.display(), e))?;
|
||||
pkgar::extract(pkey_path, &archive_path, deps_dir_tmp.to_str().unwrap()).map_err(
|
||||
pkgar::extract(pkey_path, &archive_path, &deps_dir_tmp).map_err(
|
||||
|err| {
|
||||
format!(
|
||||
"failed to install '{}' in '{}': {:?}",
|
||||
|
||||
Reference in New Issue
Block a user