use cargo_athena::{ATHENA_PROBE_KIND, ATHENA_PROTOCOL, AthenaConfig, ProbeInfo, serde_json};
use std::path::PathBuf;
use std::process::{Command, Stdio, exit};
#[derive(clap::Args)]
pub struct BinSel {
#[arg(value_name = "BINARY")]
binary: Option<String>,
#[arg(long = "manifest-path", value_name = "PATH", conflicts_with = "binary")]
manifest_path: Option<PathBuf>,
#[arg(short = 'p', long, conflicts_with = "binary")]
package: Option<String>,
#[arg(long = "bin", conflicts_with = "binary")]
bin: Option<String>,
}
pub enum BinarySource {
Exe(String),
Cargo {
manifest_path: Option<PathBuf>,
package: Option<String>,
bin: Option<String>,
},
}
impl BinSel {
pub(crate) fn apply_dev_tag(&self, dev_tag: Option<Option<String>>) {
if dev_tag.is_none() {
return;
}
if self.binary.is_some() {
die(
"--dev-tag only applies when building from source; the prebuilt \
binary carries its own sealed tag (omit the binary, or drop --dev-tag).",
);
}
crate::gitinfo::export_dev_tag(dev_tag);
}
pub(crate) fn resolve(&self) -> BinarySource {
if let Some(b) = &self.binary {
return BinarySource::Exe(b.clone());
}
crate::gitinfo::export_source_build_tag();
let d = AthenaConfig::try_load()
.unwrap_or_else(|e| die(&e))
.defaults;
BinarySource::Cargo {
manifest_path: self.manifest_path.clone(),
package: self.package.clone().or(d.package),
bin: self.bin.clone().or(d.bin),
}
}
}
impl BinarySource {
pub(crate) fn command(&self) -> Command {
match self {
BinarySource::Exe(p) => Command::new(p),
BinarySource::Cargo {
manifest_path,
package,
bin,
} => {
let mut c = Command::new("cargo");
c.arg("run");
if let Some(m) = manifest_path {
let mp = if m.is_dir() {
m.join("Cargo.toml")
} else {
m.clone()
};
c.arg("--manifest-path").arg(mp);
}
if let Some(p) = package {
c.args(["--package", p]);
}
if let Some(b) = bin {
c.args(["--bin", b]);
}
c
}
}
}
fn label(&self) -> String {
match self {
BinarySource::Exe(p) => p.clone(),
BinarySource::Cargo { .. } => "the workflow crate".to_string(),
}
}
pub(crate) fn cargo_pkg_bin(&self) -> Option<(Option<String>, Option<String>)> {
match self {
BinarySource::Exe(_) => None,
BinarySource::Cargo { package, bin, .. } => Some((package.clone(), bin.clone())),
}
}
pub(crate) fn probe(&self) -> ProbeInfo {
let out = self
.command()
.env("CARGO_ATHENA_PROBE", "1")
.stderr(Stdio::inherit())
.stdout(Stdio::piped())
.output()
.unwrap_or_else(|e| die(&format!("failed to run {}: {e}", self.label())));
if !out.status.success() {
die(&format!(
"{} did not respond to a cargo-athena probe (exit {:?}). Is it a binary built \
with cargo-athena (its `main` calls `cargo_athena::entrypoint!(Root)`)?",
self.label(),
out.status.code()
));
}
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap_or_else(|_| {
die(&format!(
"{} does not look like a cargo-athena binary (unrecognized probe response).",
self.label()
))
});
if v.get("kind").and_then(|k| k.as_str()) != Some(ATHENA_PROBE_KIND) {
die(&format!("{} is not a cargo-athena binary.", self.label()));
}
let proto = v
.get("athena_protocol")
.and_then(|p| p.as_u64())
.unwrap_or(0) as u32;
if proto != ATHENA_PROTOCOL {
let hint = if proto > ATHENA_PROTOCOL {
"upgrade the CLI (`cargo install cargo-athena`)"
} else {
"rebuild the workflow binary against this cargo-athena (its library + the CLI must match), or use a matching CLI"
};
die(&format!(
"version mismatch: {} speaks cargo-athena probe protocol {proto}, this CLI speaks {ATHENA_PROTOCOL} ({hint}).",
self.label(),
));
}
serde_json::from_value(v).unwrap_or_else(|_| {
die(&format!(
"{} was built with a different cargo-athena than this CLI (its \
probe is missing fields the CLI expects). Rebuild the workflow \
binary against the same cargo-athena — its library dependency \
and the `cargo athena` CLI must be the same version.",
self.label()
))
})
}
pub(crate) fn run_mode(&self, env: &str, val: &str, what: &str) -> Vec<u8> {
let out = self
.command()
.env(env, val)
.stderr(Stdio::inherit())
.stdout(Stdio::piped())
.output()
.unwrap_or_else(|e| die(&format!("failed to run {}: {e}", self.label())));
if !out.status.success() || out.stdout.is_empty() {
die(&format!("could not get {what} from {}", self.label()));
}
out.stdout
}
}
fn die(m: &str) -> ! {
eprintln!("cargo athena: {m}");
exit(2);
}