use cargo_athena::{AthenaConfig, S3Ref, serde_json};
use clap::{Parser, Subcommand};
use std::process::{Command, Stdio, exit};
#[path = "../emulate.rs"]
mod emulate;
#[path = "../submit.rs"]
mod submit;
#[path = "../tarball.rs"]
mod tarball;
#[derive(Parser)]
#[command(bin_name = "cargo")]
enum Cargo {
Athena(Athena),
}
#[derive(clap::Args)]
#[command(version, about, long_about = None)]
struct Athena {
#[arg(short = 'c', long = "config", global = true, value_name = "FILE")]
config: Option<std::path::PathBuf>,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Emit {
#[arg(long)]
package: Option<String>,
#[arg(long)]
bin: Option<String>,
#[arg(long)]
out: Option<String>,
#[arg(long)]
with_workflow: bool,
},
Container {
#[command(subcommand)]
cmd: ContainerCmd,
},
Workflow {
#[command(subcommand)]
cmd: WorkflowCmd,
},
Submit(submit::SubmitArgs),
Build {
#[arg(long)]
package: Option<String>,
#[arg(long)]
bin: Option<String>,
#[arg(long = "target")]
targets: Vec<String>,
#[arg(long)]
print: bool,
},
Publish {
#[arg(long)]
package: Option<String>,
#[arg(long)]
bin: Option<String>,
#[arg(long = "target")]
targets: Vec<String>,
#[arg(long)]
tarball: Option<String>,
#[arg(long)]
print: bool,
},
}
#[derive(Subcommand)]
enum ContainerCmd {
Emulate(emulate::EmulateArgs),
Describe(emulate::DescribeArgs),
Ls(emulate::LsArgs),
}
#[derive(Subcommand)]
enum WorkflowCmd {
Ls(emulate::WorkflowLsArgs),
Describe(emulate::DescribeArgs),
}
fn main() {
let Cargo::Athena(a) = Cargo::parse();
if let Some(cfg) = &a.config {
let abs = std::fs::canonicalize(cfg).unwrap_or_else(|e| {
eprintln!("--config {}: {e}", cfg.display());
exit(2);
});
unsafe { std::env::set_var("ATHENA_CONFIG", &abs) };
}
match a.cmd {
Cmd::Emit {
package,
bin,
out,
with_workflow,
} => emit(
package.as_deref(),
bin.as_deref(),
out.as_deref(),
with_workflow,
),
Cmd::Container { cmd } => match cmd {
ContainerCmd::Emulate(args) => emulate::container_emulate(args),
ContainerCmd::Describe(args) => emulate::describe_print(args),
ContainerCmd::Ls(args) => emulate::container_ls(args),
},
Cmd::Workflow { cmd } => match cmd {
WorkflowCmd::Ls(args) => emulate::workflow_ls(args),
WorkflowCmd::Describe(args) => emulate::describe_print(args),
},
Cmd::Submit(args) => submit::submit(args),
Cmd::Build {
package,
bin,
targets,
print,
} => build(package.as_deref(), bin.as_deref(), &targets, print),
Cmd::Publish {
package,
bin,
targets,
tarball,
print,
} => publish(
package.as_deref(),
bin.as_deref(),
&targets,
tarball.as_deref(),
print,
),
}
}
fn cargo_run(package: Option<&str>, bin: Option<&str>) -> Command {
let mut c = Command::new("cargo");
c.args(["run", "--quiet"]);
if let Some(p) = package {
c.args(["--package", p]);
}
if let Some(b) = bin {
c.args(["--bin", b]);
}
c
}
fn emit(package: Option<&str>, bin: Option<&str>, out: Option<&str>, with_workflow: bool) {
let mut cmd = cargo_run(package, bin);
if with_workflow {
cmd.env("CARGO_ATHENA_WITH_WORKFLOW", "1");
}
let o = cmd.output().expect("failed to run user binary");
if !o.status.success() {
eprint!("{}", String::from_utf8_lossy(&o.stderr));
exit(o.status.code().unwrap_or(1));
}
match out {
Some(path) => {
std::fs::write(path, &o.stdout).expect("write --out file");
eprintln!("wrote {path}");
}
None => print!("{}", String::from_utf8_lossy(&o.stdout)),
}
}
fn package_meta(pkg: Option<&str>) -> (String, String, String) {
let out = Command::new("cargo")
.args(["metadata", "--format-version", "1", "--no-deps"])
.output()
.expect("cargo metadata failed");
let meta: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("parse cargo metadata");
let packages = meta["packages"].as_array().cloned().unwrap_or_default();
let p = match pkg {
Some(name) => packages
.iter()
.find(|p| p["name"] == serde_json::json!(name))
.unwrap_or_else(|| panic!("package {name:?} not found")),
None if packages.len() == 1 => &packages[0],
None => panic!("multiple packages; pass --package"),
};
let name = p["name"].as_str().unwrap().to_string();
let version = p["version"].as_str().unwrap().to_string();
(name.clone(), version, name)
}
fn render_key(template: &str, krate: &str, version: &str, bin: &str) -> String {
template
.replace("{crate}", krate)
.replace("{version}", version)
.replace("{bin}", bin)
}
fn tool_ok(cmd: &str, args: &[&str]) -> bool {
Command::new(cmd)
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn preflight_zig() {
let no_zigbuild = !tool_ok("cargo-zigbuild", &["--version"]);
let no_zig = !tool_ok("zig", &["version"]);
if !no_zigbuild && !no_zig {
return;
}
let mut msg = String::from(
"`cargo athena build` cross-compiles with the Zig toolchain, \
which is missing:\n",
);
if no_zigbuild {
msg.push_str(" - cargo-zigbuild -> cargo install cargo-zigbuild\n");
}
if no_zig {
msg.push_str(
" - zig -> https://ziglang.org/download/ \
(or `pip install ziglang`, or your package manager)\n",
);
}
msg.push_str(
"(the repo's `nix develop` shell provides both. `cargo athena \
emit` and `--print` need neither.)",
);
eprintln!("{msg}");
exit(1);
}
fn build(package: Option<&str>, bin: Option<&str>, cli_targets: &[String], print: bool) {
if let Some((tarball, _s3, dest)) = build_tarball(package, bin, cli_targets, print) {
eprintln!("packaged {tarball} -> {dest}");
eprintln!(
"(`build` packages only — `cargo athena publish` does \
cross-compile + package + upload in one step.)"
);
}
}
fn artifact_s3(cfg: &AthenaConfig, krate: &str, version: &str, bin: &str) -> (S3Ref, String) {
let key = render_key(&cfg.artifact.key, krate, version, bin);
let repo = &cfg.artifact_repository.s3;
let s3 = S3Ref {
endpoint: repo.endpoint.clone(),
bucket: repo.bucket.clone(),
region: repo.region.clone(),
insecure: repo.insecure,
key: key.clone(),
};
let dest = format!("s3://{}/{} (endpoint {})", s3.bucket, key, s3.endpoint);
(s3, dest)
}
fn do_upload(s3: &S3Ref, path: &std::path::Path, dest: &str) {
eprintln!("uploading {} -> {dest}", path.display());
emulate::s3_put(s3, path);
eprintln!("published.");
println!("s3://{}/{}", s3.bucket, s3.key);
}
fn build_tarball(
package: Option<&str>,
bin: Option<&str>,
cli_targets: &[String],
print: bool,
) -> Option<(String, S3Ref, String)> {
let cfg = AthenaConfig::load();
let (krate, version, default_bin) = package_meta(package);
let bin = bin.map(str::to_string).unwrap_or(default_bin);
let targets: Vec<String> = if cli_targets.is_empty() {
cfg.bootstrap.targets.clone()
} else {
cli_targets.to_vec()
};
let (s3, dest) = artifact_s3(&cfg, &krate, &version, &bin);
let tarball = format!("target/athena/{bin}.tar.gz");
eprintln!("crate={krate} version={version} bin={bin}");
eprintln!("targets: {}", targets.join(", "));
for t in &targets {
eprintln!(" cargo zigbuild --release --target {t} -p {krate} --bin {bin} -> app-{t}");
}
eprintln!("tarball: {tarball}");
eprintln!("upload key: {}", s3.key);
eprintln!("destination: {dest}");
if print {
return None;
}
preflight_zig();
std::fs::create_dir_all("target/athena").expect("mkdir target/athena");
let stage = std::path::Path::new("target/athena/stage");
let _ = std::fs::remove_dir_all(stage);
std::fs::create_dir_all(stage).expect("mkdir stage");
for t in &targets {
let status = Command::new("cargo")
.args([
"zigbuild",
"--release",
"--target",
t,
"-p",
&krate,
"--bin",
&bin,
])
.status()
.expect("cargo zigbuild failed to start");
if !status.success() {
eprintln!("zigbuild failed for {t}");
exit(status.code().unwrap_or(1));
}
let from = format!("target/{t}/release/{bin}");
let to = stage.join(format!("app-{t}"));
std::fs::copy(&from, &to)
.unwrap_or_else(|e| panic!("copy {from} -> {}: {e}", to.display()));
}
let entries: Vec<(std::path::PathBuf, String)> = targets
.iter()
.map(|t| (stage.join(format!("app-{t}")), format!("app-{t}")))
.collect();
let refs: Vec<(&std::path::Path, &str)> = entries
.iter()
.map(|(p, n)| (p.as_path(), n.as_str()))
.collect();
if let Err(e) = tarball::create(std::path::Path::new(&tarball), &refs) {
eprintln!("tarball create failed: {e}");
exit(1);
}
Some((tarball, s3, dest))
}
fn publish(
package: Option<&str>,
bin: Option<&str>,
cli_targets: &[String],
tarball_in: Option<&str>,
print: bool,
) {
if let Some(path) = tarball_in {
let cfg = AthenaConfig::load();
let (krate, version, default_bin) = package_meta(package);
let bin = bin.map(str::to_string).unwrap_or(default_bin);
let (s3, dest) = artifact_s3(&cfg, &krate, &version, &bin);
let p = std::path::Path::new(path);
if !p.exists() {
eprintln!("no tarball at {path}");
exit(1);
}
eprintln!("crate={krate} version={version} bin={bin}");
eprintln!("upload key: {}", s3.key);
eprintln!("destination: {dest}");
if print {
eprintln!("(--print) would upload {path}");
return;
}
do_upload(&s3, p, &dest);
return;
}
let Some((tarball, s3, dest)) = build_tarball(package, bin, cli_targets, print) else {
return; };
do_upload(&s3, std::path::Path::new(&tarball), &dest);
}