use anyhow::{Context, Result};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
pub const WSC_BIN: &str = "wsc";
pub fn build_sign_argv(elf: &Path, signed_tmp: &Path) -> (PathBuf, Vec<PathBuf>) {
(
PathBuf::from(WSC_BIN),
vec![
PathBuf::from("sign"),
PathBuf::from("--keyless"),
PathBuf::from("--format"),
PathBuf::from("elf"),
PathBuf::from("-i"),
elf.to_path_buf(),
PathBuf::from("-o"),
signed_tmp.to_path_buf(),
],
)
}
pub fn signing_tmp_path(elf: &Path) -> PathBuf {
let mut p = elf.as_os_str().to_owned();
p.push(".signing.tmp");
PathBuf::from(p)
}
pub fn sign_elf(elf: &Path) -> Result<()> {
let tmp = signing_tmp_path(elf);
let (program, args) = build_sign_argv(elf, &tmp);
let output = Command::new(&program)
.args(args.iter().map(|p| p.as_os_str()).collect::<Vec<&OsStr>>())
.output()
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => anyhow::anyhow!(
"`wsc` (sigil signing CLI) not found on PATH. \
`synth compile --sign-output` requires sigil's `wsc` binary; \
install it from https://github.com/pulseengine/sigil and \
ensure it is on PATH. Original error: {e}"
),
_ => anyhow::Error::new(e).context(format!(
"failed to invoke `{}` to sign {}",
program.display(),
elf.display()
)),
})?;
if !output.status.success() {
let _ = std::fs::remove_file(&tmp);
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
anyhow::bail!(
"wsc signing failed (exit {}). \
If `wsc`'s CLI surface has changed (e.g. the `sign --format elf` \
shape we depend on), update crates/synth-cli/src/sign.rs. \
wsc stderr: {}\nwsc stdout: {}",
output.status,
stderr.trim(),
stdout.trim(),
);
}
if !tmp.exists() {
anyhow::bail!(
"wsc reported success but did not produce a signed file at {}. \
The sigil interface contract may have changed.",
tmp.display()
);
}
std::fs::rename(&tmp, elf).with_context(|| {
format!(
"wsc produced signed ELF {} but renaming it over {} failed",
tmp.display(),
elf.display()
)
})?;
println!("signed: {}", elf.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn argv_shape_matches_wsc_contract() {
let elf = Path::new("/tmp/example.elf");
let tmp = signing_tmp_path(elf);
let (program, args) = build_sign_argv(elf, &tmp);
assert_eq!(program, PathBuf::from("wsc"));
let arg_strs: Vec<&str> = args.iter().map(|p| p.to_str().unwrap()).collect();
assert_eq!(
arg_strs,
vec![
"sign",
"--keyless",
"--format",
"elf",
"-i",
"/tmp/example.elf",
"-o",
"/tmp/example.elf.signing.tmp",
]
);
}
#[test]
fn signing_tmp_path_is_deterministic_sibling() {
let elf = Path::new("/some/dir/firmware.elf");
assert_eq!(
signing_tmp_path(elf),
PathBuf::from("/some/dir/firmware.elf.signing.tmp")
);
}
#[test]
fn missing_wsc_produces_actionable_error() {
if which_wsc().is_some() {
eprintln!("skipping: `wsc` is on PATH in this environment");
return;
}
let err = sign_elf(Path::new("/tmp/nonexistent-synth-sign-test.elf"))
.expect_err("sign_elf must fail when wsc is missing");
let msg = format!("{:#}", err);
assert!(
msg.contains("wsc") && msg.contains("sigil"),
"error should mention wsc and sigil; got: {msg}"
);
}
fn which_wsc() -> Option<PathBuf> {
let path_var = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join(WSC_BIN);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
}