use std::io::Write;
use std::path::Path;
use std::process::ExitCode;
use anyhow::{Context, Result};
use clap::Args;
use rpm_spec_analyzer::config::Config;
use rpm_spec_analyzer::profile::{MacroEntry, MacroValue, Profile};
use super::fmt::{MAX_PROFILE_NAME_WIDTH, compact_value, format_opts, format_provenance};
use super::style::Style;
use super::{all_profile_names, resolve_many};
#[derive(Debug, Args)]
#[command(after_help = "\
Modes (chosen by number of PROFILES arguments):
macro NAME — table of NAME across every available profile
(always exit 0)
macro NAME P — single profile, compact value with multiline body
expanded (exit 2 if NAME is undefined in P)
macro NAME P1 P2 [P3 …] — comparison table across listed profiles
(always exit 0)")]
pub struct MacroOpts {
pub name: String,
pub profiles: Vec<String>,
#[command(flatten)]
pub defines: crate::app::MacroDefinesArg,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MacroLookup {
Found,
Undefined,
}
pub(super) fn dispatch_macro(
out: &mut impl Write,
config: &Config,
base_dir: &Path,
opts: MacroOpts,
style: &Style,
) -> Result<ExitCode> {
let names: Vec<String> = if opts.profiles.is_empty() {
all_profile_names(config)
} else {
opts.profiles
};
if names.len() == 1 {
let single = &names[0];
let profile = config
.resolve_profile(
base_dir,
rpm_spec_analyzer::profile::ResolveOptions::with_override(Some(single))
.with_defines(&opts.defines.raw),
)
.with_context(|| "failed to resolve profile")?;
let shadowed = if opts.defines.raw.is_empty() {
None
} else {
match config.resolve_profile(
base_dir,
rpm_spec_analyzer::profile::ResolveOptions::with_override(Some(single)),
) {
Ok(baseline) => {
let winning = profile.macros.get(&opts.name);
baseline
.macros
.get(&opts.name)
.cloned()
.filter(|prev| winning.map(|w| !w.is_equivalent(prev)).unwrap_or(false))
}
Err(e) => {
tracing::debug!(
profile = %single,
macro_name = %opts.name,
error = %e,
"baseline resolve failed; skipping shadow line"
);
None
}
}
};
match render_macro(out, single, &profile, &opts.name, shadowed.as_ref(), style)? {
MacroLookup::Found => Ok(ExitCode::SUCCESS),
MacroLookup::Undefined => Ok(ExitCode::from(2)),
}
} else {
let resolved = resolve_many(config, base_dir, &names, &opts.defines.raw)?;
render_macro_table(out, &opts.name, &resolved, style)?;
Ok(ExitCode::SUCCESS)
}
}
fn render_macro(
out: &mut impl Write,
profile_name: &str,
profile: &Profile,
macro_name: &str,
shadowed: Option<&MacroEntry>,
style: &Style,
) -> Result<MacroLookup> {
let Some(entry) = profile.macros.get(macro_name) else {
eprintln!("error: macro `{macro_name}` is not defined in profile `{profile_name}`");
return Ok(MacroLookup::Undefined);
};
write_macro_line(out, macro_name, entry, "", style)?;
if let Some(prev) = shadowed {
write_macro_line(out, macro_name, prev, " shadows: ", style)?;
}
Ok(MacroLookup::Found)
}
fn write_macro_line(
out: &mut impl Write,
macro_name: &str,
entry: &MacroEntry,
prefix: &str,
style: &Style,
) -> Result<()> {
let opts_str = format_opts(entry.opts.as_deref());
let prov = style.dim(&format!("[{}]", format_provenance(&entry.provenance)));
let name = style.bold_cyan(&format!("{macro_name}{opts_str}"));
match &entry.value {
MacroValue::Literal(s) => writeln!(out, "{prefix}{name} = {s} {prov}")?,
MacroValue::Builtin => {
writeln!(out, "{prefix}{name} = {} {prov}", style.dim("<builtin>"))?
}
MacroValue::Raw { body, multiline } => {
if *multiline {
writeln!(out, "{prefix}{name} = {prov}")?;
for line in body.lines() {
writeln!(out, " {line}")?;
}
} else {
writeln!(out, "{prefix}{name} = {body} {prov}")?;
}
}
}
Ok(())
}
fn render_macro_table(
out: &mut impl Write,
macro_name: &str,
resolved: &[(String, Profile)],
style: &Style,
) -> Result<()> {
writeln!(
out,
"{} {} {}",
style.bold("# Macro"),
style.bold_cyan(&format!("`{macro_name}`")),
style.bold(&format!("across {} profile(s)", resolved.len())),
)?;
writeln!(out)?;
let name_width = resolved
.iter()
.map(|(n, _)| n.len())
.max()
.unwrap_or(0)
.min(MAX_PROFILE_NAME_WIDTH);
for (profile_name, profile) in resolved {
let padded_name = format!("{profile_name:<name_width$}");
let name_styled = style.bold(&padded_name);
match profile.macros.get(macro_name) {
Some(entry) => {
let val = compact_value(&entry.value);
let prov = style.dim(&format!("[{}]", format_provenance(&entry.provenance)));
writeln!(out, " {name_styled} = {val} {prov}")?;
}
None => {
writeln!(out, " {name_styled} = {}", style.dim_red("(undefined)"))?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rpm_spec_analyzer::profile::{MacroEntry, Provenance};
#[test]
fn render_macro_returns_undefined_for_unknown() {
let profile = Profile::default();
let mut buf = Vec::new();
let style = Style::plain();
let result =
render_macro(&mut buf, "generic", &profile, "no-such-macro", None, &style).unwrap();
assert_eq!(result, MacroLookup::Undefined);
assert!(buf.is_empty());
}
#[test]
fn render_macro_returns_found_and_writes_for_known() {
let mut profile = Profile::default();
profile.macros.insert(
"dist",
MacroEntry::literal(
".el9",
Provenance::Showrc {
level: -13,
path: None,
},
),
);
let mut buf = Vec::new();
let style = Style::plain();
let result =
render_macro(&mut buf, "rhel-9-x86_64", &profile, "dist", None, &style).unwrap();
assert_eq!(result, MacroLookup::Found);
let out = String::from_utf8(buf).unwrap();
assert!(out.starts_with("dist = .el9"));
assert!(out.contains("[showrc:-13]"));
}
#[test]
fn render_macro_with_shadowed_entry_prints_second_line() {
let mut profile = Profile::default();
profile
.macros
.insert("dist", MacroEntry::literal(".fc40", Provenance::Override));
let shadowed = MacroEntry::literal(
".el9",
Provenance::Showrc {
level: -13,
path: None,
},
);
let mut buf = Vec::new();
let style = Style::plain();
let result = render_macro(
&mut buf,
"rhel-9-x86_64",
&profile,
"dist",
Some(&shadowed),
&style,
)
.unwrap();
assert_eq!(result, MacroLookup::Found);
let out = String::from_utf8(buf).unwrap();
assert!(
out.contains("dist = .fc40 [override]"),
"missing winning line: {out}"
);
assert!(
out.contains(" shadows: dist = .el9 [showrc:-13]"),
"missing shadow line: {out}"
);
}
}