🥇 export from upstream (fe643f3)

This commit is contained in:
sandcage-export
2026-05-22 01:57:18 +02:00
parent 1bc5dc11dc
commit f2f7206d06
3 changed files with 153 additions and 37 deletions
+112 -14
View File
@@ -1,3 +1,4 @@
#![allow(unused_assignments)]
use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
@@ -285,10 +286,8 @@ fn write_stored_hashes(path: &Path, hashes: &HashMap<String, String>) -> Result<
Ok(())
}
/// Build a single image.
/// Writes the `dockerfile_content` to a temp directory, then invokes
/// `docker build -t <image>:latest <temp_dir>`.
fn build_one_image(docker: &Path, image: &str, dockerfile_content: &str) -> Result<()> {
/// Build a single image from inline Dockerfile content (temp build context).
fn build_one_image_from_content(docker: &Path, image: &str, dockerfile_content: &str) -> Result<()> {
let tmp_dir = tempfile::Builder::new()
.prefix("sandcage-build-")
.tempdir()
@@ -298,14 +297,44 @@ fn build_one_image(docker: &Path, image: &str, dockerfile_content: &str) -> Resu
std::fs::write(&dockerfile_path, dockerfile_content)
.map_err(DockerError::DockerfileWriteFailed)?;
let status = Command::new(docker)
.args(["build", "-t", &format!("{image}:latest")])
.arg(tmp_dir.path())
run_docker_build(docker, image, tmp_dir.path(), None)
}
/// Build a single image from a user-provided path.
/// If the path is a directory, it's used as the build context.
/// If the path is a file, it's used as the Dockerfile with a temp context.
fn build_one_image_from_path(docker: &Path, image: &str, override_path: &Path) -> Result<()> {
if override_path.is_dir() {
run_docker_build(docker, image, override_path, None)
} else {
let tmp_dir = tempfile::Builder::new()
.prefix("sandcage-build-")
.tempdir()
.map_err(DockerError::TempDirFailed)?;
run_docker_build(docker, image, tmp_dir.path(), Some(override_path))
}
}
fn run_docker_build(
docker: &Path,
image: &str,
context: &Path,
dockerfile: Option<&Path>,
) -> Result<()> {
let mut cmd = Command::new(docker);
cmd.args(["build", "-t", &format!("{image}:latest")]);
if let Some(df) = dockerfile {
cmd.args(["-f", &df.to_string_lossy()]);
}
cmd.arg(context)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.map_err(DockerError::SpawnFailed)?;
.stderr(std::process::Stdio::inherit());
let status = cmd.status().map_err(DockerError::SpawnFailed)?;
if !status.success() {
return Err(DockerError::BuildFailed {
@@ -317,14 +346,67 @@ fn build_one_image(docker: &Path, image: &str, dockerfile_content: &str) -> Resu
Ok(())
}
/// Resolve the Dockerfile content for hashing purposes.
/// Returns the content string for override files/dirs, or None if the path is invalid.
fn read_dockerfile_at(path: &Path) -> Result<String> {
let dockerfile_path = if path.is_dir() {
path.join("Dockerfile")
} else {
path.to_path_buf()
};
std::fs::read_to_string(&dockerfile_path).map_err(DockerError::HashReadFailed)
}
/// Build all three sandcage images with cache awareness.
///
/// * Resolves layered config to check for Dockerfile overrides.
/// * When `force` is `true`, every image is rebuilt regardless of the stored hash.
/// * Otherwise, an image is skipped when its Dockerfile hash matches the stored value.
/// * After a successful build the hash is updated in `~/.sandcage/.build-hashes`.
pub fn build_images(force: bool) -> Result<()> {
let docker = require_docker()?;
let home = dirs::home_dir().ok_or(DockerError::NoHomeDir)?;
let global_config = home.join(".sandcage").join("config.toml");
let project_dir = std::env::current_dir().unwrap_or_default();
let project_config = project_dir.join(".sandcage.yml");
// Global overrides are always trusted.
let global_cfg = crate::config::resolve_config(Some(&global_config), None)
.unwrap_or_default();
let mut overrides = global_cfg.dockerfiles.unwrap_or_default();
// Project overrides require the project to be in trusted_projects.
let project_cfg = if project_config.exists() {
crate::config::load(&project_config).ok()
} else {
None
};
if let Some(ref pcfg) = project_cfg
&& pcfg.dockerfiles.is_some()
{
let trusted = crate::config::load_trusted_projects(&global_config);
let is_trusted = trusted.iter().any(|p| {
let canonical_trusted = std::fs::canonicalize(p).unwrap_or_else(|_| p.clone());
let canonical_project = std::fs::canonicalize(&project_dir)
.unwrap_or_else(|_| project_dir.clone());
canonical_project.starts_with(canonical_trusted)
});
if is_trusted {
overrides.extend(pcfg.dockerfiles.clone().unwrap_or_default());
} else {
eprintln!(
"sandcage: warning: project Dockerfile overrides ignored (not in trusted_projects)"
);
eprintln!(
"sandcage: add this project to trusted_projects in ~/.sandcage/config.toml"
);
}
}
let images: &[(&str, &str)] = &[
("sandcage-base", DOCKERFILE_BASE),
("sandcage-claude", DOCKERFILE_CLAUDE),
@@ -334,17 +416,33 @@ pub fn build_images(force: bool) -> Result<()> {
let hashes_path = build_hashes_path()?;
let mut stored = read_stored_hashes(&hashes_path)?;
for (image, dockerfile) in images {
let current_hash = sha256_hex(dockerfile);
let needs_build = force || stored.get(*image).map_or(true, |h| h != &current_hash);
for (image, bundled_dockerfile) in images {
let short_name = image.strip_prefix("sandcage-").unwrap_or(image);
let override_path = overrides.get(short_name);
let current_hash = if let Some(path) = override_path {
sha256_hex(&read_dockerfile_at(path)?)
} else {
sha256_hex(bundled_dockerfile)
};
let needs_build = force || stored.get(*image) != Some(&current_hash);
if needs_build {
if force {
eprintln!("{image}: rebuilding (--force)");
} else if override_path.is_some() {
eprintln!("{image}: rebuilding (custom Dockerfile changed)");
} else {
eprintln!("{image}: rebuilding (Dockerfile changed)");
}
build_one_image(&docker, image, dockerfile)?;
if let Some(path) = override_path {
build_one_image_from_path(&docker, image, path)?;
} else {
build_one_image_from_content(&docker, image, bundled_dockerfile)?;
}
stored.insert(image.to_string(), current_hash);
write_stored_hashes(&hashes_path, &stored)?;
} else {