This commit is contained in:
2026-05-18 20:39:21 +02:00
parent 0d610fd2de
commit 0c9a3fde94
9 changed files with 930 additions and 22 deletions
+178 -1
View File
@@ -1 +1,178 @@
pub struct Builder;
use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{Context, bail};
use sha2::{Digest, Sha256};
use crate::{
config::Config,
graph::{TaskPlan, TaskPlanner},
log,
recipe::RecipeSet,
};
#[derive(Debug)]
pub struct Builder {
root: PathBuf,
config: Config,
container_ready: bool,
}
impl Builder {
pub fn new(root: PathBuf, config: Config) -> Self {
Self {
root,
config,
container_ready: false,
}
}
pub fn build(
&mut self,
recipes: &RecipeSet,
requested: &[String],
rebuild: bool,
dry_run: bool,
) -> anyhow::Result<()> {
let plan = TaskPlanner::new(&self.root, &self.config.arch, recipes)
.build_plan(requested, rebuild)?;
self.print_plan(&plan);
if !dry_run {
bail!("task execution is not implemented yet");
}
Ok(())
}
pub fn fetch(
&mut self,
recipes: &RecipeSet,
requested: &[String],
dry_run: bool,
) -> anyhow::Result<()> {
let plan =
TaskPlanner::new(&self.root, &self.config.arch, recipes).fetch_plan(requested)?;
self.print_plan(&plan);
if !dry_run {
bail!("task execution is not implemented yet");
}
Ok(())
}
pub fn ensure_container_ready(&mut self) -> anyhow::Result<()> {
if !self.container_ready {
self.ensure_container_image(&self.abs_config_path(&self.config.container_dockerfile))?;
self.container_ready = true;
}
Ok(())
}
fn ensure_container_image(&self, dockerfile: &Path) -> anyhow::Result<()> {
if !dockerfile.exists() {
bail!(
"configured container Dockerfile does not exist: {}",
dockerfile.display()
);
}
let hash = self.container_build_hash(dockerfile)?;
let stamp = self.root.join("build/container-image.hash");
if fs::read_to_string(&stamp).ok().as_deref() == Some(hash.as_str())
&& self.container_image_exists()?
{
log::skip(
"image",
&format!("using cached {}", self.config.container_image),
);
return Ok(());
}
log::step(
"image",
&format!(
"building {} from {}",
self.config.container_image,
dockerfile.display()
),
);
let runtime = self.config.container_runtime.as_str();
let status = Command::new(runtime)
.arg("build")
.arg("-f")
.arg(dockerfile)
.arg("-t")
.arg(&self.config.container_image)
.arg(&self.root)
.status()
.with_context(|| {
format!(
"failed to build container image `{}` from {}",
self.config.container_image,
dockerfile.display()
)
})?;
if !status.success() {
bail!(
"container image build failed for `{}` with {status}",
self.config.container_image
);
}
fs::create_dir_all(stamp.parent().unwrap())?;
fs::write(stamp, hash)?;
Ok(())
}
fn container_image_exists(&self) -> anyhow::Result<bool> {
let runtime = self.config.container_runtime.as_str();
let status = Command::new(runtime)
.arg("image")
.arg("exists")
.arg(&self.config.container_image)
.status()
.with_context(|| {
format!(
"failed to inspect container image `{}`",
self.config.container_image
)
})?;
Ok(status.success())
}
fn container_build_hash(&self, dockerfile: &Path) -> anyhow::Result<String> {
let mut hasher = Sha256::new();
hasher.update(fs::read(dockerfile)?);
hasher.update(self.config.container_image.as_bytes());
Ok(hex::encode(hasher.finalize()))
}
fn print_plan(&self, plan: &TaskPlan) {
if plan.is_empty() {
log::skip("plan", "nothing to do");
return;
}
log::step(
"plan",
&format!(
"{} active task(s), {} edge(s)",
plan.order().len(),
plan.dependency_count()
),
);
for task in plan.order() {
println!("{task}");
}
}
fn abs_config_path(&self, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
self.root.join(path)
}
}
}