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#[derive(FromArgs, PartialEq, Debug)]
47#[argh(subcommand, name = "edit-config")]
48pub struct EditConfig {
49 #[argh(option, short = 'e')]
51 pub editor: Option<String>,
52}
53
54#[derive(FromArgs, PartialEq, Debug)]
56#[argh(subcommand)]
57pub enum Subcommand {
58 EditConfig(EditConfig),
60}
61
62#[derive(FromArgs)]
63struct FakeArgs {
65 #[argh(subcommand)]
66 pub sub: Option<Subcommand>,
67
68 #[argh(positional)]
70 input: Option<Utf8PathBuf>,
71
72 #[argh(option, short = 'o')]
74 output: Option<Utf8PathBuf>,
75
76 #[argh(option)]
78 from: Option<String>,
79
80 #[argh(option)]
82 to: Option<String>,
83
84 #[argh(option, short = 'm', default = "Mode::Run")]
86 mode: Mode,
87
88 #[argh(option)]
90 dir: Option<Utf8PathBuf>,
91
92 #[argh(switch)]
94 keep: Option<bool>,
95
96 #[argh(option, short = 's')]
98 set: Vec<String>,
99
100 #[argh(option)]
102 through: Vec<String>,
103
104 #[argh(switch, short = 'v')]
106 verbose: Option<bool>,
107
108 #[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 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 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 env_logger::Builder::new()
175 .format_timestamp(None)
176 .filter_level(args.log_level)
177 .target(env_logger::Target::Stderr)
178 .init();
179
180 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 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 let mut run = Run::new(driver, plan);
207
208 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 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 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}