🥇 export from upstream (fe643f3)
This commit is contained in:
+112
-14
@@ -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 != ¤t_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(¤t_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 {
|
||||
|
||||
Reference in New Issue
Block a user