use super::design::Design;
use super::{checkpipe, sbom};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
const PORT: u16 = 8000;
const MUSL_TARGET: &str = "x86_64-unknown-linux-musl";
fn built_binary_path(app_root: &Path, cargo_target_dir: Option<&OsStr>, musl: bool) -> PathBuf {
let base = app_root.join(cargo_target_dir.unwrap_or_else(|| OsStr::new("target")));
let sub = if musl {
format!("{MUSL_TARGET}/release/app")
} else {
"release/app".to_string()
};
base.join(sub)
}
const STUB_MARKER: &str = "not implemented — replace this stub";
fn unimplemented_stubs(root: &Path) -> Vec<String> {
fn walk(dir: &Path, root: &Path, out: &mut Vec<String>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if path.file_name().and_then(|n| n.to_str()) == Some("target") {
continue;
}
walk(&path, root, out);
} else if path.extension().is_some_and(|x| x == "rs")
&& std::fs::read_to_string(&path).is_ok_and(|c| c.contains(STUB_MARKER))
{
out.push(
path.strip_prefix(root)
.unwrap_or(&path)
.display()
.to_string(),
);
}
}
}
let mut out = Vec::new();
walk(&root.join("crates"), root, &mut out);
out.sort();
out
}
pub fn run_package(
root: &Path,
design: &Design,
docker: bool,
k8s: bool,
systemd: bool,
binary: bool,
) -> Result<(Vec<String>, String), String> {
let stubs = unimplemented_stubs(root);
if !stubs.is_empty() {
return Err(format!(
"check failed: {} handler(s) still return the generated \"not implemented\" stub ({}) — implement them before packaging",
stubs.len(),
stubs.join(", ")
));
}
let report = checkpipe::run_all(root, design, None, false).map_err(|e| e.to_string())?;
if !report.ok {
return Err(format!(
"check failed ({} diagnostics) — fix before packaging",
report.diagnostics.len()
));
}
let mut artifacts = Vec::new();
let mut text_targets = Vec::new();
if docker {
text_targets.push("docker");
}
if k8s {
text_targets.push("k8s");
}
if systemd {
text_targets.push("systemd");
}
if !text_targets.is_empty() {
artifacts.extend(emit_text_artifacts(root, design, &text_targets)?);
}
if binary {
artifacts.push(build_binary(root, design)?);
}
let deploy = root.join("deploy");
std::fs::create_dir_all(&deploy).map_err(|e| e.to_string())?;
let sbom = sbom::generate(root, "app")?;
std::fs::write(deploy.join("sbom.json"), &sbom).map_err(|e| e.to_string())?;
artifacts.push("deploy/sbom.json".to_string());
Ok((artifacts, "deploy/sbom.json".to_string()))
}
pub fn dockerfile(design: &Design) -> String {
let name = &design.name;
format!(
r#"# GENERATED by jerrycan package — hardened, multi-stage, non-root.
# Requires `jerrycan` published to crates.io (or vendored into this context): the
# in-container `cargo build` below fetches it like any dependency.
FROM rust:1-bookworm AS build
WORKDIR /build
RUN rustup target add x86_64-unknown-linux-musl && \
(apt-get update && apt-get install -y musl-tools || true)
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl -p app
RUN cp target/x86_64-unknown-linux-musl/release/app /build/{name}
FROM gcr.io/distroless/static:nonroot
COPY --from=build /build/{name} /usr/local/bin/{name}
USER nonroot
EXPOSE {PORT}
ENV JERRYCAN_ADDR=0.0.0.0:{PORT}
ENTRYPOINT ["/usr/local/bin/{name}"]
"#
)
}
pub fn k8s_manifests(design: &Design) -> String {
let name = &design.name;
format!(
r#"# GENERATED by jerrycan package — hardened manifests. Edit the image, then `kubectl apply -f k8s.yaml`.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {name}
labels:
app: {name}
spec:
replicas: 2
selector:
matchLabels:
app: {name}
template:
metadata:
labels:
app: {name}
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: {name}
image: {name}:latest
ports:
- containerPort: {PORT}
env:
- name: JERRYCAN_ADDR
value: "0.0.0.0:{PORT}"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
livenessProbe:
httpGet:
path: /healthz
port: {PORT}
initialDelaySeconds: 2
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: {PORT}
initialDelaySeconds: 1
periodSeconds: 5
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 500m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: {name}
spec:
selector:
app: {name}
ports:
- port: 80
targetPort: {PORT}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {name}
spec:
podSelector:
matchLabels:
app: {name}
policyTypes:
- Ingress
ingress:
- ports:
- protocol: TCP
port: {PORT}
"#
)
}
pub fn systemd_unit(design: &Design) -> String {
let name = &design.name;
format!(
r#"# GENERATED by jerrycan package. Install: copy the binary to /usr/local/bin/{name},
# this file to /etc/systemd/system/{name}.service, then `systemctl enable --now {name}`.
[Unit]
Description={name} (jerrycan)
After=network.target
[Service]
ExecStart=/usr/local/bin/{name}
Environment=JERRYCAN_ADDR=0.0.0.0:{PORT}
Environment=JERRYCAN_ENV=prod
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
"#
)
}
pub fn emit_text_artifacts(
app_root: &Path,
design: &Design,
targets: &[&str],
) -> Result<Vec<String>, String> {
let deploy = app_root.join("deploy");
std::fs::create_dir_all(&deploy).map_err(|e| e.to_string())?;
let mut written = Vec::new();
let mut write = |rel: &str, content: &str| -> Result<(), String> {
let path = deploy.join(rel);
std::fs::write(&path, content).map_err(|e| format!("write deploy/{rel}: {e}"))?;
written.push(format!("deploy/{rel}"));
Ok(())
};
if targets.contains(&"docker") {
write("Dockerfile", &dockerfile(design))?;
}
if targets.contains(&"k8s") {
write("k8s.yaml", &k8s_manifests(design))?;
}
if targets.contains(&"systemd") {
write(&format!("{}.service", design.name), &systemd_unit(design))?;
}
Ok(written)
}
pub fn build_binary(app_root: &Path, design: &Design) -> Result<String, String> {
let musl_ok = std::process::Command::new("rustc")
.args(["--print", "target-list"])
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.any(|t| t == MUSL_TARGET)
})
.unwrap_or(false)
&& target_installed(MUSL_TARGET);
let target_args: Vec<&str> = if musl_ok {
vec!["--target", MUSL_TARGET]
} else {
eprintln!(
"jerrycan package: musl target unavailable — building a host-target binary (not fully static). Install with: rustup target add {MUSL_TARGET}"
);
vec![]
};
let mut args = vec!["build", "--release", "-p", "app"];
args.extend(target_args);
let status = std::process::Command::new("cargo")
.current_dir(app_root)
.args(&args)
.status()
.map_err(|e| format!("cargo build failed to run: {e}"))?;
if !status.success() {
return Err("release build failed".to_string());
}
let built_path = built_binary_path(
app_root,
std::env::var_os("CARGO_TARGET_DIR").as_deref(),
musl_ok,
);
let deploy = app_root.join("deploy");
std::fs::create_dir_all(&deploy).map_err(|e| e.to_string())?;
let dest = deploy.join(&design.name);
std::fs::copy(&built_path, &dest).map_err(|e| format!("copy binary: {e}"))?;
Ok(format!("deploy/{}", design.name))
}
fn target_installed(target: &str) -> bool {
std::process::Command::new("rustup")
.args(["target", "list", "--installed"])
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.any(|t| t == target)
})
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::platform::design::Design;
#[test]
fn built_binary_path_honors_cargo_target_dir() {
let app = Path::new("/app");
assert_eq!(
built_binary_path(app, None, false),
Path::new("/app/target/release/app")
);
assert_eq!(
built_binary_path(app, Some(OsStr::new("shared-target")), false),
Path::new("/app/shared-target/release/app")
);
assert_eq!(
built_binary_path(app, Some(OsStr::new("/build/out")), false),
Path::new("/build/out/release/app")
);
assert_eq!(
built_binary_path(app, Some(OsStr::new("/build/out")), true),
Path::new("/build/out/x86_64-unknown-linux-musl/release/app")
);
assert_ne!(
built_binary_path(app, Some(OsStr::new("/build/out")), false),
Path::new("/app/target/release/app")
);
}
#[test]
fn stub_gate_flags_scaffold_then_clears_when_implemented() {
let tmp = tempfile::tempdir().unwrap();
let app = tmp.path().join("app");
let design: Design = serde_json::from_str(crate::platform::design::tests::MINIMAL).unwrap();
crate::platform::scaffold::scaffold(&app, &design).unwrap();
let stubs = unimplemented_stubs(&app);
assert!(
stubs.iter().any(|p| p.ends_with("handlers.rs")),
"a fresh scaffold's handlers are unimplemented stubs: {stubs:?}"
);
for rel in &stubs {
std::fs::write(app.join(rel), "// implemented — no stub marker\n").unwrap();
}
assert!(
unimplemented_stubs(&app).is_empty(),
"no stub markers remain once handlers are implemented"
);
}
#[test]
fn run_package_refuses_a_stub_scaffold() {
let tmp = tempfile::tempdir().unwrap();
let app = tmp.path().join("app");
let design: Design = serde_json::from_str(crate::platform::design::tests::MINIMAL).unwrap();
crate::platform::scaffold::scaffold(&app, &design).unwrap();
let err = run_package(&app, &design, false, true, false, false)
.expect_err("a stub scaffold must not package");
assert!(err.contains("check"), "error names the check gate: {err}");
assert!(
!app.join("deploy/k8s.yaml").exists(),
"no artifacts on a refused package"
);
}
}