Skip to main content

flowey_cli/pipeline_resolver/
direct_run.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use crate::cli::exec_snippet::VAR_DB_SEEDVAR_FLOWEY_PERSISTENT_STORAGE_DIR;
5use crate::flow_resolver::stage1_dag::OutputGraphEntry;
6use crate::flow_resolver::stage1_dag::Step;
7use crate::pipeline_resolver::generic::ResolvedJobArtifact;
8use crate::pipeline_resolver::generic::ResolvedJobUseParameter;
9use crate::pipeline_resolver::generic::ResolvedPipeline;
10use crate::pipeline_resolver::generic::ResolvedPipelineJob;
11use flowey_core::node::FlowArch;
12use flowey_core::node::FlowBackend;
13use flowey_core::node::FlowPlatform;
14use flowey_core::node::NodeHandle;
15use flowey_core::node::RuntimeVarDb;
16use flowey_core::node::steps::rust::RustRuntimeServices;
17use flowey_core::pipeline::HostExt;
18use flowey_core::pipeline::PipelineBackendHint;
19use flowey_core::pipeline::internal::Parameter;
20use petgraph::prelude::NodeIndex;
21use petgraph::visit::EdgeRef;
22use std::collections::BTreeSet;
23use std::path::Path;
24use std::path::PathBuf;
25
26struct ResolvedRunnableStep {
27    node_handle: NodeHandle,
28    label: String,
29    code: Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static>,
30    idx: usize,
31    can_merge: bool,
32}
33
34/// Directly run the pipeline using flowey
35pub fn direct_run(
36    pipeline: ResolvedPipeline,
37    windows_as_wsl: bool,
38    out_dir: PathBuf,
39    persist_dir: PathBuf,
40) -> anyhow::Result<()> {
41    direct_run_do_work(pipeline, windows_as_wsl, out_dir.clone(), persist_dir)?;
42
43    // cleanup
44    if out_dir.join(".job_artifacts").exists() {
45        fs_err::remove_dir_all(out_dir.join(".job_artifacts"))?;
46    }
47    if out_dir.join(".work").exists() {
48        fs_err::remove_dir_all(out_dir.join(".work"))?;
49    }
50
51    Ok(())
52}
53
54fn direct_run_do_work(
55    pipeline: ResolvedPipeline,
56    windows_as_wsl: bool,
57    out_dir: PathBuf,
58    persist_dir: PathBuf,
59) -> anyhow::Result<()> {
60    fs_err::create_dir_all(&out_dir)?;
61    let out_dir = std::path::absolute(out_dir)?;
62
63    fs_err::create_dir_all(&persist_dir)?;
64    let persist_dir = std::path::absolute(persist_dir)?;
65
66    let ResolvedPipeline {
67        graph,
68        order,
69        parameters,
70        ado_name: _,
71        ado_schedule_triggers: _,
72        ado_ci_triggers: _,
73        ado_pr_triggers: _,
74        ado_bootstrap_template: _,
75        ado_resources_repository: _,
76        ado_post_process_yaml_cb: _,
77        ado_variables: _,
78        ado_job_id_overrides: _,
79        gh_name: _,
80        gh_schedule_triggers: _,
81        gh_ci_triggers: _,
82        gh_pr_triggers: _,
83        gh_bootstrap_template: _,
84    } = pipeline;
85
86    let mut skipped_jobs = BTreeSet::new();
87
88    for idx in order {
89        let ResolvedPipelineJob {
90            ref root_nodes,
91            ref root_configs,
92            ref patches,
93            ref label,
94            platform,
95            arch,
96            cond_param_idx,
97            timeout_minutes: _,
98            ref command_wrapper,
99            ado_pool: _,
100            ado_variables: _,
101            gh_override_if: _,
102            gh_global_env: _,
103            gh_pool: _,
104            gh_concurrency_group: _,
105            gh_permissions: _,
106            ref external_read_vars,
107            ref parameters_used,
108            ref artifacts_used,
109            ref artifacts_published,
110        } = graph[idx];
111
112        // orange color
113        log::info!("\x1B[0;33m### job: {label} ###\x1B[0m");
114        log::info!("");
115
116        if graph
117            .edges_directed(idx, petgraph::Direction::Incoming)
118            .any(|e| skipped_jobs.contains(&NodeIndex::from(e.source().index() as u32)))
119        {
120            log::error!("job depends on job that was skipped. skipping job...");
121            log::info!("");
122            skipped_jobs.insert(idx);
123            continue;
124        }
125
126        let flow_arch = FlowArch::host(PipelineBackendHint::Local);
127        match (arch, flow_arch) {
128            (FlowArch::X86_64, FlowArch::X86_64) | (FlowArch::Aarch64, FlowArch::Aarch64) => (),
129            _ => {
130                log::error!("mismatch between job arch and local arch. skipping job...");
131                skipped_jobs.insert(idx);
132                continue;
133            }
134        }
135
136        let flow_platform = FlowPlatform::host(PipelineBackendHint::Local);
137        let platform_ok = match (platform, flow_platform) {
138            (FlowPlatform::Windows, FlowPlatform::Windows) => true,
139            (FlowPlatform::Windows, FlowPlatform::Linux(_)) if windows_as_wsl => true,
140            (FlowPlatform::Linux(_), FlowPlatform::Linux(_)) => true,
141            (FlowPlatform::MacOs, FlowPlatform::MacOs) => true,
142            _ => false,
143        };
144
145        if !platform_ok {
146            log::error!("mismatch between job platform and local platform. skipping job...");
147            log::info!("");
148            if crate::running_in_wsl() && matches!(platform, FlowPlatform::Windows) {
149                log::warn!("###");
150                log::warn!("### NOTE: detected that you're running in WSL2");
151                log::warn!(
152                    "###       if the the pipeline supports it, you can try passing --windows-as-wsl"
153                );
154                log::warn!("###");
155                log::info!("");
156            }
157            skipped_jobs.insert(idx);
158            continue;
159        }
160
161        // Use the job's declared platform for DAG resolution and runtime,
162        // except when --windows-as-wsl is active: in that case, the job
163        // declares Windows but we're actually running on Linux/WSL, so
164        // use the host platform instead.
165        let runtime_platform = if windows_as_wsl
166            && matches!(platform, FlowPlatform::Windows)
167            && matches!(flow_platform, FlowPlatform::Linux(_))
168        {
169            flow_platform
170        } else {
171            platform
172        };
173
174        let nodes = {
175            let mut resolved_local_steps = Vec::new();
176
177            let crate::flow_resolver::stage1_dag::Stage1DagOutput {
178                mut output_graph,
179                found_unreachable_nodes,
180                ..
181            } = crate::flow_resolver::stage1_dag::stage1_dag(
182                FlowBackend::Local,
183                runtime_platform,
184                arch,
185                patches.clone(),
186                root_nodes
187                    .clone()
188                    .into_iter()
189                    .map(|(node, requests)| (node, (true, requests)))
190                    .collect(),
191                root_configs.clone(),
192                external_read_vars.clone(),
193                Some(VAR_DB_SEEDVAR_FLOWEY_PERSISTENT_STORAGE_DIR.into()),
194            )?;
195
196            if found_unreachable_nodes {
197                anyhow::bail!("detected unreachable nodes")
198            }
199
200            let output_order = petgraph::algo::toposort(&output_graph, None)
201                .map_err(|e| {
202                    format!(
203                        "includes node {}",
204                        output_graph[e.node_id()].0.node.modpath()
205                    )
206                })
207                .expect("runtime variables cannot introduce a DAG cycle");
208
209            for idx in output_order.into_iter().rev() {
210                let OutputGraphEntry { node_handle, step } = output_graph[idx].1.take().unwrap();
211
212                let (label, code, idx, can_merge) = match step {
213                    Step::Anchor { .. } => continue,
214                    Step::Rust {
215                        label,
216                        code,
217                        idx,
218                        can_merge,
219                    } => (label, code, idx, can_merge),
220                    Step::AdoYaml { .. } => {
221                        anyhow::bail!(
222                            "{} emitted ADO YAML. Fix the node by checking `ctx.backend()` appropriately",
223                            node_handle.modpath()
224                        )
225                    }
226                    Step::GitHubYaml { .. } => {
227                        anyhow::bail!(
228                            "{} emitted GitHub YAML. Fix the node by checking `ctx.backend()` appropriately",
229                            node_handle.modpath()
230                        )
231                    }
232                };
233
234                resolved_local_steps.push(ResolvedRunnableStep {
235                    node_handle,
236                    label,
237                    code: code.lock().take().unwrap(),
238                    idx,
239                    can_merge,
240                });
241            }
242
243            resolved_local_steps
244        };
245
246        let mut in_mem_var_db = crate::var_db::in_memory::InMemoryVarDb::new();
247
248        for ResolvedJobUseParameter {
249            flowey_var,
250            pipeline_param_idx,
251        } in parameters_used
252        {
253            log::trace!(
254                "resolving parameter idx {}, flowey_var {:?}",
255                pipeline_param_idx,
256                flowey_var
257            );
258            let (desc, value) = match &parameters[*pipeline_param_idx] {
259                Parameter::Bool {
260                    name: _,
261                    description,
262                    kind: _,
263                    default,
264                } => (
265                    description,
266                    default.as_ref().map(|v| serde_json::to_vec(v).unwrap()),
267                ),
268                Parameter::String {
269                    name: _,
270                    description,
271                    kind: _,
272                    default,
273                    possible_values: _,
274                } => (
275                    description,
276                    default.as_ref().map(|v| serde_json::to_vec(v).unwrap()),
277                ),
278                Parameter::Num {
279                    name: _,
280                    description,
281                    kind: _,
282                    default,
283                    possible_values: _,
284                } => (
285                    description,
286                    default.as_ref().map(|v| serde_json::to_vec(v).unwrap()),
287                ),
288            };
289
290            let Some(value) = value else {
291                anyhow::bail!(
292                    "pipeline must specify default value for params when running locally. missing default for '{desc}'"
293                )
294            };
295
296            in_mem_var_db.set_var(flowey_var, false, value);
297        }
298
299        in_mem_var_db.set_var(
300            VAR_DB_SEEDVAR_FLOWEY_PERSISTENT_STORAGE_DIR,
301            false,
302            serde_json::to_string(&persist_dir).unwrap().into(),
303        );
304
305        for ResolvedJobArtifact { flowey_var, name } in artifacts_published {
306            let path = out_dir.join("artifacts").join(name);
307            fs_err::create_dir_all(&path)?;
308
309            in_mem_var_db.set_var(
310                flowey_var,
311                false,
312                serde_json::to_string(&path).unwrap().into(),
313            );
314        }
315
316        if out_dir.join(".job_artifacts").exists() {
317            fs_err::remove_dir_all(out_dir.join(".job_artifacts"))?;
318        }
319        fs_err::create_dir_all(out_dir.join(".job_artifacts"))?;
320
321        for ResolvedJobArtifact { flowey_var, name } in artifacts_used {
322            let path = out_dir.join(".job_artifacts").join(name);
323            fs_err::create_dir_all(&path)?;
324            copy_dir_all(out_dir.join("artifacts").join(name), &path)?;
325
326            in_mem_var_db.set_var(
327                flowey_var,
328                false,
329                serde_json::to_string(&path).unwrap().into(),
330            );
331        }
332
333        if out_dir.join(".work").exists() {
334            fs_err::remove_dir_all(out_dir.join(".work"))?;
335        }
336        fs_err::create_dir_all(out_dir.join(".work"))?;
337
338        if let Some(cond_param_idx) = cond_param_idx {
339            let Parameter::Bool {
340                name,
341                description: _,
342                kind: _,
343                default: _,
344            } = &parameters[cond_param_idx]
345            else {
346                panic!("cond param is guaranteed to be bool by type system")
347            };
348
349            // Vars should have had their default already applied, so this should never fail.
350            let (data, _secret) = in_mem_var_db.get_var(name);
351            let should_run: bool = serde_json::from_slice(&data).unwrap();
352
353            if !should_run {
354                log::warn!("job condition was false - skipping job...");
355                skipped_jobs.insert(idx);
356                continue;
357            }
358        }
359
360        let mut runtime_services = flowey_core::node::steps::rust::new_rust_runtime_services(
361            &mut in_mem_var_db,
362            FlowBackend::Local,
363            runtime_platform,
364            arch,
365        )?;
366
367        if let Some(wrapper) = command_wrapper {
368            runtime_services.sh.set_wrapper(Some(wrapper.clone()));
369        }
370
371        for ResolvedRunnableStep {
372            node_handle,
373            label,
374            code,
375            idx,
376            can_merge,
377        } in nodes
378        {
379            let node_working_dir = out_dir.join(".work").join(format!(
380                "{}_{}",
381                node_handle.modpath().replace("::", "__"),
382                idx
383            ));
384            if !node_working_dir.exists() {
385                fs_err::create_dir(&node_working_dir)?;
386            }
387
388            std::env::set_current_dir(node_working_dir.clone())?;
389            runtime_services.sh.change_dir(node_working_dir);
390
391            if can_merge {
392                log::debug!("minor step: {} ({})", label, node_handle.modpath(),);
393            } else {
394                log::info!(
395                    // green color
396                    "\x1B[0;32m=== {} ({}) ===\x1B[0m",
397                    label,
398                    node_handle.modpath(),
399                );
400            }
401            code(&mut runtime_services)?;
402            if can_merge {
403                log::debug!("done!");
404                log::debug!(""); // log a newline, for the pretty
405            } else {
406                log::info!("\x1B[0;32m=== done! ===\x1B[0m");
407                log::info!(""); // log a newline, for the pretty
408            }
409        }
410
411        // Leave the last node's working dir so it can be deleted by later steps
412        std::env::set_current_dir(&out_dir)?;
413    }
414
415    Ok(())
416}
417
418fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
419    fs_err::create_dir_all(&dst)?;
420    for entry in fs_err::read_dir(src.as_ref())? {
421        let entry = entry?;
422        let ty = entry.file_type()?;
423        if ty.is_dir() {
424            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
425        } else {
426            fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
427        }
428    }
429    Ok(())
430}