use std::io::Write as _;
use std::path::{Path, PathBuf};
use clap::Args;
use crate::compile::manifest;
use crate::compile::policy;
use crate::compile::trailer::{self, Trailer, TrailerKind};
const EXIT_REJECTION: i32 = 2;
#[derive(Args, Debug)]
pub struct CompileArgs {
#[arg(value_name = "DOCUMENT")]
pub document: PathBuf,
#[arg(short, long, value_name = "ARTIFACT")]
pub output: PathBuf,
#[arg(long, value_name = "TRIPLE")]
pub target: Option<String>,
}
fn native_triple() -> String {
let libc = if cfg!(target_env = "musl") {
"musl"
} else {
"gnu"
};
format!("{}-unknown-linux-{}", std::env::consts::ARCH, libc)
}
pub fn run_compile(args: &CompileArgs) -> i32 {
if !cfg!(target_os = "linux") {
eprintln!(
"camel compile v1 supports native Linux only; this host is {}",
std::env::consts::OS
);
return EXIT_REJECTION;
}
if let Some(requested) = &args.target
&& *requested != native_triple()
{
eprintln!(
"camel compile v1 supports native Linux only: target '{requested}' does not match the native target '{}'",
native_triple()
);
return EXIT_REJECTION;
}
let overrides: Vec<String> = std::env::vars_os()
.filter_map(|(name, _)| {
let name = name.to_string_lossy();
name.starts_with("CAMEL_").then(|| name.into_owned())
})
.collect();
if !overrides.is_empty() {
eprintln!(
"camel compile v1 requires a clean compile environment; CAMEL_* variable(s) present: {}",
overrides.join(", ")
);
return EXIT_REJECTION;
}
let cwd = match std::env::current_dir() {
Ok(cwd) => cwd,
Err(e) => {
eprintln!("camel compile: cannot determine the working directory: {e}");
return EXIT_REJECTION;
}
};
if cwd.join("Camel.toml").is_file() {
eprintln!(
"camel compile v1 rejects a Camel.toml in the compile working directory ('{}'); compiled documents must be self-contained",
cwd.display()
);
return EXIT_REJECTION;
}
let raw = match std::fs::read(&args.document) {
Ok(raw) => raw,
Err(e) => {
eprintln!(
"camel compile: cannot read document '{}': {e}",
args.document.display()
);
return EXIT_REJECTION;
}
};
let text = match trailer::normalize_document(&raw) {
Ok(text) => text,
Err(e) => {
eprintln!("camel compile: {e}");
return EXIT_REJECTION;
}
};
let doc_name = args
.document
.file_name()
.map(|name| name.to_string_lossy().to_ascii_lowercase())
.unwrap_or_default();
if doc_name.ends_with(".job.json") {
eprintln!(
"camel compile: unsupported document '{}': the '.job.json' suffix is not a compilable document kind; job documents must be '*.job.yaml' or '*.job.yml'",
args.document.display()
);
return EXIT_REJECTION;
}
let Some(kind) = document_kind(&args.document) else {
eprintln!(
"camel compile: unsupported document '{}': expected a route document (*.yaml, *.yml, *.json) or a job document (*.job.yaml, *.job.yml)",
args.document.display()
);
return EXIT_REJECTION;
};
if let Err(e) = policy::reject_unsupported_assets(&text, kind) {
eprintln!("camel compile: {e}");
return EXIT_REJECTION;
}
let manifest = match manifest::derive(&source_name(&args.document, &cwd), kind, &text) {
Ok(manifest) => manifest,
Err(e) => {
eprintln!("camel compile: {e}");
return EXIT_REJECTION;
}
};
let artifact = Trailer {
kind,
payload: text.into_bytes(),
manifest: manifest.to_canonical_json().into_bytes(),
};
if let Err(e) = write_artifact(&args.output, &trailer::encode(&artifact)) {
eprintln!("camel compile: {e}");
return EXIT_REJECTION;
}
0
}
fn document_kind(path: &Path) -> Option<TrailerKind> {
let name = path.file_name()?.to_string_lossy().to_ascii_lowercase();
if name.ends_with(".job.yaml") || name.ends_with(".job.yml") {
Some(TrailerKind::Job)
} else if name.ends_with(".yaml") || name.ends_with(".yml") || name.ends_with(".json") {
Some(TrailerKind::Route)
} else {
None
}
}
fn source_name(document: &Path, cwd: &Path) -> String {
let rel = if document.is_absolute() {
document.strip_prefix(cwd).unwrap_or(document)
} else {
document
};
let cleaned: PathBuf = rel
.components()
.filter(|c| !matches!(c, std::path::Component::CurDir))
.collect();
cleaned.to_string_lossy().into_owned()
}
fn write_artifact(output: &Path, trailer_bytes: &[u8]) -> Result<(), String> {
let exe = std::env::current_exe()
.map_err(|e| format!("cannot locate the current executable: {e}"))?;
let mut tmp_name = output.as_os_str().to_owned();
tmp_name.push(".tmp");
let tmp = PathBuf::from(tmp_name);
fn discard(tmp: &Path, message: String) -> String {
let _ = std::fs::remove_file(tmp);
message
}
let write = || -> std::io::Result<()> {
let mut out = std::fs::File::create(&tmp)?;
let mut exe_file = std::fs::File::open(&exe)?;
std::io::copy(&mut exe_file, &mut out)?;
out.write_all(trailer_bytes)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
out.set_permissions(std::fs::Permissions::from_mode(0o755))?;
}
out.sync_all()
};
if let Err(e) = write() {
return Err(discard(
&tmp,
format!("cannot write the artifact to '{}': {e}", tmp.display()),
));
}
if let Err(e) = std::fs::rename(&tmp, output) {
return Err(discard(
&tmp,
format!("cannot publish the artifact to '{}': {e}", output.display()),
));
}
Ok(())
}