use cargo_athena::{AthenaConfig, S3Ref, serde_json};
use clap::{Parser, Subcommand};
use std::process::{Command, Stdio, exit};
#[path = "../binsrc.rs"]
mod binsrc;
#[path = "../doctor.rs"]
mod doctor;
#[path = "../emulate.rs"]
mod emulate;
#[path = "../feedback.rs"]
mod feedback;
#[path = "../gitinfo.rs"]
mod gitinfo;
#[path = "../init.rs"]
mod init;
#[path = "../ls.rs"]
mod ls;
#[path = "../pkg.rs"]
mod pkg;
#[path = "../style.rs"]
mod style;
#[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,
after_help = "Typical flow: init -> publish -> submit"
)]
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 {
Init(init::InitArgs),
Ls(ls::LsArgs),
Describe(emulate::DescribeArgs),
Emit {
#[command(flatten)]
bin: binsrc::BinSel,
#[arg(long = "dev-tag", value_name = "SLOT", num_args = 0..=1)]
dev_tag: Option<Option<String>>,
#[arg(long)]
out: Option<String>,
#[arg(long)]
with_workflow: bool,
},
Emulate(emulate::EmulateArgs),
Build {
#[command(flatten)]
pkg: pkg::PkgSel,
#[arg(long = "target")]
targets: Vec<String>,
#[command(flatten)]
gate: GateArgs,
#[arg(long)]
print: bool,
},
Publish {
#[command(flatten)]
pkg: pkg::PkgSel,
#[arg(long = "target")]
targets: Vec<String>,
#[arg(long)]
tarball: Option<String>,
#[command(flatten)]
gate: GateArgs,
#[arg(long)]
print: bool,
},
Submit(submit::SubmitArgs),
Prune(submit::PruneArgs),
Doctor(doctor::DoctorArgs),
}
#[derive(clap::Args)]
struct GateArgs {
#[arg(long = "dev-tag", value_name = "TAG", num_args = 0..=1)]
dev_tag: Option<Option<String>>,
#[arg(long = "allow-dirty")]
allow_dirty: bool,
#[arg(long)]
yes: bool,
}
fn global_config_dir() -> Option<std::path::PathBuf> {
use etcetera::base_strategy::{BaseStrategy, Xdg};
Xdg::new()
.ok()
.map(|xdg| xdg.config_dir().join("cargo-athena"))
}
fn main() {
let Cargo::Athena(a) = Cargo::parse();
let env_cfg = std::env::var_os("ATHENA_CONFIG").map(std::path::PathBuf::from);
let cwd = std::env::current_dir().unwrap_or_default();
let xdg = global_config_dir();
if let Some(path) = AthenaConfig::resolve_config_path(
a.config.as_deref(),
env_cfg.as_deref(),
&cwd,
xdg.as_deref(),
) {
let abs = std::fs::canonicalize(&path).unwrap_or_else(|e| {
if a.config.is_some() {
eprintln!("--config {}: {e}", path.display());
exit(2);
}
path.clone()
});
unsafe { std::env::set_var("ATHENA_CONFIG", &abs) };
}
match a.cmd {
Cmd::Init(args) => init::init(args),
Cmd::Doctor(args) => doctor::doctor(args),
Cmd::Emit {
bin,
dev_tag,
out,
with_workflow,
} => {
bin.apply_dev_tag(dev_tag);
emit(&bin.resolve(), out.as_deref(), with_workflow);
}
Cmd::Ls(args) => ls::ls(args),
Cmd::Describe(args) => emulate::describe(args),
Cmd::Emulate(args) => emulate::emulate(args),
Cmd::Submit(args) => submit::submit(args),
Cmd::Prune(args) => submit::prune(args),
Cmd::Build {
pkg,
targets,
gate,
print,
} => {
let (package, bin) = pkg.resolve();
build(package.as_deref(), bin.as_deref(), &targets, gate, print);
}
Cmd::Publish {
pkg,
targets,
tarball,
gate,
print,
} => {
let (package, bin) = pkg.resolve();
publish(
package.as_deref(),
bin.as_deref(),
&targets,
tarball.as_deref(),
gate,
print,
);
}
}
}
fn emit(src: &binsrc::BinarySource, out: Option<&str>, with_workflow: bool) {
src.probe();
let mut cmd = src.command();
if with_workflow {
cmd.env("CARGO_ATHENA_WITH_WORKFLOW", "1");
}
cmd.stdout(Stdio::piped()).stderr(Stdio::inherit());
let o = cmd
.output()
.unwrap_or_else(|e| die(&format!("failed to run the workflow binary: {e}")));
if !o.status.success() {
exit(o.status.code().unwrap_or(1));
}
match out {
Some(path) => {
std::fs::write(path, &o.stdout).unwrap_or_else(|e| die(&format!("write {path}: {e}")));
eprintln!("wrote {path}");
}
None => std::io::Write::write_all(&mut std::io::stdout(), &o.stdout).expect("write stdout"),
}
}
fn package_meta(pkg: Option<&str>) -> (String, String, String) {
let out = Command::new("cargo")
.args(["metadata", "--format-version", "1", "--no-deps"])
.output()
.unwrap_or_else(|e| die(&format!("failed to run `cargo metadata`: {e}")));
if !out.status.success() {
eprint!("{}", String::from_utf8_lossy(&out.stderr));
die("`cargo metadata` failed — run from inside the workflow crate");
}
let meta: serde_json::Value = serde_json::from_slice(&out.stdout)
.unwrap_or_else(|e| die(&format!("parse `cargo metadata` output: {e}")));
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(|| die(&format!("package {name:?} not found in this workspace"))),
None if packages.len() == 1 => &packages[0],
None => die("multiple packages in this workspace; pass --package <name>"),
};
let name = p["name"].as_str().unwrap().to_string();
let version = p["version"].as_str().unwrap().to_string();
(name.clone(), version, name)
}
fn die(m: &str) -> ! {
eprintln!("cargo athena: {m}");
exit(2);
}
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],
gate: GateArgs,
print: bool,
) {
if let Some((tarball, _s3, dest)) = build_tarball(package, bin, cli_targets, gate, 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, tag: &str, bin: &str) -> (S3Ref, String) {
let key = cargo_athena::api::munge::binary_key(krate, tag, bin);
let s3 = S3Ref::from_repo(&cfg.artifact_repository.s3, key);
let dest = format!("s3://{}/{} (endpoint {})", s3.bucket, s3.key, s3.endpoint);
(s3, dest)
}
fn do_upload(s3: &S3Ref, path: &std::path::Path, dest: &str) {
let st = feedback::step(format!("Uploading {} -> {dest}", path.display()));
emulate::s3_put(s3, path);
st.finish();
println!("s3://{}/{}", s3.bucket, s3.key);
}
fn build_tarball(
package: Option<&str>,
bin: Option<&str>,
cli_targets: &[String],
gate: GateArgs,
print: bool,
) -> Option<(String, S3Ref, String)> {
let cfg = AthenaConfig::try_load().unwrap_or_else(|e| die(&e));
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 bt = gitinfo::resolve(&version, gate.dev_tag, gate.allow_dirty, gate.yes, !print);
let (s3, dest) = artifact_s3(&cfg, &krate, &bt.tag, &bin);
let tarball = format!("target/athena/{bin}.tar.gz");
eprintln!("crate={krate} version={version} bin={bin}");
eprintln!("tag={} channel={}", bt.tag, bt.channel);
eprintln!("targets: {}", targets.join(", "));
eprintln!("destination: {dest}");
if print {
return None;
}
unsafe {
std::env::set_var("ATHENA_VERSION_TAG", &bt.tag);
if let Some(c) = &bt.commit {
std::env::set_var("ATHENA_GIT_COMMIT", c);
}
if bt.dirty {
std::env::set_var("ATHENA_GIT_DIRTY", "true");
}
}
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 st = feedback::step(format!("Cross-compiling for {t}"));
let status = Command::new("cargo")
.args([
"zigbuild",
"--release",
"--target",
t,
"-p",
&krate,
"--bin",
&bin,
])
.status()
.expect("cargo zigbuild failed to start");
if !status.success() {
drop(st);
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()));
st.finish();
}
let st = feedback::step(format!("Packaging {tarball}"));
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) {
drop(st);
eprintln!("tarball create failed: {e}");
exit(1);
}
st.finish();
Some((tarball, s3, dest))
}
fn publish(
package: Option<&str>,
bin: Option<&str>,
cli_targets: &[String],
tarball_in: Option<&str>,
gate: GateArgs,
print: bool,
) {
if let Some(path) = tarball_in {
let cfg = AthenaConfig::try_load().unwrap_or_else(|e| die(&e));
let (krate, version, default_bin) = package_meta(package);
let bin = bin.map(str::to_string).unwrap_or(default_bin);
let dev_tag_given = gate.dev_tag.is_some();
let bt = gitinfo::resolve(&version, gate.dev_tag, gate.allow_dirty, gate.yes, false);
let explicit_tag = std::env::var_os("ATHENA_VERSION_TAG").is_some_and(|v| !v.is_empty());
if bt.channel == "dev" && !dev_tag_given && !explicit_tag {
eprintln!(
"error: `publish --tarball` can't infer the prebuilt binary's \
dev tag from the current tree (it would guess `{}` from the \
working commit).\n Pass the tag the tarball was built with: \
`--dev-tag <slot>`, or set `ATHENA_VERSION_TAG=<tag>`.",
bt.tag
);
exit(2);
}
let (s3, dest) = artifact_s3(&cfg, &krate, &bt.tag, &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!("tag={} channel={}", bt.tag, bt.channel);
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, gate, print) else {
return; };
do_upload(&s3, std::path::Path::new(&tarball), &dest);
}