fud_core/
cli.rs

1use crate::config;
2use crate::exec::{Driver, Request, StateRef};
3use crate::run::Run;
4use anyhow::{anyhow, bail};
5use argh::FromArgs;
6use camino::{Utf8Path, Utf8PathBuf};
7use std::fmt::Display;
8use std::str::FromStr;
9
10enum Mode {
11    EmitNinja,
12    ShowPlan,
13    ShowDot,
14    Generate,
15    Run,
16}
17
18impl FromStr for Mode {
19    type Err = String;
20
21    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
22        match s {
23            "emit" => Ok(Mode::EmitNinja),
24            "plan" => Ok(Mode::ShowPlan),
25            "gen" => Ok(Mode::Generate),
26            "run" => Ok(Mode::Run),
27            "dot" => Ok(Mode::ShowDot),
28            _ => Err("unknown mode".to_string()),
29        }
30    }
31}
32
33impl Display for Mode {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            Mode::EmitNinja => write!(f, "emit"),
37            Mode::ShowPlan => write!(f, "plan"),
38            Mode::Generate => write!(f, "gen"),
39            Mode::Run => write!(f, "run"),
40            Mode::ShowDot => write!(f, "dot"),
41        }
42    }
43}
44
45/// edit the configuration file
46#[derive(FromArgs, PartialEq, Debug)]
47#[argh(subcommand, name = "edit-config")]
48pub struct EditConfig {
49    /// the editor to use
50    #[argh(option, short = 'e')]
51    pub editor: Option<String>,
52}
53
54/// supported subcommands
55#[derive(FromArgs, PartialEq, Debug)]
56#[argh(subcommand)]
57pub enum Subcommand {
58    /// edit the configuration file
59    EditConfig(EditConfig),
60}
61
62#[derive(FromArgs)]
63/// A generic compiler driver.
64struct FakeArgs {
65    #[argh(subcommand)]
66    pub sub: Option<Subcommand>,
67
68    /// the input file
69    #[argh(positional)]
70    input: Option<Utf8PathBuf>,
71
72    /// the output file
73    #[argh(option, short = 'o')]
74    output: Option<Utf8PathBuf>,
75
76    /// the state to start from
77    #[argh(option)]
78    from: Option<String>,
79
80    /// the state to produce
81    #[argh(option)]
82    to: Option<String>,
83
84    /// execution mode (run, plan, emit, gen, dot)
85    #[argh(option, short = 'm', default = "Mode::Run")]
86    mode: Mode,
87
88    /// working directory for the build
89    #[argh(option)]
90    dir: Option<Utf8PathBuf>,
91
92    /// in run mode, keep the temporary directory
93    #[argh(switch)]
94    keep: Option<bool>,
95
96    /// set a configuration variable (key=value)
97    #[argh(option, short = 's')]
98    set: Vec<String>,
99
100    /// route the conversion through a specific operation
101    #[argh(option)]
102    through: Vec<String>,
103
104    /// verbose ouput
105    #[argh(switch, short = 'v')]
106    verbose: Option<bool>,
107
108    /// log level for debugging fud internal
109    #[argh(option, long = "log", default = "log::LevelFilter::Warn")]
110    pub log_level: log::LevelFilter,
111}
112
113fn from_state(driver: &Driver, args: &FakeArgs) -> anyhow::Result<StateRef> {
114    match &args.from {
115        Some(name) => driver
116            .get_state(name)
117            .ok_or(anyhow!("unknown --from state")),
118        None => match args.input {
119            Some(ref input) => driver
120                .guess_state(input)
121                .ok_or(anyhow!("could not infer input state")),
122            None => bail!("specify an input file or use --from"),
123        },
124    }
125}
126
127fn to_state(driver: &Driver, args: &FakeArgs) -> anyhow::Result<StateRef> {
128    match &args.to {
129        Some(name) => {
130            driver.get_state(name).ok_or(anyhow!("unknown --to state"))
131        }
132        None => match &args.output {
133            Some(out) => driver
134                .guess_state(out)
135                .ok_or(anyhow!("could not infer output state")),
136            None => Err(anyhow!("specify an output file or use --to")),
137        },
138    }
139}
140
141fn get_request(driver: &Driver, args: &FakeArgs) -> anyhow::Result<Request> {
142    // The default working directory (if not specified) depends on the mode.
143    let default_workdir = driver.default_workdir();
144    let workdir = args.dir.as_deref().unwrap_or_else(|| match args.mode {
145        Mode::Generate | Mode::Run => default_workdir.as_ref(),
146        _ => Utf8Path::new("."),
147    });
148
149    // Find all the operations to route through.
150    let through: Result<Vec<_>, _> = args
151        .through
152        .iter()
153        .map(|s| {
154            driver
155                .get_op(s)
156                .ok_or(anyhow!("unknown --through op {}", s))
157        })
158        .collect();
159
160    Ok(Request {
161        start_file: args.input.clone(),
162        start_state: from_state(driver, args)?,
163        end_file: args.output.clone(),
164        end_state: to_state(driver, args)?,
165        through: through?,
166        workdir: workdir.into(),
167    })
168}
169
170pub fn cli(driver: &Driver) -> anyhow::Result<()> {
171    let args: FakeArgs = argh::from_env();
172
173    // enable tracing
174    env_logger::Builder::new()
175        .format_timestamp(None)
176        .filter_level(args.log_level)
177        .target(env_logger::Target::Stderr)
178        .init();
179
180    // edit the configuration file
181    if let Some(Subcommand::EditConfig(EditConfig { editor })) = args.sub {
182        let editor =
183            if let Some(e) = editor.or_else(|| std::env::var("EDITOR").ok()) {
184                e
185            } else {
186                bail!("$EDITOR not specified. Use -e")
187            };
188        let config_path = config::config_path(&driver.name);
189        log::info!("Editing config at {}", config_path.display());
190        let status = std::process::Command::new(editor)
191            .arg(config_path)
192            .status()
193            .expect("failed to execute editor");
194        if !status.success() {
195            bail!("editor exited with status {}", status);
196        }
197        return Ok(());
198    }
199
200    // Make a plan.
201    let req = get_request(driver, &args)?;
202    let workdir = req.workdir.clone();
203    let plan = driver.plan(req).ok_or(anyhow!("could not find path"))?;
204
205    // Configure.
206    let mut run = Run::new(driver, plan);
207
208    // Override some global config options.
209    if let Some(keep) = args.keep {
210        run.global_config.keep_build_dir = keep;
211    }
212    if let Some(verbose) = args.verbose {
213        run.global_config.verbose = verbose;
214    }
215
216    // Use `--set` arguments to override configuration values.
217    for set in args.set {
218        let mut parts = set.splitn(2, '=');
219        let key = parts.next().unwrap();
220        let value = parts
221            .next()
222            .ok_or(anyhow!("--set arguments must be in key=value form"))?;
223        let dict = figment::util::nest(key, value.into());
224        run.config_data = run
225            .config_data
226            .merge(figment::providers::Serialized::defaults(dict));
227    }
228
229    // Execute.
230    match args.mode {
231        Mode::ShowPlan => run.show(),
232        Mode::ShowDot => run.show_dot(),
233        Mode::EmitNinja => run.emit_to_stdout()?,
234        Mode::Generate => run.emit_to_dir(&workdir)?,
235        Mode::Run => run.emit_and_run(&workdir)?,
236    }
237
238    Ok(())
239}