fud_core/exec/
driver.rs

1use super::{OpRef, Operation, Request, Setup, SetupRef, State, StateRef};
2use crate::{run, utils};
3use camino::{Utf8Path, Utf8PathBuf};
4use cranelift_entity::{PrimaryMap, SecondaryMap};
5
6#[derive(PartialEq)]
7enum Destination {
8    State(StateRef),
9    Op(OpRef),
10}
11
12/// A Driver encapsulates a set of States and the Operations that can transform between them. It
13/// contains all the machinery to perform builds in a given ecosystem.
14pub struct Driver {
15    pub name: String,
16    pub setups: PrimaryMap<SetupRef, Setup>,
17    pub states: PrimaryMap<StateRef, State>,
18    pub ops: PrimaryMap<OpRef, Operation>,
19}
20
21impl Driver {
22    /// Find a chain of Operations from the `start` state to the `end`, which may be a state or the
23    /// final operation in the chain.
24    fn find_path_segment(
25        &self,
26        start: StateRef,
27        end: Destination,
28    ) -> Option<Vec<OpRef>> {
29        // Our start state is the input.
30        let mut visited = SecondaryMap::<StateRef, bool>::new();
31        visited[start] = true;
32
33        // Build the incoming edges for each vertex.
34        let mut breadcrumbs = SecondaryMap::<StateRef, Option<OpRef>>::new();
35
36        // Breadth-first search.
37        let mut state_queue: Vec<StateRef> = vec![start];
38        while !state_queue.is_empty() {
39            let cur_state = state_queue.remove(0);
40
41            // Finish when we reach the goal vertex.
42            if end == Destination::State(cur_state) {
43                break;
44            }
45
46            // Traverse any edge from the current state to an unvisited state.
47            for (op_ref, op) in self.ops.iter() {
48                if op.input == cur_state && !visited[op.output] {
49                    state_queue.push(op.output);
50                    visited[op.output] = true;
51                    breadcrumbs[op.output] = Some(op_ref);
52                }
53
54                // Finish when we reach the goal edge.
55                if end == Destination::Op(op_ref) {
56                    break;
57                }
58            }
59        }
60
61        // Traverse the breadcrumbs backward to build up the path back from output to input.
62        let mut op_path: Vec<OpRef> = vec![];
63        let mut cur_state = match end {
64            Destination::State(state) => state,
65            Destination::Op(op) => {
66                op_path.push(op);
67                self.ops[op].input
68            }
69        };
70        while cur_state != start {
71            match breadcrumbs[cur_state] {
72                Some(op) => {
73                    op_path.push(op);
74                    cur_state = self.ops[op].input;
75                }
76                None => return None,
77            }
78        }
79        op_path.reverse();
80
81        Some(op_path)
82    }
83
84    /// Find a chain of operations from the `start` state to the `end` state, passing through each
85    /// `through` operation in order.
86    pub fn find_path(
87        &self,
88        start: StateRef,
89        end: StateRef,
90        through: &[OpRef],
91    ) -> Option<Vec<OpRef>> {
92        let mut cur_state = start;
93        let mut op_path: Vec<OpRef> = vec![];
94
95        // Build path segments through each through required operation.
96        for op in through {
97            let segment =
98                self.find_path_segment(cur_state, Destination::Op(*op))?;
99            op_path.extend(segment);
100            cur_state = self.ops[*op].output;
101        }
102
103        // Build the final path segment to the destination state.
104        let segment =
105            self.find_path_segment(cur_state, Destination::State(end))?;
106        op_path.extend(segment);
107
108        Some(op_path)
109    }
110
111    /// Generate a filename with an extension appropriate for the given State.
112    fn gen_name(&self, stem: &str, state: StateRef) -> Utf8PathBuf {
113        let state = &self.states[state];
114        if state.is_pseudo() {
115            Utf8PathBuf::from(format!("_pseudo_{}", state.name))
116        } else {
117            // TODO avoid collisions in case we reuse extensions...
118            Utf8PathBuf::from(stem).with_extension(&state.extensions[0])
119        }
120    }
121
122    pub fn plan(&self, req: Request) -> Option<Plan> {
123        // Find a path through the states.
124        let path =
125            self.find_path(req.start_state, req.end_state, &req.through)?;
126
127        let mut steps: Vec<(OpRef, Utf8PathBuf)> = vec![];
128
129        // Get the initial input filename and the stem to use to generate all intermediate filenames.
130        let (stdin, start_file) = match req.start_file {
131            Some(path) => (false, utils::relative_path(&path, &req.workdir)),
132            None => (true, "stdin".into()),
133        };
134        let stem = start_file.file_stem().unwrap();
135
136        // Generate filenames for each step.
137        steps.extend(path.into_iter().map(|op| {
138            let filename = self.gen_name(stem, self.ops[op].output);
139            (op, filename)
140        }));
141
142        // If we have a specified output filename, use that instead of the generated one.
143        let stdout = if let Some(end_file) = req.end_file {
144            // TODO Can we just avoid generating the unused filename in the first place?
145            let last_step = steps.last_mut().expect("no steps");
146            last_step.1 = utils::relative_path(&end_file, &req.workdir);
147            false
148        } else {
149            // Print to stdout if the last state is a real (non-pseudo) state.
150            !self.states[req.end_state].is_pseudo()
151        };
152
153        Some(Plan {
154            start: start_file,
155            steps,
156            workdir: req.workdir,
157            stdin,
158            stdout,
159        })
160    }
161
162    pub fn guess_state(&self, path: &Utf8Path) -> Option<StateRef> {
163        let ext = path.extension()?;
164        self.states
165            .iter()
166            .find(|(_, state_data)| state_data.ext_matches(ext))
167            .map(|(state, _)| state)
168    }
169
170    pub fn get_state(&self, name: &str) -> Option<StateRef> {
171        self.states
172            .iter()
173            .find(|(_, state_data)| state_data.name == name)
174            .map(|(state, _)| state)
175    }
176
177    pub fn get_op(&self, name: &str) -> Option<OpRef> {
178        self.ops
179            .iter()
180            .find(|(_, op_data)| op_data.name == name)
181            .map(|(op, _)| op)
182    }
183
184    /// The working directory to use when running a build.
185    pub fn default_workdir(&self) -> Utf8PathBuf {
186        format!(".{}", &self.name).into()
187    }
188}
189
190pub struct DriverBuilder {
191    name: String,
192    setups: PrimaryMap<SetupRef, Setup>,
193    states: PrimaryMap<StateRef, State>,
194    ops: PrimaryMap<OpRef, Operation>,
195}
196
197impl DriverBuilder {
198    pub fn new(name: &str) -> Self {
199        Self {
200            name: name.to_string(),
201            setups: Default::default(),
202            states: Default::default(),
203            ops: Default::default(),
204        }
205    }
206
207    pub fn state(&mut self, name: &str, extensions: &[&str]) -> StateRef {
208        self.states.push(State {
209            name: name.to_string(),
210            extensions: extensions.iter().map(|s| s.to_string()).collect(),
211        })
212    }
213
214    fn add_op<T: run::EmitBuild + 'static>(
215        &mut self,
216        name: &str,
217        setups: &[SetupRef],
218        input: StateRef,
219        output: StateRef,
220        emit: T,
221    ) -> OpRef {
222        self.ops.push(Operation {
223            name: name.into(),
224            setups: setups.into(),
225            input,
226            output,
227            emit: Box::new(emit),
228        })
229    }
230
231    pub fn add_setup<T: run::EmitSetup + 'static>(
232        &mut self,
233        name: &str,
234        emit: T,
235    ) -> SetupRef {
236        self.setups.push(Setup {
237            name: name.into(),
238            emit: Box::new(emit),
239        })
240    }
241
242    pub fn setup(&mut self, name: &str, func: run::EmitSetupFn) -> SetupRef {
243        self.add_setup(name, func)
244    }
245
246    pub fn op(
247        &mut self,
248        name: &str,
249        setups: &[SetupRef],
250        input: StateRef,
251        output: StateRef,
252        build: run::EmitBuildFn,
253    ) -> OpRef {
254        self.add_op(name, setups, input, output, build)
255    }
256
257    pub fn rule(
258        &mut self,
259        setups: &[SetupRef],
260        input: StateRef,
261        output: StateRef,
262        rule_name: &str,
263    ) -> OpRef {
264        self.add_op(
265            rule_name,
266            setups,
267            input,
268            output,
269            run::EmitRuleBuild {
270                rule_name: rule_name.to_string(),
271            },
272        )
273    }
274
275    pub fn build(self) -> Driver {
276        Driver {
277            name: self.name,
278            setups: self.setups,
279            states: self.states,
280            ops: self.ops,
281        }
282    }
283}
284
285#[derive(Debug)]
286pub struct Plan {
287    /// The input to the first step.
288    pub start: Utf8PathBuf,
289
290    /// The chain of operations to run and each step's output file.
291    pub steps: Vec<(OpRef, Utf8PathBuf)>,
292
293    /// The directory that the build will happen in.
294    pub workdir: Utf8PathBuf,
295
296    /// Read the first input from stdin.
297    pub stdin: bool,
298
299    /// Write the final output to stdout.
300    pub stdout: bool,
301}
302
303impl Plan {
304    pub fn end(&self) -> &Utf8Path {
305        match self.steps.last() {
306            Some((_, path)) => path,
307            None => &self.start,
308        }
309    }
310}