use std::path::Path;
use std::process::Command;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum BuilderRequest {
Build {
flake_ref: String,
attr: String,
timeout_secs: Option<u64>,
},
Ping,
}
pub const BUILDER_AGENT_PORT: u32 = 21470;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum BuilderResponse {
Ok { out_path: String },
Err { message: String },
Log { line: String },
Pong,
}
fn log_frame(line: &str) -> BuilderResponse {
BuilderResponse::Log {
line: line.to_string(),
}
}
pub fn handle_request(req: BuilderRequest) -> Result<BuilderResponse> {
match req {
BuilderRequest::Ping => Ok(BuilderResponse::Pong),
BuilderRequest::Build {
flake_ref,
attr,
timeout_secs,
} => {
let timeout = timeout_secs.unwrap_or(1800);
let out_mount = Path::new("/build-out");
if !out_mount.is_dir() {
return Ok(BuilderResponse::Err {
message: "/build-out missing or not a directory".into(),
});
}
if Command::new("sh")
.arg("-c")
.arg("mountpoint -q /build-out || (mkdir -p /build-out && mount /dev/vdb /build-out)")
.status()
.is_err()
{
}
if flake_ref == "/build-in" {
let _ = Command::new("sh").arg("-c").arg(
"mountpoint -q /build-in || (mkdir -p /build-in && mount /dev/vdc /build-in)",
).status();
}
let build_cmd = format!(
"timeout {} nix build {}#{} --no-link --print-out-paths",
timeout, flake_ref, attr
);
let output = Command::new("sh")
.arg("-c")
.arg(&build_cmd)
.output()
.with_context(|| "failed to run nix build")?;
for line in String::from_utf8_lossy(&output.stdout).lines() {
let _ = log_frame(line);
}
for line in String::from_utf8_lossy(&output.stderr).lines() {
let _ = log_frame(line);
}
if !output.status.success() {
return Ok(BuilderResponse::Err {
message: format!("nix build failed (exit {}): {}", output.status, build_cmd),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
let out_path = stdout
.lines()
.rev()
.find(|l| l.starts_with("/nix/store/"))
.map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("nix build produced no store path"))?;
let copy_cmd = format!(
"set -euo pipefail; \
cp {p}/kernel /build-out/vmlinux 2>/dev/null || cp {p}/vmlinux /build-out/vmlinux; \
cp {p}/rootfs /build-out/rootfs.ext4 2>/dev/null || cp {p}/rootfs.ext4 /build-out/rootfs.ext4; \
echo '{{\"note\":\"Base fc config placeholder\"}}' > /build-out/fc-base.json",
p = out_path
);
let copy_out = Command::new("sh")
.arg("-c")
.arg(©_cmd)
.output()
.with_context(|| "failed to copy build artifacts")?;
if !copy_out.status.success() {
return Ok(BuilderResponse::Err {
message: format!("failed to copy artifacts: exit {}", copy_out.status),
});
}
Ok(BuilderResponse::Ok { out_path })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_round_trip() {
let req = BuilderRequest::Build {
flake_ref: ".".into(),
attr: "packages.aarch64-linux.tenant-worker".into(),
timeout_secs: Some(123),
};
let json = serde_json::to_string(&req).unwrap();
let back: BuilderRequest = serde_json::from_str(&json).unwrap();
assert_eq!(req, back);
}
}