1use 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
34pub 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 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 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 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 ¶meters[*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 } = ¶meters[cond_param_idx]
345 else {
346 panic!("cond param is guaranteed to be bool by type system")
347 };
348
349 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 "\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!(""); } else {
406 log::info!("\x1B[0;32m=== done! ===\x1B[0m");
407 log::info!(""); }
409 }
410
411 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}