use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use rpm_spec_analyzer::config::Config;
use rpm_spec_analyzer::profile::{Profile, ResolveOptions, builtin};
mod common;
mod fmt;
mod list;
mod macro_lookup;
mod macros;
mod show;
mod style;
pub use common::CommonOpts;
pub use list::ListOpts;
pub use macro_lookup::MacroOpts;
pub use macros::MacrosOpts;
pub use show::ShowOpts;
pub(super) const DEFAULT_PROFILE: &str = "generic";
#[derive(Debug, Args)]
pub struct Cmd {
#[arg(long, global = true)]
pub config: Option<PathBuf>,
#[command(subcommand)]
pub action: Action,
}
#[derive(Debug, Subcommand)]
pub enum Action {
Show(ShowOpts),
List(ListOpts),
Macros(MacrosOpts),
Macro(MacroOpts),
Common(CommonOpts),
}
impl Cmd {
pub fn run(self, color: crate::app::ColorChoice) -> Result<ExitCode> {
let (config, base_dir) = load_config(self.config.as_deref())?;
let style = style::Style::new(color);
let stdout = std::io::stdout();
let mut out = stdout.lock();
match self.action {
Action::Show(opts) => {
let cli_override = opts.name.as_deref();
let profile = config
.resolve_profile(
&base_dir,
ResolveOptions::with_override(cli_override).with_defines(&opts.defines.raw),
)
.with_context(|| "failed to resolve profile")?;
show::render(&mut out, &profile, opts.full, &style)?;
}
Action::List(opts) => {
return list::render_list(&mut out, &config, &base_dir, opts, &style);
}
Action::Macros(opts) => {
let profile_name = opts.profile.as_deref();
let profile = config
.resolve_profile(
&base_dir,
ResolveOptions::with_override(profile_name).with_defines(&opts.defines.raw),
)
.with_context(|| "failed to resolve profile")?;
let effective_name = active_profile_name(profile_name, &config);
macros::render_macros(&mut out, effective_name, &profile, &opts, &style)?;
}
Action::Macro(opts) => {
return macro_lookup::dispatch_macro(&mut out, &config, &base_dir, opts, &style);
}
Action::Common(opts) => {
return common::dispatch_common(&mut out, &config, &base_dir, opts, &style);
}
}
Ok(ExitCode::SUCCESS)
}
}
pub(super) fn active_profile_name<'a>(
cli_override: Option<&'a str>,
config: &'a Config,
) -> &'a str {
cli_override
.or(config.profile.as_deref())
.unwrap_or(DEFAULT_PROFILE)
}
pub(super) fn all_profile_names(config: &Config) -> Vec<String> {
let mut names: Vec<String> = builtin::names().iter().map(|s| (*s).to_string()).collect();
for key in config.profiles.keys() {
if !names.iter().any(|n| n == key) {
names.push(key.clone());
}
}
names
}
pub(super) fn resolve_many(
config: &Config,
base_dir: &Path,
names: &[String],
defines: &[String],
) -> Result<Vec<(String, Profile)>> {
names
.iter()
.map(|name| {
let p = config
.resolve_profile(
base_dir,
ResolveOptions::with_override(Some(name)).with_defines(defines),
)
.with_context(|| format!("failed to resolve profile `{name}`"))?;
Ok((name.clone(), p))
})
.collect()
}
fn load_config(explicit: Option<&Path>) -> Result<(Config, PathBuf)> {
if let Some(path) = explicit {
let text =
std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
let cfg =
Config::from_toml_str(&text).with_context(|| format!("parsing {}", path.display()))?;
let base = path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
return Ok((cfg, base));
}
let cwd = std::env::current_dir().context("getting current directory")?;
let mut dir = cwd.clone();
loop {
let candidate = dir.join(".rpmspec.toml");
if candidate.is_file() {
tracing::debug!(path = %candidate.display(), "found .rpmspec.toml");
let text = std::fs::read_to_string(&candidate)
.with_context(|| format!("reading {}", candidate.display()))?;
let cfg = Config::from_toml_str(&text)
.with_context(|| format!("parsing {}", candidate.display()))?;
return Ok((cfg, dir));
}
if !dir.pop() {
tracing::debug!(
cwd = %cwd.display(),
"no .rpmspec.toml found while walking up; using Config::default()"
);
return Ok((Config::default(), cwd));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rpm_spec_analyzer::profile::ProfileEntry;
#[test]
fn all_profile_names_builtin_first_then_user_alpha() {
let mut config = Config::default();
config
.profiles
.insert("zzz-user".to_string(), ProfileEntry::default());
config
.profiles
.insert("aaa-user".to_string(), ProfileEntry::default());
config
.profiles
.insert("generic".to_string(), ProfileEntry::default());
let names = all_profile_names(&config);
assert_eq!(names[0], DEFAULT_PROFILE);
assert_eq!(names.iter().filter(|n| *n == DEFAULT_PROFILE).count(), 1);
let aaa_pos = names.iter().position(|n| n == "aaa-user").unwrap();
let zzz_pos = names.iter().position(|n| n == "zzz-user").unwrap();
let last_builtin_pos = names
.iter()
.rposition(|n| builtin::names().contains(&n.as_str()))
.unwrap();
assert!(
aaa_pos > last_builtin_pos,
"user profiles must follow builtins"
);
assert!(aaa_pos < zzz_pos, "user profiles must be alphabetical");
}
}