flowey_cli/cli/
regen.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use anyhow::Context;
5use std::collections::BTreeMap;
6use std::path::Path;
7use std::path::PathBuf;
8
9/// Regenerate all pipelines defined in the repo's root `.flowey.toml`
10#[derive(clap::Args)]
11pub struct Regen {
12    /// Check that pipelines are up to date, without regenerating them.
13    #[clap(long)]
14    check: bool,
15
16    /// Pass `--quiet` to any subprocess invocations of `cargo run`.
17    #[clap(long)]
18    quiet: bool,
19}
20
21impl Regen {
22    pub fn run(self, repo_root: &Path) -> anyhow::Result<()> {
23        install_flowey_merge_driver()?;
24
25        if !repo_root.join(".flowey.toml").exists() {
26            log::warn!("no .flowey.toml exists in the repo root");
27            return Ok(());
28        }
29
30        let flowey_toml = fs_err::read_to_string(repo_root.join(".flowey.toml"))?;
31        let flowey_toml: flowey_toml::FloweyToml =
32            toml_edit::de::from_str(&flowey_toml).context("while parsing .flowey.toml")?;
33
34        let data = resolve_flowey_toml(flowey_toml, repo_root.to_owned())
35            .context("while resolving .flowey.toml")?;
36
37        let mut bin2flowey = BTreeMap::<String, PathBuf>::new();
38
39        let mut error = false;
40        for ResolvedFloweyToml {
41            working_dir,
42            pipelines,
43        } in data
44        {
45            for (bin_name, pipelines) in pipelines {
46                let exe_name = format!("{bin_name}{}", std::env::consts::EXE_SUFFIX);
47
48                let bin = if let Some(bin) = bin2flowey.get(&bin_name) {
49                    bin.clone()
50                } else {
51                    // build the requested flowey
52                    {
53                        let quiet = self.quiet.then_some("-q");
54                        let sh = xshell::Shell::new()?;
55                        sh.change_dir(&working_dir);
56                        xshell::cmd!(sh, "cargo build -p {bin_name} {quiet...}").run()?;
57                    }
58
59                    // find the built flowey
60                    let bin = working_dir
61                        .join(
62                            std::env::var("CARGO_TARGET_DIR")
63                                .as_deref()
64                                .unwrap_or("target"),
65                        )
66                        .join(std::env::var("CARGO_BUILD_TARGET").as_deref().unwrap_or(""))
67                        .join("debug")
68                        .join(&exe_name);
69
70                    if !bin.exists() {
71                        panic!("should have found built {bin_name} at {}", bin.display());
72                    }
73
74                    // stash result for future consumers
75                    bin2flowey.insert(bin_name.clone(), bin.clone());
76                    bin
77                };
78
79                for (backend, defns) in pipelines {
80                    for flowey_toml::PipelineDefn { file, cmd } in defns {
81                        let check = if self.check {
82                            vec!["--check".into(), file.display().to_string()]
83                        } else {
84                            vec![]
85                        };
86
87                        let sh = xshell::Shell::new()?;
88                        sh.change_dir(&working_dir);
89                        let res = xshell::cmd!(
90                            sh,
91                            "{bin} pipeline {backend} --out {file} {check...} {cmd...}"
92                        )
93                        .run();
94
95                        if res.is_err() {
96                            error = true;
97                        }
98                    }
99                }
100            }
101        }
102
103        if error {
104            anyhow::bail!("encountered one or more errors")
105        }
106
107        Ok(())
108    }
109}
110
111#[derive(Debug)]
112pub struct ResolvedFloweyToml {
113    pub working_dir: PathBuf,
114    // (bin, (backend, metadata))
115    pub pipelines: BTreeMap<String, BTreeMap<String, Vec<flowey_toml::PipelineDefn>>>,
116}
117
118fn resolve_flowey_toml(
119    flowey_toml: flowey_toml::FloweyToml,
120    working_dir: PathBuf,
121) -> anyhow::Result<Vec<ResolvedFloweyToml>> {
122    let mut v = Vec::new();
123    resolve_flowey_toml_inner(flowey_toml, working_dir, &mut v)?;
124    Ok(v)
125}
126
127fn resolve_flowey_toml_inner(
128    flowey_toml: flowey_toml::FloweyToml,
129    working_dir: PathBuf,
130    resolved: &mut Vec<ResolvedFloweyToml>,
131) -> anyhow::Result<()> {
132    let flowey_toml::FloweyToml { include, pipeline } = flowey_toml;
133
134    let mut resolved_pipelines: BTreeMap<String, BTreeMap<String, Vec<_>>> = BTreeMap::new();
135    for (bin_name, pipelines) in pipeline {
136        for (backend, defns) in pipelines {
137            resolved_pipelines
138                .entry(bin_name.clone())
139                .or_default()
140                .entry(backend)
141                .or_default()
142                .extend(defns);
143        }
144    }
145
146    for path in include.unwrap_or_default() {
147        let path = working_dir.join(path);
148        let flowey_toml = fs_err::read_to_string(&path)?;
149        let flowey_toml: flowey_toml::FloweyToml = toml_edit::de::from_str(&flowey_toml)
150            .with_context(|| anyhow::anyhow!("while parsing {}", path.display()))?;
151        let mut working_dir = path;
152        working_dir.pop();
153        resolve_flowey_toml_inner(flowey_toml, working_dir, resolved)?
154    }
155
156    resolved.push(ResolvedFloweyToml {
157        working_dir,
158        pipelines: resolved_pipelines,
159    });
160
161    Ok(())
162}
163
164mod flowey_toml {
165    use serde::Deserialize;
166    use serde::Serialize;
167    use std::collections::BTreeMap;
168    use std::path::PathBuf;
169
170    #[derive(Debug, Serialize, Deserialize)]
171    pub struct FloweyToml {
172        pub include: Option<Vec<PathBuf>>,
173        // (bin, (backend, metadata))
174        pub pipeline: BTreeMap<String, BTreeMap<String, Vec<PipelineDefn>>>,
175    }
176
177    #[derive(Debug, Serialize, Deserialize)]
178    pub struct PipelineDefn {
179        pub file: PathBuf,
180        pub cmd: Vec<String>,
181    }
182}
183
184fn install_flowey_merge_driver() -> anyhow::Result<()> {
185    const DRIVER_NAME: &str = "flowey-theirs merge driver";
186    const DRIVER_COMMAND: &str = "cp %B %A";
187
188    let sh = xshell::Shell::new()?;
189    xshell::cmd!(sh, "git config merge.flowey-theirs.name {DRIVER_NAME}")
190        .quiet()
191        .ignore_status()
192        .run()?;
193    xshell::cmd!(sh, "git config merge.flowey-theirs.driver {DRIVER_COMMAND}")
194        .quiet()
195        .ignore_status()
196        .run()?;
197
198    Ok(())
199}