use std::io::Write as _;
use std::path::{Path, PathBuf};
use clap::Args;
use crate::compile::manifest;
use crate::compile::policy;
use crate::compile::sources::{self, SourceSelection};
use crate::compile::store::VirtualDocumentStore;
use crate::compile::trailer::{self, TrailerKind, TrailerV2};
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>,
#[arg(long, value_name = "CONFIG")]
pub config: Option<PathBuf>,
#[arg(long = "profile", value_name = "NAME")]
pub profile: Vec<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 v2 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 v2 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 v2 requires a clean compile environment; CAMEL_* variable(s) present: {}",
overrides.join(", ")
);
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;
};
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 args.config.is_none() && cwd.join("Camel.toml").is_file() {
eprintln!(
"camel compile v2 rejects a Camel.toml in the compile working directory ('{}'); pass --config <Camel.toml> to embed one explicitly",
cwd.display()
);
return EXIT_REJECTION;
}
let selection = SourceSelection {
config_path: args.config.clone(),
profiles: args.profile.clone(),
};
let sources = match sources::resolve(&args.document, kind, &selection) {
Ok(sources) => sources,
Err(e) => {
eprintln!("camel compile: {e}");
return EXIT_REJECTION;
}
};
let entry_document = sources
.route_documents
.first()
.expect("the resolver always leads the plan with the entry document"); if kind == TrailerKind::Job
&& let Err(e) =
crate::commands::job::validate_job_declarations_for_compile(&entry_document.1)
{
eprintln!("camel compile: {e}");
return EXIT_REJECTION;
}
if let Err(e) = policy::reject_entry_document_assets(&entry_document.1, kind) {
eprintln!("camel compile: {e}");
return EXIT_REJECTION;
}
for (path, doc_text) in sources.route_documents.iter().skip(1) {
if let Err(e) = policy::reject_unsupported_assets(doc_text, TrailerKind::Route) {
eprintln!("camel compile: embedded document '{path}': {e}");
return EXIT_REJECTION;
}
}
let store = match VirtualDocumentStore::build(
&sources.entry_point,
&sources.documents,
&sources.config_references,
&sources.source_plan,
) {
Ok(store) => store,
Err(e) => {
eprintln!("camel compile: invalid source set: {e}");
return EXIT_REJECTION;
}
};
let operational = match manifest::derive_for_store(&store, kind, &sources.route_documents) {
Ok(operational) => operational,
Err(e) => {
eprintln!("camel compile: {e}");
return EXIT_REJECTION;
}
};
let index_bytes = match store.index.encode_canonical() {
Ok(bytes) => bytes,
Err(e) => {
eprintln!("camel compile: cannot encode the store index: {e}");
return EXIT_REJECTION;
}
};
let artifact = TrailerV2 {
kind,
content: store.content,
index: index_bytes,
manifest: operational.to_canonical_json().into_bytes(),
};
if let Err(e) = write_artifact(&args.output, &trailer::encode_v2(&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 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(())
}