use crate::config::{Config, Tree};
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use std::path::PathBuf;
pub mod buckify;
pub mod config_check;
pub mod debug;
pub mod fixups;
pub mod init;
pub mod stub;
pub mod vendor;
#[derive(Parser, Debug)]
#[command(
name = "muntjac",
version,
about = "Translate uv.lock into Buck2 build rules",
long_about = None,
)]
pub struct Cli {
#[command(flatten)]
pub globals: Globals,
#[command(subcommand)]
pub command: Command,
}
#[derive(Args, Debug, Clone)]
pub struct Globals {
#[arg(short = 'C', long = "cd", global = true, value_name = "PATH")]
pub cd: Option<PathBuf>,
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long, global = true)]
pub no_network: bool,
#[arg(long, global = true)]
pub frozen: bool,
#[arg(long, global = true, value_name = "NAME")]
pub tree: Option<String>,
}
impl Globals {
pub fn workdir(&self) -> std::io::Result<PathBuf> {
match &self.cd {
Some(p) => std::fs::canonicalize(p),
None => std::env::current_dir(),
}
}
}
pub fn resolve_trees<'a>(config: &'a Config, tree_filter: Option<&str>) -> Result<Vec<&'a Tree>> {
match tree_filter {
None => Ok(config.trees.iter().collect()),
Some(name) => match config.trees.iter().find(|t| t.name == name) {
Some(t) => Ok(vec![t]),
None => {
let available: Vec<&str> = config.trees.iter().map(|t| t.name.as_str()).collect();
anyhow::bail!(
"tree `{}` not found in muntjac.toml; available: {}",
name,
available.join(", ")
)
}
},
}
}
#[derive(Subcommand, Debug)]
pub enum Command {
Init(init::InitArgs),
Config {
#[command(subcommand)]
op: ConfigOp,
},
#[command(hide = true)]
Debug {
#[command(subcommand)]
op: Option<debug::DebugOp>,
},
Vendor,
Buckify,
Audit,
Fixups {
#[command(subcommand)]
op: fixups::FixupsOp,
},
Unused,
}
#[derive(Subcommand, Debug)]
pub enum ConfigOp {
Check(config_check::ConfigCheckArgs),
}
pub fn run(cli: Cli) -> Result<()> {
match cli.command {
Command::Init(args) => init::run(args, &cli.globals),
Command::Config {
op: ConfigOp::Check(args),
} => config_check::run(args, &cli.globals),
Command::Debug { op } => debug::run(op, &cli.globals),
Command::Vendor => vendor::run(&cli.globals),
Command::Buckify => buckify::run(&cli.globals),
Command::Audit => stub::run("audit", "S10"),
Command::Fixups { op } => fixups::run(op, &cli.globals),
Command::Unused => stub::run("unused", "S10"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn cfg() -> Config {
Config::from_str(
r#"
[platforms]
macos-arm64 = { target = "aarch64-apple-darwin", macos_min = "11.0" }
[tree.modern]
manifest_path = "m/pyproject.toml"
third_party_dir = "tp/modern"
python_versions = ["3.12"]
[tree.legacy]
manifest_path = "l/pyproject.toml"
third_party_dir = "tp/legacy"
python_versions = ["3.12"]
"#,
)
.unwrap()
}
#[test]
fn resolve_trees_none_returns_all() {
let c = cfg();
assert_eq!(resolve_trees(&c, None).unwrap().len(), 2);
}
#[test]
fn resolve_trees_filters_by_name() {
let c = cfg();
let got = resolve_trees(&c, Some("modern")).unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].name, "modern");
}
#[test]
fn resolve_trees_unknown_errors() {
let c = cfg();
let err = resolve_trees(&c, Some("ghost")).unwrap_err().to_string();
assert!(err.contains("ghost"));
assert!(err.contains("modern"));
assert!(err.contains("legacy"));
}
}