Skip to main content

flowey_core/
node.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Core types and traits used to create and work with flowey nodes.
5
6mod github_context;
7mod spec;
8
9pub use github_context::GhOutput;
10pub use github_context::GhToRust;
11pub use github_context::RustToGh;
12
13use self::steps::ado::AdoRuntimeVar;
14use self::steps::ado::AdoStepServices;
15use self::steps::github::GhStepBuilder;
16use self::steps::rust::RustRuntimeServices;
17use self::user_facing::ClaimedGhParam;
18use self::user_facing::GhPermission;
19use self::user_facing::GhPermissionValue;
20use crate::node::github_context::GhContextVarReader;
21use github_context::state::Root;
22use serde::Deserialize;
23use serde::Serialize;
24use serde::de::DeserializeOwned;
25use std::cell::RefCell;
26use std::collections::BTreeMap;
27use std::path::PathBuf;
28use std::rc::Rc;
29use user_facing::GhParam;
30
31/// Node types which are considered "user facing", and re-exported in the
32/// `flowey` crate.
33pub mod user_facing {
34    pub use super::ClaimVar;
35    pub use super::ClaimedReadVar;
36    pub use super::ClaimedWriteVar;
37    pub use super::ConfigField;
38    pub use super::ConfigMerge;
39    pub use super::ConfigVar;
40    pub use super::FlowArch;
41    pub use super::FlowBackend;
42    pub use super::FlowNode;
43    pub use super::FlowNodeWithConfig;
44    pub use super::FlowPlatform;
45    pub use super::FlowPlatformKind;
46    pub use super::GhUserSecretVar;
47    pub use super::ImportCtx;
48    pub use super::IntoConfig;
49    pub use super::IntoRequest;
50    pub use super::NodeCtx;
51    pub use super::ReadVar;
52    pub use super::SideEffect;
53    pub use super::SimpleFlowNode;
54    pub use super::StepCtx;
55    pub use super::VarClaimed;
56    pub use super::VarEqBacking;
57    pub use super::VarNotClaimed;
58    pub use super::WriteVar;
59    pub use super::steps::ado::AdoResourcesRepositoryId;
60    pub use super::steps::ado::AdoRuntimeVar;
61    pub use super::steps::ado::AdoStepServices;
62    pub use super::steps::github::ClaimedGhParam;
63    pub use super::steps::github::GhParam;
64    pub use super::steps::github::GhPermission;
65    pub use super::steps::github::GhPermissionValue;
66    pub use super::steps::rust::RustRuntimeServices;
67    pub use crate::flowey_config;
68    pub use crate::flowey_request;
69    pub use crate::new_flow_node;
70    pub use crate::new_flow_node_with_config;
71    pub use crate::new_simple_flow_node;
72    pub use crate::node::FlowPlatformLinuxDistro;
73    pub use crate::pipeline::Artifact;
74    pub use crate::pipeline::ArtifactType;
75    pub use crate::pipeline::resolve as resolve_artifact;
76
77    /// Helper method to streamline request validation in cases where a value is
78    /// expected to be identical across all incoming requests.
79    ///
80    /// # Example: Request Aggregation Pattern
81    ///
82    /// When a node receives multiple requests, it often needs to ensure certain
83    /// values are consistent across all requests. This helper simplifies that pattern:
84    ///
85    /// ```rust,ignore
86    /// fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
87    ///     let mut version = None;
88    ///     let mut ensure_installed = Vec::new();
89    ///
90    ///     for req in requests {
91    ///         match req {
92    ///             Request::Version(v) => {
93    ///                 // Ensure all requests agree on the version
94    ///                 same_across_all_reqs("Version", &mut version, v)?;
95    ///             }
96    ///             Request::EnsureInstalled(v) => {
97    ///                 ensure_installed.push(v);
98    ///             }
99    ///         }
100    ///     }
101    ///
102    ///     let version = version.ok_or(anyhow::anyhow!("Missing required request: Version"))?;
103    ///
104    ///     // ... emit steps using aggregated requests
105    ///     Ok(())
106    /// }
107    /// ```
108    pub fn same_across_all_reqs<T: PartialEq>(
109        req_name: &str,
110        var: &mut Option<T>,
111        new: T,
112    ) -> anyhow::Result<()> {
113        match (var.as_ref(), new) {
114            (None, v) => *var = Some(v),
115            (Some(old), new) => {
116                if *old != new {
117                    anyhow::bail!("`{}` must be consistent across requests", req_name);
118                }
119            }
120        }
121
122        Ok(())
123    }
124
125    /// Helper method to streamline request validation in cases where a value is
126    /// expected to be identical across all incoming requests, using a custom
127    /// comparison function.
128    pub fn same_across_all_reqs_backing_var<V: VarEqBacking>(
129        req_name: &str,
130        var: &mut Option<V>,
131        new: V,
132    ) -> anyhow::Result<()> {
133        match (var.as_ref(), new) {
134            (None, v) => *var = Some(v),
135            (Some(old), new) => {
136                if !old.eq(&new) {
137                    anyhow::bail!("`{}` must be consistent across requests", req_name);
138                }
139            }
140        }
141
142        Ok(())
143    }
144
145    /// Helper method to handle Linux distros that are supported only on one
146    /// host architecture.
147    /// match_arch!(var, arch, result)
148    #[macro_export]
149    macro_rules! match_arch {
150        ($host_arch:expr, $match_arch:pat, $expr:expr) => {
151            if matches!($host_arch, $match_arch) {
152                $expr
153            } else {
154                anyhow::bail!("Linux distro not supported on host arch {}", $host_arch);
155            }
156        };
157    }
158
159    /// Claim a set of vars
160    #[macro_export]
161    macro_rules! claim_vars {
162        ($ctx:ident, ($($var:ident),* $(,)?)) => {
163            $(let $var = $var.claim($ctx);)*
164        };
165    }
166
167    /// Read a set of vars
168    #[macro_export]
169    macro_rules! read_vars {
170        ($rt:ident, ($($var:ident),* $(,)?)) => {
171            $(let $var = $rt.read($var);)*
172        };
173    }
174}
175
176/// Check if `ReadVar` / `WriteVar` instances are backed by the same underlying
177/// flowey Var.
178///
179/// # Why not use `Eq`? Why have a whole separate trait?
180///
181/// `ReadVar` and `WriteVar` are, in some sense, flowey's analog to
182/// "pointers", insofar as these types primary purpose is to mediate access to
183/// some contained value, as opposed to being "values" themselves.
184///
185/// Assuming you agree with this analogy, then we can apply the same logic to
186/// `ReadVar` and `WriteVar` as Rust does to `Box<T>` wrt. what the `Eq`
187/// implementation should mean.
188///
189/// Namely: `Eq` should check the equality of the _contained objects_, as
190/// opposed to the pointers themselves.
191///
192/// Unfortunately, unlike `Box<T>`, it is _impossible_ to have an `Eq` impl for
193/// `ReadVar` / `WriteVar` that checks contents for equality, due to the fact
194/// that these types exist at flow resolution time, whereas the values they
195/// contain only exist at flow runtime.
196///
197/// As such, we have a separate trait to perform different kinds of equality
198/// checks on Vars.
199pub trait VarEqBacking {
200    /// Check if `self` is backed by the same variable as `other`.
201    fn eq(&self, other: &Self) -> bool;
202}
203
204impl<T> VarEqBacking for WriteVar<T>
205where
206    T: Serialize + DeserializeOwned,
207{
208    fn eq(&self, other: &Self) -> bool {
209        self.backing_var == other.backing_var
210    }
211}
212
213impl<T> VarEqBacking for ReadVar<T>
214where
215    T: Serialize + DeserializeOwned + PartialEq + Eq + Clone,
216{
217    fn eq(&self, other: &Self) -> bool {
218        self.backing_var == other.backing_var
219    }
220}
221
222// TODO: this should be generic across all tuple sizes
223impl<T, U> VarEqBacking for (T, U)
224where
225    T: VarEqBacking,
226    U: VarEqBacking,
227{
228    fn eq(&self, other: &Self) -> bool {
229        (self.0.eq(&other.0)) && (self.1.eq(&other.1))
230    }
231}
232
233/// A wrapper around [`ReadVar<T>`] that implements [`PartialEq`] via
234/// backing-variable identity ([`VarEqBacking`]).
235///
236/// Use this in config structs where a `ReadVar` field needs equality
237/// comparison for config merging. Since `ReadVar` deliberately does not
238/// implement `PartialEq` (its values aren't known at flow-resolution time),
239/// `ConfigVar` provides identity-based comparison instead.
240///
241/// # Example
242///
243/// ```rust,ignore
244/// flowey_config! {
245///     pub struct Config {
246///         pub verbose: Option<ConfigVar<bool>>,
247///     }
248/// }
249/// ```
250#[derive(Serialize, Deserialize)]
251#[serde(bound(serialize = "T: Serialize", deserialize = "T: DeserializeOwned"))]
252pub struct ConfigVar<T>(pub ReadVar<T>);
253
254impl<T: Serialize + DeserializeOwned> Clone for ConfigVar<T> {
255    fn clone(&self) -> Self {
256        ConfigVar(self.0.clone())
257    }
258}
259
260impl<T> std::fmt::Debug for ConfigVar<T> {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        f.debug_tuple("ConfigVar").finish()
263    }
264}
265
266impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone> PartialEq for ConfigVar<T> {
267    fn eq(&self, other: &Self) -> bool {
268        VarEqBacking::eq(&self.0, &other.0)
269    }
270}
271
272impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone> ClaimVar for ConfigVar<T> {
273    type Claimed = ClaimedReadVar<T>;
274
275    fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedReadVar<T> {
276        self.0.claim(ctx)
277    }
278}
279
280impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone> From<ReadVar<T>> for ConfigVar<T> {
281    fn from(v: ReadVar<T>) -> Self {
282        ConfigVar(v)
283    }
284}
285
286/// Type corresponding to a step which performs a side-effect,
287/// without returning a specific value.
288///
289/// e.g: A step responsible for installing a package from `apt` might claim a
290/// `WriteVar<SideEffect>`, with any step requiring the package to have been
291/// installed prior being able to claim the corresponding `ReadVar<SideEffect>.`
292pub type SideEffect = ();
293
294/// Uninhabited type used to denote that a particular [`WriteVar`] / [`ReadVar`]
295/// is not currently claimed by any step, and cannot be directly accessed.
296#[derive(Clone, Debug, Serialize, Deserialize)]
297pub enum VarNotClaimed {}
298
299/// Uninhabited type used to denote that a particular [`WriteVar`] / [`ReadVar`]
300/// is currently claimed by a step, and can be read/written to.
301#[derive(Clone, Debug, Serialize, Deserialize)]
302pub enum VarClaimed {}
303
304/// Write a value into a flowey Var at runtime, which can then be read via a
305/// corresponding [`ReadVar`].
306///
307/// Vars in flowey must be serde de/serializable, in order to be de/serialized
308/// between multiple steps/nodes.
309///
310/// In order to write a value into a `WriteVar`, it must first be _claimed_ by a
311/// particular step (using the [`ClaimVar::claim`] API). Once claimed, the Var
312/// can be written to using APIs such as [`RustRuntimeServices::write`], or
313/// [`AdoStepServices::set_var`]
314///
315/// Note that it is only possible to write a value into a `WriteVar` _once_.
316/// Once the value has been written, the `WriteVar` type is immediately
317/// consumed, making it impossible to overwrite the stored value at some later
318/// point in execution.
319///
320/// This "write-once" property is foundational to flowey's execution model, as
321/// by recoding what step wrote to a Var, and what step(s) read from the Var, it
322/// is possible to infer what order steps must be run in.
323#[derive(Debug, Serialize, Deserialize)]
324pub struct WriteVar<T: Serialize + DeserializeOwned, C = VarNotClaimed> {
325    backing_var: String,
326    /// If true, then readers on this var expect to read a side effect (`()`)
327    /// and not `T`.
328    is_side_effect: bool,
329
330    #[serde(skip)]
331    _kind: core::marker::PhantomData<(T, C)>,
332}
333
334/// A [`WriteVar`] which has been claimed by a particular step, allowing it
335/// to be written to at runtime.
336pub type ClaimedWriteVar<T> = WriteVar<T, VarClaimed>;
337
338impl<T: Serialize + DeserializeOwned> WriteVar<T, VarNotClaimed> {
339    /// (Internal API) Switch the claim marker to "claimed".
340    fn into_claimed(self) -> WriteVar<T, VarClaimed> {
341        let Self {
342            backing_var,
343            is_side_effect,
344            _kind,
345        } = self;
346
347        WriteVar {
348            backing_var,
349            is_side_effect,
350            _kind: std::marker::PhantomData,
351        }
352    }
353
354    /// Write a static value into the Var.
355    #[track_caller]
356    pub fn write_static(self, ctx: &mut NodeCtx<'_>, val: T)
357    where
358        T: 'static,
359    {
360        let val = ReadVar::from_static(val);
361        val.write_into(ctx, self);
362    }
363
364    pub(crate) fn into_json(self) -> WriteVar<serde_json::Value> {
365        WriteVar {
366            backing_var: self.backing_var,
367            is_side_effect: self.is_side_effect,
368            _kind: std::marker::PhantomData,
369        }
370    }
371}
372
373impl WriteVar<SideEffect, VarNotClaimed> {
374    /// Transforms this writer into one that can be used to write a `T`.
375    ///
376    /// This is useful when a reader only cares about the side effect of an
377    /// operation, but the writer wants to provide output as well.
378    pub fn discard_result<T: Serialize + DeserializeOwned>(self) -> WriteVar<T> {
379        WriteVar {
380            backing_var: self.backing_var,
381            is_side_effect: true,
382            _kind: std::marker::PhantomData,
383        }
384    }
385}
386
387/// Claim one or more flowey Vars for a particular step.
388///
389/// By having this be a trait, it is possible to `claim` both single instances
390/// of `ReadVar` / `WriteVar`, as well as whole _collections_ of Vars.
391//
392// FUTURE: flowey should include a derive macro for easily claiming read/write
393// vars in user-defined structs / enums.
394pub trait ClaimVar {
395    /// The claimed version of Self.
396    type Claimed;
397    /// Claim the Var for this step, allowing it to be accessed at runtime.
398    fn claim(self, ctx: &mut StepCtx<'_>) -> Self::Claimed;
399}
400
401/// Read the value of one or more flowey Vars.
402///
403/// By having this be a trait, it is possible to `read` both single
404/// instances of `ReadVar` / `WriteVar`, as well as whole _collections_ of
405/// Vars.
406pub trait ReadVarValue {
407    /// The read value of Self.
408    type Value;
409    /// Read the value of the Var at runtime.
410    fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value;
411}
412
413impl<T: Serialize + DeserializeOwned> ClaimVar for ReadVar<T> {
414    type Claimed = ClaimedReadVar<T>;
415
416    fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedReadVar<T> {
417        if let ReadVarBacking::RuntimeVar {
418            var,
419            is_side_effect: _,
420        } = &self.backing_var
421        {
422            ctx.backend.borrow_mut().on_claimed_runtime_var(var, true);
423        }
424        self.into_claimed()
425    }
426}
427
428impl<T: Serialize + DeserializeOwned> ClaimVar for WriteVar<T> {
429    type Claimed = ClaimedWriteVar<T>;
430
431    fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedWriteVar<T> {
432        ctx.backend
433            .borrow_mut()
434            .on_claimed_runtime_var(&self.backing_var, false);
435        self.into_claimed()
436    }
437}
438
439impl<T: Serialize + DeserializeOwned> ReadVarValue for ClaimedReadVar<T> {
440    type Value = T;
441
442    fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
443        match self.backing_var {
444            ReadVarBacking::RuntimeVar {
445                var,
446                is_side_effect,
447            } => {
448                // Always get the data to validate that the variable is actually there.
449                let data = rt.get_var(&var, is_side_effect);
450                if is_side_effect {
451                    // This was converted into a `ReadVar<SideEffect>` from
452                    // another type, so parse the value that a
453                    // `WriteVar<SideEffect>` would have written.
454                    serde_json::from_slice(b"null").expect("should be deserializing into ()")
455                } else {
456                    // This is a normal variable.
457                    serde_json::from_slice(&data).expect("improve this error path")
458                }
459            }
460            ReadVarBacking::Inline(val) => val,
461        }
462    }
463}
464
465impl<T: ClaimVar> ClaimVar for Vec<T> {
466    type Claimed = Vec<T::Claimed>;
467
468    fn claim(self, ctx: &mut StepCtx<'_>) -> Vec<T::Claimed> {
469        self.into_iter().map(|v| v.claim(ctx)).collect()
470    }
471}
472
473impl<T: ReadVarValue> ReadVarValue for Vec<T> {
474    type Value = Vec<T::Value>;
475
476    fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
477        self.into_iter().map(|v| v.read_value(rt)).collect()
478    }
479}
480
481impl<T: ClaimVar> ClaimVar for Option<T> {
482    type Claimed = Option<T::Claimed>;
483
484    fn claim(self, ctx: &mut StepCtx<'_>) -> Option<T::Claimed> {
485        self.map(|x| x.claim(ctx))
486    }
487}
488
489impl<T: ReadVarValue> ReadVarValue for Option<T> {
490    type Value = Option<T::Value>;
491
492    fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
493        self.map(|x| x.read_value(rt))
494    }
495}
496
497impl<U: Ord, T: ClaimVar> ClaimVar for BTreeMap<U, T> {
498    type Claimed = BTreeMap<U, T::Claimed>;
499
500    fn claim(self, ctx: &mut StepCtx<'_>) -> BTreeMap<U, T::Claimed> {
501        self.into_iter().map(|(k, v)| (k, v.claim(ctx))).collect()
502    }
503}
504
505impl<U: Ord, T: ReadVarValue> ReadVarValue for BTreeMap<U, T> {
506    type Value = BTreeMap<U, T::Value>;
507
508    fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
509        self.into_iter()
510            .map(|(k, v)| (k, v.read_value(rt)))
511            .collect()
512    }
513}
514
515macro_rules! impl_tuple_claim {
516    ($($T:tt)*) => {
517        impl<$($T,)*> $crate::node::ClaimVar for ($($T,)*)
518        where
519            $($T: $crate::node::ClaimVar,)*
520        {
521            type Claimed = ($($T::Claimed,)*);
522
523            #[expect(non_snake_case)]
524            fn claim(self, ctx: &mut $crate::node::StepCtx<'_>) -> Self::Claimed {
525                let ($($T,)*) = self;
526                ($($T.claim(ctx),)*)
527            }
528        }
529
530        impl<$($T,)*> $crate::node::ReadVarValue for ($($T,)*)
531        where
532            $($T: $crate::node::ReadVarValue,)*
533        {
534            type Value = ($($T::Value,)*);
535
536            #[expect(non_snake_case)]
537            fn read_value(self, rt: &mut $crate::node::RustRuntimeServices<'_>) -> Self::Value {
538                let ($($T,)*) = self;
539                ($($T.read_value(rt),)*)
540            }
541        }
542    };
543}
544
545impl_tuple_claim!(A B C D E F G H I J);
546impl_tuple_claim!(A B C D E F G H I);
547impl_tuple_claim!(A B C D E F G H);
548impl_tuple_claim!(A B C D E F G);
549impl_tuple_claim!(A B C D E F);
550impl_tuple_claim!(A B C D E);
551impl_tuple_claim!(A B C D);
552impl_tuple_claim!(A B C);
553impl_tuple_claim!(A B);
554impl_tuple_claim!(A);
555
556impl ClaimVar for () {
557    type Claimed = ();
558
559    fn claim(self, _ctx: &mut StepCtx<'_>) -> Self::Claimed {}
560}
561
562impl ReadVarValue for () {
563    type Value = ();
564
565    fn read_value(self, _rt: &mut RustRuntimeServices<'_>) -> Self::Value {}
566}
567
568/// Read a custom, user-defined secret by passing in the secret name.
569///
570/// Intended usage is to get a secret using the [`crate::pipeline::Pipeline::gh_use_secret`] API
571/// and to use the returned value through the [`NodeCtx::get_gh_context_var`] API.
572#[derive(Serialize, Deserialize, Clone)]
573pub struct GhUserSecretVar(pub(crate) String);
574
575/// Read a value from a flowey Var at runtime, returning the value written by
576/// the Var's corresponding [`WriteVar`].
577///
578/// Vars in flowey must be serde de/serializable, in order to be de/serialized
579/// between multiple steps/nodes.
580///
581/// In order to read the value contained within a `ReadVar`, it must first be
582/// _claimed_ by a particular step (using the [`ClaimVar::claim`] API). Once
583/// claimed, the Var can be read using APIs such as
584/// [`RustRuntimeServices::read`], or [`AdoStepServices::get_var`]
585///
586/// Note that all `ReadVar`s in flowey are _immutable_. In other words:
587/// reading the value of a `ReadVar` multiple times from multiple nodes will
588/// _always_ return the same value.
589///
590/// This is a natural consequence `ReadVar` obtaining its value from the result
591/// of a write into [`WriteVar`], whose API enforces that there can only ever be
592/// a single Write to a `WriteVar`.
593#[derive(Debug, Serialize, Deserialize)]
594pub struct ReadVar<T, C = VarNotClaimed> {
595    backing_var: ReadVarBacking<T>,
596    #[serde(skip)]
597    _kind: std::marker::PhantomData<C>,
598}
599
600/// A [`ReadVar`] which has been claimed by a particular step, allowing it to
601/// be read at runtime.
602pub type ClaimedReadVar<T> = ReadVar<T, VarClaimed>;
603
604// cloning is fine, since you can totally have multiple dependents
605impl<T: Serialize + DeserializeOwned, C> Clone for ReadVar<T, C> {
606    fn clone(&self) -> Self {
607        ReadVar {
608            backing_var: self.backing_var.clone(),
609            _kind: std::marker::PhantomData,
610        }
611    }
612}
613
614#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
615enum ReadVarBacking<T> {
616    RuntimeVar {
617        var: String,
618        /// If true, then don't try to parse this variable--it was converted
619        /// into a side effect (of type `()`) from another type, so the
620        /// serialization will not match.
621        ///
622        /// If false, it may still be a "side effect" variable, but type `T`
623        /// matches its serialization.
624        is_side_effect: bool,
625    },
626    Inline(T),
627}
628
629// avoid requiring types to include an explicit clone bound
630impl<T: Serialize + DeserializeOwned> Clone for ReadVarBacking<T> {
631    fn clone(&self) -> Self {
632        match self {
633            Self::RuntimeVar {
634                var,
635                is_side_effect,
636            } => Self::RuntimeVar {
637                var: var.clone(),
638                is_side_effect: *is_side_effect,
639            },
640            Self::Inline(v) => {
641                Self::Inline(serde_json::from_value(serde_json::to_value(v).unwrap()).unwrap())
642            }
643        }
644    }
645}
646
647impl<T: Serialize + DeserializeOwned> ReadVar<T> {
648    /// (Internal API) Switch the claim marker to "claimed".
649    fn into_claimed(self) -> ReadVar<T, VarClaimed> {
650        let Self { backing_var, _kind } = self;
651
652        ReadVar {
653            backing_var,
654            _kind: std::marker::PhantomData,
655        }
656    }
657
658    /// Discard any type information associated with the Var, and treat the Var
659    /// as through it was only a side effect.
660    ///
661    /// e.g: if a Node returns a `ReadVar<PathBuf>`, but you know that the mere
662    /// act of having _run_ the node has ensured the file is placed in a "magic
663    /// location" for some other node, then it may be useful to treat the
664    /// `ReadVar<PathBuf>` as a simple `ReadVar<SideEffect>`, which can be
665    /// passed along as part of a larger bundle of `Vec<ReadVar<SideEffect>>`.
666    #[must_use]
667    pub fn into_side_effect(self) -> ReadVar<SideEffect> {
668        ReadVar {
669            backing_var: match self.backing_var {
670                ReadVarBacking::RuntimeVar {
671                    var,
672                    is_side_effect: _,
673                } => ReadVarBacking::RuntimeVar {
674                    var,
675                    is_side_effect: true,
676                },
677                ReadVarBacking::Inline(_) => ReadVarBacking::Inline(()),
678            },
679            _kind: std::marker::PhantomData,
680        }
681    }
682
683    /// Maps a `ReadVar<T>` to a new `ReadVar<U>`, by applying a function to the
684    /// Var at runtime.
685    #[track_caller]
686    #[must_use]
687    pub fn map<F, U>(&self, ctx: &mut NodeCtx<'_>, f: F) -> ReadVar<U>
688    where
689        T: 'static,
690        U: Serialize + DeserializeOwned + 'static,
691        F: FnOnce(T) -> U + 'static,
692    {
693        let (read_from, write_into) = ctx.new_var();
694        self.write_into_with(ctx, write_into, f);
695        read_from
696    }
697
698    /// Maps a `ReadVar<T>` into an existing `WriteVar<U>` by applying a
699    /// function to the Var at runtime.
700    #[track_caller]
701    pub fn write_into_with<F, U>(&self, ctx: &mut NodeCtx<'_>, write_into: WriteVar<U>, f: F)
702    where
703        T: 'static,
704        U: Serialize + DeserializeOwned + 'static,
705        F: FnOnce(T) -> U + 'static,
706    {
707        let this = self.clone();
708        ctx.emit_minor_rust_step("🌼 write_into Var", move |ctx| {
709            let this = this.claim(ctx);
710            let write_into = write_into.claim(ctx);
711            move |rt| {
712                let this = rt.read(this);
713                rt.write(write_into, &f(this));
714            }
715        });
716    }
717
718    /// Maps a `ReadVar<T>` into an existing `WriteVar<U>`
719    #[track_caller]
720    pub fn write_into(&self, ctx: &mut NodeCtx<'_>, write_into: WriteVar<T>)
721    where
722        T: 'static,
723    {
724        self.write_into_with(ctx, write_into, |x| x);
725    }
726
727    /// Zips self (`ReadVar<T>`) with another `ReadVar<U>`, returning a new
728    /// `ReadVar<(T, U)>`
729    #[track_caller]
730    #[must_use]
731    pub fn zip<U>(&self, ctx: &mut NodeCtx<'_>, other: ReadVar<U>) -> ReadVar<(T, U)>
732    where
733        T: 'static,
734        U: Serialize + DeserializeOwned + 'static,
735    {
736        let (read_from, write_into) = ctx.new_var();
737        let this = self.clone();
738        ctx.emit_minor_rust_step("🌼 Zip Vars", move |ctx| {
739            let this = this.claim(ctx);
740            let other = other.claim(ctx);
741            let write_into = write_into.claim(ctx);
742            move |rt| {
743                let this = rt.read(this);
744                let other = rt.read(other);
745                rt.write(write_into, &(this, other));
746            }
747        });
748        read_from
749    }
750
751    /// Create a new `ReadVar` from a static value.
752    ///
753    /// **WARNING:** Static values **CANNOT BE SECRETS**, as they are encoded as
754    /// plain-text in the output flow.
755    #[track_caller]
756    #[must_use]
757    pub fn from_static(val: T) -> ReadVar<T>
758    where
759        T: 'static,
760    {
761        ReadVar {
762            backing_var: ReadVarBacking::Inline(val),
763            _kind: std::marker::PhantomData,
764        }
765    }
766
767    /// If this [`ReadVar`] contains a static value, return it.
768    ///
769    /// Nodes can opt-in to using this method as a way to generate optimized
770    /// steps in cases where the value of a variable is known ahead of time.
771    ///
772    /// e.g: a node doing a git checkout could leverage this method to decide
773    /// whether its ADO backend should emit a conditional step for checking out
774    /// a repo, or if it can statically include / exclude the checkout request.
775    pub fn get_static(&self) -> Option<T> {
776        match self.clone().backing_var {
777            ReadVarBacking::Inline(v) => Some(v),
778            _ => None,
779        }
780    }
781
782    /// Transpose a `Vec<ReadVar<T>>` into a `ReadVar<Vec<T>>`
783    #[track_caller]
784    #[must_use]
785    pub fn transpose_vec(ctx: &mut NodeCtx<'_>, vec: Vec<ReadVar<T>>) -> ReadVar<Vec<T>>
786    where
787        T: 'static,
788    {
789        let (read_from, write_into) = ctx.new_var();
790        ctx.emit_minor_rust_step("🌼 Transpose Vec<ReadVar<T>>", move |ctx| {
791            let vec = vec.claim(ctx);
792            let write_into = write_into.claim(ctx);
793            move |rt| {
794                let mut v = Vec::new();
795                for var in vec {
796                    v.push(rt.read(var));
797                }
798                rt.write(write_into, &v);
799            }
800        });
801        read_from
802    }
803
804    /// Returns a new instance of this variable with an artificial dependency on
805    /// `other`.
806    ///
807    /// This is useful for making explicit a non-explicit dependency between the
808    /// two variables. For example, if `self` contains a path to a file, and
809    /// `other` is only written once that file has been created, then this
810    /// method can be used to return a new `ReadVar` which depends on `other`
811    /// but is otherwise identical to `self`. This ensures that when the new
812    /// variable is read, the file has been created.
813    ///
814    /// In general, it is better to ensure that the dependency is explicit, so
815    /// that if you have a variable with a path, then you know that the file
816    /// exists when you read it. This method is useful in cases where this is
817    /// not naturally the case, e.g., when you are providing a path as part of a
818    /// request, as opposed to the path being returned to you.
819    #[must_use]
820    pub fn depending_on<U>(&self, ctx: &mut NodeCtx<'_>, other: &ReadVar<U>) -> Self
821    where
822        T: 'static,
823        U: Serialize + DeserializeOwned + 'static,
824    {
825        // This could probably be handled without an additional Rust step with some
826        // additional work in the backend, but this is simple enough for now.
827        ctx.emit_minor_rust_stepv("🌼 Add dependency", |ctx| {
828            let this = self.clone().claim(ctx);
829            other.clone().claim(ctx);
830            move |rt| rt.read(this)
831        })
832    }
833
834    /// Consume this `ReadVar` outside the context of a step, signalling that it
835    /// won't be used.
836    pub fn claim_unused(self, ctx: &mut NodeCtx<'_>) {
837        match self.backing_var {
838            ReadVarBacking::RuntimeVar {
839                var,
840                is_side_effect: _,
841            } => ctx.backend.borrow_mut().on_unused_read_var(&var),
842            ReadVarBacking::Inline(_) => {}
843        }
844    }
845
846    pub(crate) fn into_json(self) -> ReadVar<serde_json::Value> {
847        match self.backing_var {
848            ReadVarBacking::RuntimeVar {
849                var,
850                is_side_effect,
851            } => ReadVar {
852                backing_var: ReadVarBacking::RuntimeVar {
853                    var,
854                    is_side_effect,
855                },
856                _kind: std::marker::PhantomData,
857            },
858            ReadVarBacking::Inline(v) => ReadVar {
859                backing_var: ReadVarBacking::Inline(serde_json::to_value(v).unwrap()),
860                _kind: std::marker::PhantomData,
861            },
862        }
863    }
864}
865
866/// DANGER: obtain a handle to a [`ReadVar`] "out of thin air".
867///
868/// This should NEVER be used from within a flowey node. This is a sharp tool,
869/// and should only be used by code implementing flow / pipeline resolution
870/// logic.
871#[must_use]
872pub fn thin_air_read_runtime_var<T>(backing_var: String) -> ReadVar<T>
873where
874    T: Serialize + DeserializeOwned,
875{
876    ReadVar {
877        backing_var: ReadVarBacking::RuntimeVar {
878            var: backing_var,
879            is_side_effect: false,
880        },
881        _kind: std::marker::PhantomData,
882    }
883}
884
885/// DANGER: obtain a handle to a [`WriteVar`] "out of thin air".
886///
887/// This should NEVER be used from within a flowey node. This is a sharp tool,
888/// and should only be used by code implementing flow / pipeline resolution
889/// logic.
890#[must_use]
891pub fn thin_air_write_runtime_var<T>(backing_var: String) -> WriteVar<T>
892where
893    T: Serialize + DeserializeOwned,
894{
895    WriteVar {
896        backing_var,
897        is_side_effect: false,
898        _kind: std::marker::PhantomData,
899    }
900}
901
902/// DANGER: obtain a [`ReadVar`] backing variable and side effect status.
903///
904/// This should NEVER be used from within a flowey node. This relies on
905/// flowey variable implementation details, and should only be used by code
906/// implementing flow / pipeline resolution logic.
907pub fn read_var_internals<T: Serialize + DeserializeOwned, C>(
908    var: &ReadVar<T, C>,
909) -> (Option<String>, bool) {
910    match var.backing_var {
911        ReadVarBacking::RuntimeVar {
912            var: ref s,
913            is_side_effect,
914        } => (Some(s.clone()), is_side_effect),
915        ReadVarBacking::Inline(_) => (None, false),
916    }
917}
918
919pub trait ImportCtxBackend {
920    fn on_possible_dep(&mut self, node_handle: NodeHandle);
921}
922
923/// Context passed to [`FlowNode::imports`].
924pub struct ImportCtx<'a> {
925    backend: &'a mut dyn ImportCtxBackend,
926}
927
928impl ImportCtx<'_> {
929    /// Declare that a Node can be referenced in [`FlowNode::emit`]
930    pub fn import<N: FlowNodeBase + 'static>(&mut self) {
931        self.backend.on_possible_dep(NodeHandle::from_type::<N>())
932    }
933}
934
935pub fn new_import_ctx(backend: &mut dyn ImportCtxBackend) -> ImportCtx<'_> {
936    ImportCtx { backend }
937}
938
939pub trait NodeCtxBackend {
940    /// Handle to the current node this `ctx` corresponds to
941    fn current_node(&self) -> NodeHandle;
942
943    /// Return a string which uniquely identifies this particular Var
944    /// registration.
945    ///
946    /// Typically consists of `{current node handle}{ordinal}`
947    fn on_new_var(&mut self) -> String;
948
949    /// Invoked when a node claims a particular runtime variable
950    fn on_claimed_runtime_var(&mut self, var: &str, is_read: bool);
951
952    /// Invoked when a node marks a particular runtime variable as unused
953    fn on_unused_read_var(&mut self, var: &str);
954
955    /// Invoked when a node sets a request on a node.
956    ///
957    /// - `node_typeid` will always correspond to a node that was previously
958    ///   passed to `on_register`.
959    /// - `req` may be an error, in the case where the NodeCtx failed to
960    ///   serialize the provided request.
961    // FIXME: this should be using type-erased serde
962    fn on_request(&mut self, node_handle: NodeHandle, req: anyhow::Result<Box<[u8]>>);
963
964    /// Invoked when a node sets config on another node.
965    ///
966    /// Config is merged by the resolver and delivered before action requests.
967    fn on_config(&mut self, node_handle: NodeHandle, config: anyhow::Result<Box<[u8]>>);
968
969    fn on_emit_rust_step(
970        &mut self,
971        label: &str,
972        can_merge: bool,
973        code: Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>>,
974    );
975
976    fn on_emit_ado_step(
977        &mut self,
978        label: &str,
979        yaml_snippet: Box<dyn for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String>,
980        inline_script: Option<
981            Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>>,
982        >,
983        condvar: Option<String>,
984    );
985
986    fn on_emit_gh_step(
987        &mut self,
988        label: &str,
989        uses: &str,
990        with: BTreeMap<String, ClaimedGhParam>,
991        condvar: Option<String>,
992        outputs: BTreeMap<String, Vec<GhOutput>>,
993        permissions: BTreeMap<GhPermission, GhPermissionValue>,
994        gh_to_rust: Vec<GhToRust>,
995        rust_to_gh: Vec<RustToGh>,
996    );
997
998    fn on_emit_side_effect_step(&mut self);
999
1000    fn backend(&mut self) -> FlowBackend;
1001    fn platform(&mut self) -> FlowPlatform;
1002    fn arch(&mut self) -> FlowArch;
1003
1004    /// Return a node-specific persistent store path. The backend does not need
1005    /// to ensure that the path exists - flowey will automatically emit a step
1006    /// to construct the directory at runtime.
1007    fn persistent_dir_path_var(&mut self) -> Option<String>;
1008}
1009
1010pub fn new_node_ctx(backend: &mut dyn NodeCtxBackend) -> NodeCtx<'_> {
1011    NodeCtx {
1012        backend: Rc::new(RefCell::new(backend)),
1013    }
1014}
1015
1016/// What backend the flow is being running on.
1017#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1018pub enum FlowBackend {
1019    /// Running locally.
1020    Local,
1021    /// Running on ADO.
1022    Ado,
1023    /// Running on GitHub Actions
1024    Github,
1025}
1026
1027/// The kind platform the flow is being running on, Windows or Unix.
1028#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1029pub enum FlowPlatformKind {
1030    Windows,
1031    Unix,
1032}
1033
1034/// The kind platform the flow is being running on, Windows or Unix.
1035#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1036pub enum FlowPlatformLinuxDistro {
1037    /// Fedora (including WSL2)
1038    Fedora,
1039    /// Ubuntu (including WSL2)
1040    Ubuntu,
1041    /// Azure Linux (tdnf-based)
1042    AzureLinux,
1043    /// Arch Linux (including WSL2)
1044    Arch,
1045    /// Nix environment (detected via IN_NIX_SHELL env var or having a `/nix/store` in PATH)
1046    Nix,
1047    /// An unknown distribution
1048    Unknown,
1049}
1050
1051/// What platform the flow is being running on.
1052#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1053#[non_exhaustive]
1054pub enum FlowPlatform {
1055    /// Windows
1056    Windows,
1057    /// Linux (including WSL2)
1058    Linux(FlowPlatformLinuxDistro),
1059    /// macOS
1060    MacOs,
1061}
1062
1063impl FlowPlatform {
1064    pub fn kind(&self) -> FlowPlatformKind {
1065        match self {
1066            Self::Windows => FlowPlatformKind::Windows,
1067            Self::Linux(_) | Self::MacOs => FlowPlatformKind::Unix,
1068        }
1069    }
1070
1071    fn as_str(&self) -> &'static str {
1072        match self {
1073            Self::Windows => "windows",
1074            Self::Linux(_) => "linux",
1075            Self::MacOs => "macos",
1076        }
1077    }
1078
1079    /// The suffix to use for executables on this platform.
1080    pub fn exe_suffix(&self) -> &'static str {
1081        if self == &Self::Windows { ".exe" } else { "" }
1082    }
1083
1084    /// The full name for a binary on this platform (i.e. `name + self.exe_suffix()`).
1085    pub fn binary(&self, name: &str) -> String {
1086        format!("{}{}", name, self.exe_suffix())
1087    }
1088}
1089
1090impl std::fmt::Display for FlowPlatform {
1091    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1092        f.pad(self.as_str())
1093    }
1094}
1095
1096/// What architecture the flow is being running on.
1097#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1098#[non_exhaustive]
1099pub enum FlowArch {
1100    X86_64,
1101    Aarch64,
1102}
1103
1104impl FlowArch {
1105    fn as_str(&self) -> &'static str {
1106        match self {
1107            Self::X86_64 => "x86_64",
1108            Self::Aarch64 => "aarch64",
1109        }
1110    }
1111}
1112
1113impl std::fmt::Display for FlowArch {
1114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1115        f.pad(self.as_str())
1116    }
1117}
1118
1119/// Context object for an individual step.
1120pub struct StepCtx<'a> {
1121    backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
1122}
1123
1124impl StepCtx<'_> {
1125    /// What backend the flow is being running on (e.g: locally, ADO, GitHub,
1126    /// etc...)
1127    pub fn backend(&self) -> FlowBackend {
1128        self.backend.borrow_mut().backend()
1129    }
1130
1131    /// What platform the flow is being running on (e.g: windows, linux, wsl2,
1132    /// etc...).
1133    pub fn platform(&self) -> FlowPlatform {
1134        self.backend.borrow_mut().platform()
1135    }
1136}
1137
1138const NO_ADO_INLINE_SCRIPT: Option<
1139    for<'a> fn(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>,
1140> = None;
1141
1142/// Context object for a `FlowNode`.
1143pub struct NodeCtx<'a> {
1144    backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
1145}
1146
1147impl<'ctx> NodeCtx<'ctx> {
1148    /// Emit a Rust-based step.
1149    ///
1150    /// As a convenience feature, this function returns a special _optional_
1151    /// [`ReadVar<SideEffect>`], which will not result in a "unused variable"
1152    /// error if no subsequent step ends up claiming it.
1153    pub fn emit_rust_step<F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<SideEffect>
1154    where
1155        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1156        G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1157    {
1158        self.emit_rust_step_inner(label.as_ref(), false, code)
1159    }
1160
1161    /// Emit a Rust-based step that cannot fail.
1162    ///
1163    /// This is equivalent to [`NodeCtx::emit_rust_step`], but it is for steps that cannot
1164    /// fail and that do not need to be emitted as a separate step in a YAML
1165    /// pipeline. This simplifies the pipeline logs.
1166    pub fn emit_minor_rust_step<F, G>(
1167        &mut self,
1168        label: impl AsRef<str>,
1169        code: F,
1170    ) -> ReadVar<SideEffect>
1171    where
1172        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1173        G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) + 'static,
1174    {
1175        self.emit_rust_step_inner(label.as_ref(), true, |ctx| {
1176            let f = code(ctx);
1177            |rt| {
1178                f(rt);
1179                Ok(())
1180            }
1181        })
1182    }
1183
1184    /// Emit a Rust-based step, creating a new `ReadVar<T>` from the step's
1185    /// return value.
1186    ///
1187    /// This is a convenience function that streamlines the following common
1188    /// flowey pattern:
1189    ///
1190    /// ```ignore
1191    /// // creating a new Var explicitly
1192    /// let (read_foo, write_foo) = ctx.new_var();
1193    /// ctx.emit_rust_step("foo", |ctx| {
1194    ///     let write_foo = write_foo.claim(ctx);
1195    ///     |rt| {
1196    ///         rt.write(write_foo, &get_foo());
1197    ///         Ok(())
1198    ///     }
1199    /// });
1200    ///
1201    /// // creating a new Var automatically
1202    /// let read_foo = ctx.emit_rust_stepv("foo", |ctx| |rt| Ok(get_foo()));
1203    /// ```
1204    #[must_use]
1205    #[track_caller]
1206    pub fn emit_rust_stepv<T, F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<T>
1207    where
1208        T: Serialize + DeserializeOwned + 'static,
1209        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1210        G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<T> + 'static,
1211    {
1212        self.emit_rust_stepv_inner(label.as_ref(), false, code)
1213    }
1214
1215    /// Emit a Rust-based step, creating a new `ReadVar<T>` from the step's
1216    /// return value.
1217    ///
1218    /// This is equivalent to `emit_rust_stepv`, but it is for steps that cannot
1219    /// fail and that do not need to be emitted as a separate step in a YAML
1220    /// pipeline. This simplifies the pipeline logs.
1221    ///
1222    /// This is a convenience function that streamlines the following common
1223    /// flowey pattern:
1224    ///
1225    /// ```ignore
1226    /// // creating a new Var explicitly
1227    /// let (read_foo, write_foo) = ctx.new_var();
1228    /// ctx.emit_minor_rust_step("foo", |ctx| {
1229    ///     let write_foo = write_foo.claim(ctx);
1230    ///     |rt| {
1231    ///         rt.write(write_foo, &get_foo());
1232    ///     }
1233    /// });
1234    ///
1235    /// // creating a new Var automatically
1236    /// let read_foo = ctx.emit_minor_rust_stepv("foo", |ctx| |rt| get_foo());
1237    /// ```
1238    #[must_use]
1239    #[track_caller]
1240    pub fn emit_minor_rust_stepv<T, F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<T>
1241    where
1242        T: Serialize + DeserializeOwned + 'static,
1243        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1244        G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> T + 'static,
1245    {
1246        self.emit_rust_stepv_inner(label.as_ref(), true, |ctx| {
1247            let f = code(ctx);
1248            |rt| Ok(f(rt))
1249        })
1250    }
1251
1252    fn emit_rust_step_inner<F, G>(
1253        &mut self,
1254        label: &str,
1255        can_merge: bool,
1256        code: F,
1257    ) -> ReadVar<SideEffect>
1258    where
1259        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1260        G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1261    {
1262        let (read, write) = self.new_prefixed_var("auto_se");
1263
1264        let ctx = &mut StepCtx {
1265            backend: self.backend.clone(),
1266        };
1267        write.claim(ctx);
1268
1269        let code = code(ctx);
1270        self.backend
1271            .borrow_mut()
1272            .on_emit_rust_step(label.as_ref(), can_merge, Box::new(code));
1273        read
1274    }
1275
1276    #[must_use]
1277    #[track_caller]
1278    fn emit_rust_stepv_inner<T, F, G>(
1279        &mut self,
1280        label: impl AsRef<str>,
1281        can_merge: bool,
1282        code: F,
1283    ) -> ReadVar<T>
1284    where
1285        T: Serialize + DeserializeOwned + 'static,
1286        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1287        G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<T> + 'static,
1288    {
1289        let (read, write) = self.new_var();
1290
1291        let ctx = &mut StepCtx {
1292            backend: self.backend.clone(),
1293        };
1294        let write = write.claim(ctx);
1295
1296        let code = code(ctx);
1297        self.backend.borrow_mut().on_emit_rust_step(
1298            label.as_ref(),
1299            can_merge,
1300            Box::new(|rt| {
1301                let val = code(rt)?;
1302                rt.write(write, &val);
1303                Ok(())
1304            }),
1305        );
1306        read
1307    }
1308
1309    /// Load an ADO global runtime variable into a flowey [`ReadVar`].
1310    #[track_caller]
1311    #[must_use]
1312    pub fn get_ado_variable(&mut self, ado_var: AdoRuntimeVar) -> ReadVar<String> {
1313        let (var, write_var) = self.new_var();
1314        self.emit_ado_step(format!("🌼 read {}", ado_var.as_raw_var_name()), |ctx| {
1315            let write_var = write_var.claim(ctx);
1316            |rt| {
1317                rt.set_var(write_var, ado_var);
1318                "".into()
1319            }
1320        });
1321        var
1322    }
1323
1324    /// Emit an ADO step.
1325    pub fn emit_ado_step<F, G>(&mut self, display_name: impl AsRef<str>, yaml_snippet: F)
1326    where
1327        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1328        G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1329    {
1330        self.emit_ado_step_inner(display_name, None, |ctx| {
1331            (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1332        })
1333    }
1334
1335    /// Emit an ADO step, conditionally executed based on the value of `cond` at
1336    /// runtime.
1337    pub fn emit_ado_step_with_condition<F, G>(
1338        &mut self,
1339        display_name: impl AsRef<str>,
1340        cond: ReadVar<bool>,
1341        yaml_snippet: F,
1342    ) where
1343        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1344        G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1345    {
1346        self.emit_ado_step_inner(display_name, Some(cond), |ctx| {
1347            (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1348        })
1349    }
1350
1351    /// Emit an ADO step, conditionally executed based on the value of`cond` at
1352    /// runtime.
1353    pub fn emit_ado_step_with_condition_optional<F, G>(
1354        &mut self,
1355        display_name: impl AsRef<str>,
1356        cond: Option<ReadVar<bool>>,
1357        yaml_snippet: F,
1358    ) where
1359        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1360        G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1361    {
1362        self.emit_ado_step_inner(display_name, cond, |ctx| {
1363            (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1364        })
1365    }
1366
1367    /// Emit an ADO step which invokes a rust callback using an inline script.
1368    ///
1369    /// By using the `{{FLOWEY_INLINE_SCRIPT}}` template in the returned yaml
1370    /// snippet, flowey will interpolate a command ~roughly akin to `flowey
1371    /// exec-snippet <rust-snippet-id>` into the generated yaml.
1372    ///
1373    /// e.g: if we wanted to _manually_ wrap the bash ADO snippet for whatever
1374    /// reason:
1375    ///
1376    /// ```text
1377    /// - bash: |
1378    ///     echo "hello there!"
1379    ///     {{FLOWEY_INLINE_SCRIPT}}
1380    ///     echo echo "bye!"
1381    /// ```
1382    ///
1383    /// # Limitations
1384    ///
1385    /// At the moment, due to flowey API limitations, it is only possible to
1386    /// embed a single inline script into a YAML step.
1387    ///
1388    /// In the future, rather than having separate methods for "emit step with X
1389    /// inline scripts", flowey should support declaring "first-class" callbacks
1390    /// via a (hypothetical) `ctx.new_callback_var(|ctx| |rt, input: Input| ->
1391    /// Output { ... })` API, at which point.
1392    ///
1393    /// If such an API were to exist, one could simply use the "vanilla" emit
1394    /// yaml step functions with these first-class callbacks.
1395    pub fn emit_ado_step_with_inline_script<F, G, H>(
1396        &mut self,
1397        display_name: impl AsRef<str>,
1398        yaml_snippet: F,
1399    ) where
1400        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> (G, H),
1401        G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1402        H: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1403    {
1404        self.emit_ado_step_inner(display_name, None, |ctx| {
1405            let (f, g) = yaml_snippet(ctx);
1406            (f, Some(g))
1407        })
1408    }
1409
1410    fn emit_ado_step_inner<F, G, H>(
1411        &mut self,
1412        display_name: impl AsRef<str>,
1413        cond: Option<ReadVar<bool>>,
1414        yaml_snippet: F,
1415    ) where
1416        F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> (G, Option<H>),
1417        G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1418        H: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1419    {
1420        let condvar = match cond.map(|c| c.backing_var) {
1421            // it seems silly to allow this... but it's not hard so why not?
1422            Some(ReadVarBacking::Inline(cond)) => {
1423                if !cond {
1424                    return;
1425                } else {
1426                    None
1427                }
1428            }
1429            Some(ReadVarBacking::RuntimeVar {
1430                var,
1431                is_side_effect,
1432            }) => {
1433                assert!(!is_side_effect);
1434                self.backend.borrow_mut().on_claimed_runtime_var(&var, true);
1435                Some(var)
1436            }
1437            None => None,
1438        };
1439
1440        let (yaml_snippet, inline_script) = yaml_snippet(&mut StepCtx {
1441            backend: self.backend.clone(),
1442        });
1443        self.backend.borrow_mut().on_emit_ado_step(
1444            display_name.as_ref(),
1445            Box::new(yaml_snippet),
1446            if let Some(inline_script) = inline_script {
1447                Some(Box::new(inline_script))
1448            } else {
1449                None
1450            },
1451            condvar,
1452        );
1453    }
1454
1455    /// Load a GitHub context variable into a flowey [`ReadVar`].
1456    #[track_caller]
1457    #[must_use]
1458    pub fn get_gh_context_var(&mut self) -> GhContextVarReader<'ctx, Root> {
1459        GhContextVarReader {
1460            ctx: NodeCtx {
1461                backend: self.backend.clone(),
1462            },
1463            _state: std::marker::PhantomData,
1464        }
1465    }
1466
1467    /// Emit a GitHub Actions action step.
1468    pub fn emit_gh_step(
1469        &mut self,
1470        display_name: impl AsRef<str>,
1471        uses: impl AsRef<str>,
1472    ) -> GhStepBuilder {
1473        GhStepBuilder::new(display_name, uses)
1474    }
1475
1476    fn emit_gh_step_inner(
1477        &mut self,
1478        display_name: impl AsRef<str>,
1479        cond: Option<ReadVar<bool>>,
1480        uses: impl AsRef<str>,
1481        with: Option<BTreeMap<String, GhParam>>,
1482        outputs: BTreeMap<String, Vec<WriteVar<String>>>,
1483        run_after: Vec<ReadVar<SideEffect>>,
1484        permissions: BTreeMap<GhPermission, GhPermissionValue>,
1485    ) {
1486        let condvar = match cond.map(|c| c.backing_var) {
1487            // it seems silly to allow this... but it's not hard so why not?
1488            Some(ReadVarBacking::Inline(cond)) => {
1489                if !cond {
1490                    return;
1491                } else {
1492                    None
1493                }
1494            }
1495            Some(ReadVarBacking::RuntimeVar {
1496                var,
1497                is_side_effect,
1498            }) => {
1499                assert!(!is_side_effect);
1500                self.backend.borrow_mut().on_claimed_runtime_var(&var, true);
1501                Some(var)
1502            }
1503            None => None,
1504        };
1505
1506        let with = with
1507            .unwrap_or_default()
1508            .into_iter()
1509            .map(|(k, v)| {
1510                (
1511                    k.clone(),
1512                    v.claim(&mut StepCtx {
1513                        backend: self.backend.clone(),
1514                    }),
1515                )
1516            })
1517            .collect();
1518
1519        for var in run_after {
1520            var.claim(&mut StepCtx {
1521                backend: self.backend.clone(),
1522            });
1523        }
1524
1525        let outputvars = outputs
1526            .into_iter()
1527            .map(|(name, vars)| {
1528                (
1529                    name,
1530                    vars.into_iter()
1531                        .map(|var| {
1532                            let var = var.claim(&mut StepCtx {
1533                                backend: self.backend.clone(),
1534                            });
1535                            GhOutput {
1536                                backing_var: var.backing_var,
1537                                is_secret: false,
1538                                is_object: false,
1539                            }
1540                        })
1541                        .collect(),
1542                )
1543            })
1544            .collect();
1545
1546        self.backend.borrow_mut().on_emit_gh_step(
1547            display_name.as_ref(),
1548            uses.as_ref(),
1549            with,
1550            condvar,
1551            outputvars,
1552            permissions,
1553            Vec::new(),
1554            Vec::new(),
1555        );
1556    }
1557
1558    /// Emit a "side-effect" step, which simply claims a set of side-effects in
1559    /// order to resolve another set of side effects.
1560    ///
1561    /// The same functionality could be achieved (less efficiently) by emitting
1562    /// a Rust step (or ADO step, or github step, etc...) that claims both sets
1563    /// of side-effects, and then does nothing. By using this method - flowey is
1564    /// able to avoid emitting that additional noop step at runtime.
1565    pub fn emit_side_effect_step(
1566        &mut self,
1567        use_side_effects: impl IntoIterator<Item = ReadVar<SideEffect>>,
1568        resolve_side_effects: impl IntoIterator<Item = WriteVar<SideEffect>>,
1569    ) {
1570        let mut backend = self.backend.borrow_mut();
1571        for var in use_side_effects.into_iter() {
1572            if let ReadVarBacking::RuntimeVar {
1573                var,
1574                is_side_effect: _,
1575            } = &var.backing_var
1576            {
1577                backend.on_claimed_runtime_var(var, true);
1578            }
1579        }
1580
1581        for var in resolve_side_effects.into_iter() {
1582            backend.on_claimed_runtime_var(&var.backing_var, false);
1583        }
1584
1585        backend.on_emit_side_effect_step();
1586    }
1587
1588    /// What backend the flow is being running on (e.g: locally, ADO, GitHub,
1589    /// etc...)
1590    pub fn backend(&self) -> FlowBackend {
1591        self.backend.borrow_mut().backend()
1592    }
1593
1594    /// What platform the flow is being running on (e.g: windows, linux, wsl2,
1595    /// etc...).
1596    pub fn platform(&self) -> FlowPlatform {
1597        self.backend.borrow_mut().platform()
1598    }
1599
1600    /// What architecture the flow is being running on (x86_64 or Aarch64)
1601    pub fn arch(&self) -> FlowArch {
1602        self.backend.borrow_mut().arch()
1603    }
1604
1605    /// Set a request on a particular node.
1606    pub fn req<R>(&mut self, req: R)
1607    where
1608        R: IntoRequest + 'static,
1609    {
1610        let mut backend = self.backend.borrow_mut();
1611        backend.on_request(
1612            NodeHandle::from_type::<R::Node>(),
1613            serde_json::to_vec(&req.into_request())
1614                .map(Into::into)
1615                .map_err(Into::into),
1616        );
1617    }
1618
1619    /// Set config on a particular node.
1620    ///
1621    /// Config is merged by the resolver (all callers must agree on values)
1622    /// and delivered to the target node before any action requests.
1623    pub fn config<C>(&mut self, config: C)
1624    where
1625        C: IntoConfig + 'static,
1626    {
1627        let mut backend = self.backend.borrow_mut();
1628        backend.on_config(
1629            NodeHandle::from_type::<C::Node>(),
1630            serde_json::to_vec(&config)
1631                .map(Into::into)
1632                .map_err(Into::into),
1633        );
1634    }
1635
1636    /// Set a request on a particular node, simultaneously creating a new flowey
1637    /// Var in the process.
1638    #[track_caller]
1639    #[must_use]
1640    pub fn reqv<T, R>(&mut self, f: impl FnOnce(WriteVar<T>) -> R) -> ReadVar<T>
1641    where
1642        T: Serialize + DeserializeOwned,
1643        R: IntoRequest + 'static,
1644    {
1645        let (read, write) = self.new_var();
1646        self.req::<R>(f(write));
1647        read
1648    }
1649
1650    /// Set multiple requests on a particular node.
1651    pub fn requests<N>(&mut self, reqs: impl IntoIterator<Item = N::Request>)
1652    where
1653        N: FlowNodeBase + 'static,
1654    {
1655        let mut backend = self.backend.borrow_mut();
1656        for req in reqs.into_iter() {
1657            backend.on_request(
1658                NodeHandle::from_type::<N>(),
1659                serde_json::to_vec(&req).map(Into::into).map_err(Into::into),
1660            );
1661        }
1662    }
1663
1664    /// Allocate a new flowey Var, returning two handles: one for reading the
1665    /// value, and another for writing the value.
1666    #[track_caller]
1667    #[must_use]
1668    pub fn new_var<T>(&self) -> (ReadVar<T>, WriteVar<T>)
1669    where
1670        T: Serialize + DeserializeOwned,
1671    {
1672        self.new_prefixed_var("")
1673    }
1674
1675    #[track_caller]
1676    #[must_use]
1677    fn new_prefixed_var<T>(&self, prefix: &'static str) -> (ReadVar<T>, WriteVar<T>)
1678    where
1679        T: Serialize + DeserializeOwned,
1680    {
1681        // normalize call path to ensure determinism between windows and linux
1682        //
1683        // Only the source file is kept, not line/column: `{modpath}:{ordinal}`
1684        // already uniquely identifies the var, so including line/column would
1685        // needlessly churn every generated pipeline whenever unrelated edits
1686        // shift line numbers.
1687        let caller = std::panic::Location::caller().file().replace('\\', "/");
1688
1689        // until we have a proper way to "split" debug info related to vars, we
1690        // kinda just lump it in with the var name itself.
1691        //
1692        // HACK: to work around cases where - depending on what the
1693        // current-working-dir is when incoking flowey - the returned
1694        // caller.file() path may leak the full path of the file (as opposed to
1695        // the relative path), resulting in inconsistencies between build
1696        // environments.
1697        //
1698        // For expediency, and to preserve some semblance of useful error
1699        // messages, we decided to play some sketchy games with the resulting
1700        // string to only preserve the _consistent_ bit of the path for a human
1701        // to use as reference.
1702        //
1703        // This is not ideal in the slightest, but it works OK for now
1704        let caller = caller
1705            .split_once("flowey/")
1706            .expect("due to a known limitation with flowey, all flowey code must have an ancestor dir called 'flowey/' somewhere in its full path")
1707            .1;
1708
1709        let colon = if prefix.is_empty() { "" } else { ":" };
1710        let ordinal = self.backend.borrow_mut().on_new_var();
1711        let backing_var = format!("{prefix}{colon}{ordinal}:{caller}");
1712
1713        (
1714            ReadVar {
1715                backing_var: ReadVarBacking::RuntimeVar {
1716                    var: backing_var.clone(),
1717                    is_side_effect: false,
1718                },
1719                _kind: std::marker::PhantomData,
1720            },
1721            WriteVar {
1722                backing_var,
1723                is_side_effect: false,
1724                _kind: std::marker::PhantomData,
1725            },
1726        )
1727    }
1728
1729    /// Allocate special [`SideEffect`] var which can be used to schedule a
1730    /// "post-job" step associated with some existing step.
1731    ///
1732    /// This "post-job" step will then only run after all other regular steps
1733    /// have run (i.e: steps required to complete any top-level objectives
1734    /// passed in via [`crate::pipeline::PipelineJob::dep_on`]). This makes it
1735    /// useful for implementing various "cleanup" or "finalize" tasks.
1736    ///
1737    /// e.g: the Cache node uses this to upload the contents of a cache
1738    /// directory at the end of a Job.
1739    #[track_caller]
1740    #[must_use]
1741    pub fn new_post_job_side_effect(&self) -> (ReadVar<SideEffect>, WriteVar<SideEffect>) {
1742        self.new_prefixed_var("post_job")
1743    }
1744
1745    /// Return a flowey Var pointing to a **node-specific** directory which
1746    /// will be persisted between runs, if such a directory is available.
1747    ///
1748    /// WARNING: this method is _very likely_ to return None when running on CI
1749    /// machines, as most CI agents are wiped between jobs!
1750    ///
1751    /// As such, it is NOT recommended that node authors reach for this method
1752    /// directly, and instead use abstractions such as the
1753    /// `flowey_lib_common::cache` Node, which implements node-level persistence
1754    /// in a way that works _regardless_ if a persistent_dir is available (e.g:
1755    /// by falling back to uploading / downloading artifacts to a "cache store"
1756    /// on platforms like ADO or Github Actions).
1757    #[track_caller]
1758    #[must_use]
1759    pub fn persistent_dir(&mut self) -> Option<ReadVar<PathBuf>> {
1760        let path: ReadVar<PathBuf> = ReadVar {
1761            backing_var: ReadVarBacking::RuntimeVar {
1762                var: self.backend.borrow_mut().persistent_dir_path_var()?,
1763                is_side_effect: false,
1764            },
1765            _kind: std::marker::PhantomData,
1766        };
1767
1768        let folder_name = self
1769            .backend
1770            .borrow_mut()
1771            .current_node()
1772            .modpath()
1773            .replace("::", "__");
1774
1775        Some(
1776            self.emit_rust_stepv("🌼 Create persistent store dir", |ctx| {
1777                let path = path.claim(ctx);
1778                |rt| {
1779                    let dir = rt.read(path).join(folder_name);
1780                    fs_err::create_dir_all(&dir)?;
1781                    Ok(dir)
1782                }
1783            }),
1784        )
1785    }
1786
1787    /// Check to see if a persistent dir is available, without yet creating it.
1788    pub fn supports_persistent_dir(&mut self) -> bool {
1789        self.backend
1790            .borrow_mut()
1791            .persistent_dir_path_var()
1792            .is_some()
1793    }
1794}
1795
1796// FUTURE: explore using type-erased serde here, instead of relying on
1797// `serde_json` in `flowey_core`.
1798pub trait RuntimeVarDb {
1799    fn get_var(&mut self, var_name: &str) -> (Vec<u8>, bool) {
1800        self.try_get_var(var_name)
1801            .unwrap_or_else(|| panic!("db is missing var {}", var_name))
1802    }
1803
1804    fn try_get_var(&mut self, var_name: &str) -> Option<(Vec<u8>, bool)>;
1805    fn set_var(&mut self, var_name: &str, is_secret: bool, value: Vec<u8>);
1806}
1807
1808impl RuntimeVarDb for Box<dyn RuntimeVarDb> {
1809    fn try_get_var(&mut self, var_name: &str) -> Option<(Vec<u8>, bool)> {
1810        (**self).try_get_var(var_name)
1811    }
1812
1813    fn set_var(&mut self, var_name: &str, is_secret: bool, value: Vec<u8>) {
1814        (**self).set_var(var_name, is_secret, value)
1815    }
1816}
1817
1818pub mod steps {
1819    pub mod ado {
1820        use crate::node::ClaimedReadVar;
1821        use crate::node::ClaimedWriteVar;
1822        use crate::node::ReadVarBacking;
1823        use serde::Deserialize;
1824        use serde::Serialize;
1825        use std::borrow::Cow;
1826
1827        /// An ADO repository declared as a resource in the top-level pipeline.
1828        ///
1829        /// Created via [`crate::pipeline::Pipeline::ado_add_resources_repository`].
1830        ///
1831        /// Consumed via [`AdoStepServices::resolve_repository_id`].
1832        #[derive(Debug, Clone, Serialize, Deserialize)]
1833        pub struct AdoResourcesRepositoryId {
1834            pub(crate) repo_id: String,
1835        }
1836
1837        impl AdoResourcesRepositoryId {
1838            /// Create a `AdoResourcesRepositoryId` corresponding to `self`
1839            /// (i.e: the repo which stores the current pipeline).
1840            ///
1841            /// This is safe to do from any context, as the `self` resource will
1842            /// _always_ be available.
1843            pub fn new_self() -> Self {
1844                Self {
1845                    repo_id: "self".into(),
1846                }
1847            }
1848
1849            /// (dangerous) get the raw ID associated with this resource.
1850            ///
1851            /// It is highly recommended to avoid losing type-safety, and
1852            /// sticking to [`AdoStepServices::resolve_repository_id`].in order
1853            /// to resolve this type to a String.
1854            pub fn dangerous_get_raw_id(&self) -> &str {
1855                &self.repo_id
1856            }
1857
1858            /// (dangerous) create a new ID out of thin air.
1859            ///
1860            /// It is highly recommended to avoid losing type-safety, and
1861            /// sticking to [`AdoStepServices::resolve_repository_id`].in order
1862            /// to resolve this type to a String.
1863            pub fn dangerous_new(repo_id: &str) -> Self {
1864                Self {
1865                    repo_id: repo_id.into(),
1866                }
1867            }
1868        }
1869
1870        /// Handle to an ADO variable.
1871        ///
1872        /// Includes a (non-exhaustive) list of associated constants
1873        /// corresponding to global ADO vars which are _always_ available.
1874        #[derive(Clone, Debug, Serialize, Deserialize)]
1875        pub struct AdoRuntimeVar {
1876            is_secret: bool,
1877            ado_var: Cow<'static, str>,
1878        }
1879
1880        impl AdoRuntimeVar {
1881            /// `build.SourceBranch`
1882            ///
1883            /// NOTE: Includes the full branch ref (ex: `refs/heads/main`) so
1884            /// unlike `build.SourceBranchName`, a branch like `user/foo/bar`
1885            /// won't be stripped to just `bar`
1886            pub const BUILD_SOURCE_BRANCH: AdoRuntimeVar = AdoRuntimeVar::new("build.SourceBranch");
1887
1888            /// `build.BuildNumber`
1889            pub const BUILD_BUILD_NUMBER: AdoRuntimeVar = AdoRuntimeVar::new("build.BuildNumber");
1890
1891            /// `System.AccessToken`
1892            pub const SYSTEM_ACCESS_TOKEN: AdoRuntimeVar =
1893                AdoRuntimeVar::new_secret("System.AccessToken");
1894
1895            /// `System.System.JobAttempt`
1896            pub const SYSTEM_JOB_ATTEMPT: AdoRuntimeVar =
1897                AdoRuntimeVar::new_secret("System.JobAttempt");
1898
1899            /// `Pipeline.Workspace`
1900            pub const PIPELINE_WORKSPACE: AdoRuntimeVar = AdoRuntimeVar::new("Pipeline.Workspace");
1901        }
1902
1903        impl AdoRuntimeVar {
1904            const fn new(s: &'static str) -> Self {
1905                Self {
1906                    is_secret: false,
1907                    ado_var: Cow::Borrowed(s),
1908                }
1909            }
1910
1911            const fn new_secret(s: &'static str) -> Self {
1912                Self {
1913                    is_secret: true,
1914                    ado_var: Cow::Borrowed(s),
1915                }
1916            }
1917
1918            /// Check if the ADO var is tagged as being a secret
1919            pub fn is_secret(&self) -> bool {
1920                self.is_secret
1921            }
1922
1923            /// Get the raw underlying ADO variable name
1924            pub fn as_raw_var_name(&self) -> String {
1925                self.ado_var.as_ref().into()
1926            }
1927
1928            /// Get a handle to an ADO runtime variable corresponding to a
1929            /// global ADO variable with the given name.
1930            ///
1931            /// This method should be used rarely and with great care!
1932            ///
1933            /// ADO variables are global, and sidestep the type-safe data flow
1934            /// between flowey nodes entirely!
1935            pub fn dangerous_from_global(ado_var_name: impl AsRef<str>, is_secret: bool) -> Self {
1936                Self {
1937                    is_secret,
1938                    ado_var: ado_var_name.as_ref().to_owned().into(),
1939                }
1940            }
1941        }
1942
1943        pub fn new_ado_step_services(
1944            fresh_ado_var: &mut dyn FnMut() -> String,
1945        ) -> AdoStepServices<'_> {
1946            AdoStepServices {
1947                fresh_ado_var,
1948                ado_to_rust: Vec::new(),
1949                rust_to_ado: Vec::new(),
1950            }
1951        }
1952
1953        pub struct CompletedAdoStepServices {
1954            pub ado_to_rust: Vec<(String, String, bool)>,
1955            pub rust_to_ado: Vec<(String, String)>,
1956        }
1957
1958        impl CompletedAdoStepServices {
1959            pub fn from_ado_step_services(access: AdoStepServices<'_>) -> Self {
1960                let AdoStepServices {
1961                    fresh_ado_var: _,
1962                    ado_to_rust,
1963                    rust_to_ado,
1964                } = access;
1965
1966                Self {
1967                    ado_to_rust,
1968                    rust_to_ado,
1969                }
1970            }
1971        }
1972
1973        pub struct AdoStepServices<'a> {
1974            fresh_ado_var: &'a mut dyn FnMut() -> String,
1975            ado_to_rust: Vec<(String, String, bool)>,
1976            rust_to_ado: Vec<(String, String)>,
1977        }
1978
1979        impl AdoStepServices<'_> {
1980            /// Return the raw string identifier for the given
1981            /// [`AdoResourcesRepositoryId`].
1982            pub fn resolve_repository_id(&self, repo_id: AdoResourcesRepositoryId) -> String {
1983                repo_id.repo_id
1984            }
1985
1986            /// Set the specified flowey Var using the value of the given ADO var.
1987            // TODO: is there a good way to allow auto-casting the ADO var back
1988            // to a WriteVar<T>, instead of just a String? It's complicated by
1989            // the fact that the ADO var to flowey bridge is handled by the ADO
1990            // backend, which itself needs to know type info...
1991            pub fn set_var(&mut self, var: ClaimedWriteVar<String>, from_ado_var: AdoRuntimeVar) {
1992                self.ado_to_rust.push((
1993                    from_ado_var.ado_var.into(),
1994                    var.backing_var,
1995                    from_ado_var.is_secret,
1996                ))
1997            }
1998
1999            /// Get the value of a flowey Var as a ADO runtime variable.
2000            pub fn get_var(&mut self, var: ClaimedReadVar<String>) -> AdoRuntimeVar {
2001                let backing_var = if let ReadVarBacking::RuntimeVar {
2002                    var,
2003                    is_side_effect,
2004                } = &var.backing_var
2005                {
2006                    assert!(!is_side_effect);
2007                    var
2008                } else {
2009                    todo!("support inline ado read vars")
2010                };
2011
2012                let new_ado_var_name = (self.fresh_ado_var)();
2013
2014                self.rust_to_ado
2015                    .push((backing_var.clone(), new_ado_var_name.clone()));
2016                AdoRuntimeVar::dangerous_from_global(new_ado_var_name, false)
2017            }
2018        }
2019    }
2020
2021    pub mod github {
2022        use crate::node::ClaimVar;
2023        use crate::node::NodeCtx;
2024        use crate::node::ReadVar;
2025        use crate::node::ReadVarBacking;
2026        use crate::node::SideEffect;
2027        use crate::node::StepCtx;
2028        use crate::node::VarClaimed;
2029        use crate::node::VarNotClaimed;
2030        use crate::node::WriteVar;
2031        use std::collections::BTreeMap;
2032
2033        pub struct GhStepBuilder {
2034            display_name: String,
2035            cond: Option<ReadVar<bool>>,
2036            uses: String,
2037            with: Option<BTreeMap<String, GhParam>>,
2038            outputs: BTreeMap<String, Vec<WriteVar<String>>>,
2039            run_after: Vec<ReadVar<SideEffect>>,
2040            permissions: BTreeMap<GhPermission, GhPermissionValue>,
2041        }
2042
2043        impl GhStepBuilder {
2044            /// Creates a new GitHub step builder, with the given display name and
2045            /// action to use. For example, the following code generates the following yaml:
2046            ///
2047            /// ```ignore
2048            /// GhStepBuilder::new("Check out repository code", "actions/checkout@v6").finish()
2049            /// ```
2050            ///
2051            /// ```ignore
2052            /// - name: Check out repository code
2053            ///   uses: actions/checkout@v6
2054            /// ```
2055            ///
2056            /// For more information on the yaml syntax for the `name` and `uses` parameters,
2057            /// see <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsname>
2058            pub fn new(display_name: impl AsRef<str>, uses: impl AsRef<str>) -> Self {
2059                Self {
2060                    display_name: display_name.as_ref().into(),
2061                    cond: None,
2062                    uses: uses.as_ref().into(),
2063                    with: None,
2064                    outputs: BTreeMap::new(),
2065                    run_after: Vec::new(),
2066                    permissions: BTreeMap::new(),
2067                }
2068            }
2069
2070            /// Adds a condition [`ReadVar<bool>`] to the step,
2071            /// such that the step only executes if the condition is true.
2072            /// This is equivalent to using an `if` conditional in the yaml.
2073            ///
2074            /// For more information on the yaml syntax for `if` conditionals, see
2075            /// <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsname>
2076            pub fn condition(mut self, cond: ReadVar<bool>) -> Self {
2077                self.cond = Some(cond);
2078                self
2079            }
2080
2081            /// Adds a parameter to the step, specified as a key-value pair corresponding
2082            /// to the param name and value. For example the following code generates the following yaml:
2083            ///
2084            /// ```rust,ignore
2085            /// let (client_id, write_client_id) = ctx.new_var();
2086            /// let (tenant_id, write_tenant_id) = ctx.new_var();
2087            /// let (subscription_id, write_subscription_id) = ctx.new_var();
2088            /// // ... insert rust step writing to each of those secrets ...
2089            /// GhStepBuilder::new("Azure Login", "Azure/login@v2")
2090            ///               .with("client-id", client_id)
2091            ///               .with("tenant-id", tenant_id)
2092            ///               .with("subscription-id", subscription_id)
2093            /// ```
2094            ///
2095            /// ```text
2096            /// - name: Azure Login
2097            ///   uses: Azure/login@v2
2098            ///   with:
2099            ///     client-id: ${{ env.floweyvar1 }} // Assuming the backend wrote client_id to floweyvar1
2100            ///     tenant-id: ${{ env.floweyvar2 }} // Assuming the backend wrote tenant-id to floweyvar2
2101            ///     subscription-id: ${{ env.floweyvar3 }} // Assuming the backend wrote subscription-id to floweyvar3
2102            /// ```
2103            ///
2104            /// For more information on the yaml syntax for the `with` parameters,
2105            /// see <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepswith>
2106            pub fn with(mut self, k: impl AsRef<str>, v: impl Into<GhParam>) -> Self {
2107                self.with.get_or_insert_with(BTreeMap::new);
2108                if let Some(with) = &mut self.with {
2109                    with.insert(k.as_ref().to_string(), v.into());
2110                }
2111                self
2112            }
2113
2114            /// Specifies an output to read from the step, specified as a key-value pair
2115            /// corresponding to the output name and the flowey var to write the output to.
2116            ///
2117            /// This is equivalent to writing into `v` the output of a step in the yaml using:
2118            /// `${{ steps.<backend-assigned-step-id>.outputs.<k> }}`
2119            ///
2120            /// For more information on step outputs, see
2121            /// <https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#outputs-for-composite-actions>
2122            pub fn output(mut self, k: impl AsRef<str>, v: WriteVar<String>) -> Self {
2123                self.outputs
2124                    .entry(k.as_ref().to_string())
2125                    .or_default()
2126                    .push(v);
2127                self
2128            }
2129
2130            /// Specifies a side-effect that must be resolved before this step can run.
2131            pub fn run_after(mut self, side_effect: ReadVar<SideEffect>) -> Self {
2132                self.run_after.push(side_effect);
2133                self
2134            }
2135
2136            /// Declare that this step requires a certain GITHUB_TOKEN permission in order to run.
2137            ///
2138            /// For more info about Github Actions permissions, see [`gh_grant_permissions`](crate::pipeline::PipelineJob::gh_grant_permissions) and
2139            /// <https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/assigning-permissions-to-jobs>
2140            pub fn requires_permission(
2141                mut self,
2142                perm: GhPermission,
2143                value: GhPermissionValue,
2144            ) -> Self {
2145                self.permissions.insert(perm, value);
2146                self
2147            }
2148
2149            /// Finish building the step, emitting it to the backend and returning a side-effect.
2150            #[track_caller]
2151            pub fn finish(self, ctx: &mut NodeCtx<'_>) -> ReadVar<SideEffect> {
2152                let (side_effect, claim_side_effect) = ctx.new_prefixed_var("auto_se");
2153                ctx.backend
2154                    .borrow_mut()
2155                    .on_claimed_runtime_var(&claim_side_effect.backing_var, false);
2156
2157                ctx.emit_gh_step_inner(
2158                    self.display_name,
2159                    self.cond,
2160                    self.uses,
2161                    self.with,
2162                    self.outputs,
2163                    self.run_after,
2164                    self.permissions,
2165                );
2166
2167                side_effect
2168            }
2169        }
2170
2171        #[derive(Clone, Debug)]
2172        pub enum GhParam<C = VarNotClaimed> {
2173            Static(String),
2174            FloweyVar(ReadVar<String, C>),
2175        }
2176
2177        impl From<String> for GhParam {
2178            fn from(param: String) -> GhParam {
2179                GhParam::Static(param)
2180            }
2181        }
2182
2183        impl From<&str> for GhParam {
2184            fn from(param: &str) -> GhParam {
2185                GhParam::Static(param.to_string())
2186            }
2187        }
2188
2189        impl From<ReadVar<String>> for GhParam {
2190            fn from(param: ReadVar<String>) -> GhParam {
2191                GhParam::FloweyVar(param)
2192            }
2193        }
2194
2195        pub type ClaimedGhParam = GhParam<VarClaimed>;
2196
2197        impl ClaimVar for GhParam {
2198            type Claimed = ClaimedGhParam;
2199
2200            fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedGhParam {
2201                match self {
2202                    GhParam::Static(s) => ClaimedGhParam::Static(s),
2203                    GhParam::FloweyVar(var) => match &var.backing_var {
2204                        ReadVarBacking::RuntimeVar { is_side_effect, .. } => {
2205                            assert!(!is_side_effect);
2206                            ClaimedGhParam::FloweyVar(var.claim(ctx))
2207                        }
2208                        ReadVarBacking::Inline(var) => ClaimedGhParam::Static(var.clone()),
2209                    },
2210                }
2211            }
2212        }
2213
2214        /// The assigned permission value for a scope.
2215        ///
2216        /// For more details on how these values affect a particular scope, refer to:
2217        /// <https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs>
2218        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
2219        pub enum GhPermissionValue {
2220            None = 0,
2221            Read = 1,
2222            Write = 2,
2223        }
2224
2225        /// Refers to the scope of a permission granted to the GITHUB_TOKEN
2226        /// for a job.
2227        ///
2228        /// For more details on each scope, refer to:
2229        /// <https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs>
2230        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2231        pub enum GhPermission {
2232            Actions,
2233            ArtifactMetadata,
2234            Attestations,
2235            Checks,
2236            Contents,
2237            Deployments,
2238            Discussions,
2239            IdToken,
2240            Issues,
2241            Packages,
2242            Pages,
2243            PullRequests,
2244            RepositoryProjects,
2245            SecurityEvents,
2246            Statuses,
2247        }
2248    }
2249
2250    pub mod rust {
2251        use crate::node::ClaimedWriteVar;
2252        use crate::node::FlowArch;
2253        use crate::node::FlowBackend;
2254        use crate::node::FlowPlatform;
2255        use crate::node::ReadVarValue;
2256        use crate::node::RuntimeVarDb;
2257        use crate::shell::FloweyShell;
2258        use serde::Serialize;
2259        use serde::de::DeserializeOwned;
2260
2261        pub fn new_rust_runtime_services(
2262            runtime_var_db: &mut dyn RuntimeVarDb,
2263            backend: FlowBackend,
2264            platform: FlowPlatform,
2265            arch: FlowArch,
2266        ) -> anyhow::Result<RustRuntimeServices<'_>> {
2267            Ok(RustRuntimeServices {
2268                runtime_var_db,
2269                backend,
2270                platform,
2271                arch,
2272                has_read_secret: false,
2273                sh: FloweyShell::new()?,
2274            })
2275        }
2276
2277        pub struct RustRuntimeServices<'a> {
2278            runtime_var_db: &'a mut dyn RuntimeVarDb,
2279            backend: FlowBackend,
2280            platform: FlowPlatform,
2281            arch: FlowArch,
2282            has_read_secret: bool,
2283            /// A pre-initialized [`FloweyShell`] for running commands.
2284            ///
2285            /// This wraps [`xshell::Shell`] and supports transparent command
2286            /// wrapping. Implements [`Deref<Target = xshell::Shell>`](std::ops::Deref)
2287            /// so methods like `change_dir()`, `set_var()`, etc. work directly.
2288            pub sh: FloweyShell,
2289        }
2290
2291        impl RustRuntimeServices<'_> {
2292            /// What backend the flow is being running on (e.g: locally, ADO,
2293            /// GitHub, etc...)
2294            pub fn backend(&self) -> FlowBackend {
2295                self.backend
2296            }
2297
2298            /// What platform the flow is being running on (e.g: windows, linux,
2299            /// etc...).
2300            pub fn platform(&self) -> FlowPlatform {
2301                self.platform
2302            }
2303
2304            /// What arch the flow is being running on (X86_64 or Aarch64)
2305            pub fn arch(&self) -> FlowArch {
2306                self.arch
2307            }
2308
2309            /// Write a value.
2310            ///
2311            /// If this step has already read a secret value, then this will be
2312            /// written as a secret value, as a conservative estimate to avoid
2313            /// leaking secrets. Use [`write_secret`](Self::write_secret) or
2314            /// [`write_not_secret`](Self::write_not_secret) to override this
2315            /// behavior.
2316            pub fn write<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2317            where
2318                T: Serialize + DeserializeOwned,
2319            {
2320                self.write_maybe_secret(var, val, self.has_read_secret)
2321            }
2322
2323            /// Write a secret value, such as a key or token.
2324            ///
2325            /// Flowey will avoid logging this value, and if the value is
2326            /// converted to a CI environment variable, the CI system will be
2327            /// told not to print the value either.
2328            pub fn write_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2329            where
2330                T: Serialize + DeserializeOwned,
2331            {
2332                self.write_maybe_secret(var, val, true)
2333            }
2334
2335            /// Write a value that is not secret, even if this step has already
2336            /// read secret values.
2337            ///
2338            /// Usually [`write`](Self::write) is preferred--use this only when
2339            /// your step reads secret values and you explicitly want to write a
2340            /// non-secret value.
2341            pub fn write_not_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2342            where
2343                T: Serialize + DeserializeOwned,
2344            {
2345                self.write_maybe_secret(var, val, false)
2346            }
2347
2348            fn write_maybe_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T, is_secret: bool)
2349            where
2350                T: Serialize + DeserializeOwned,
2351            {
2352                let val = if var.is_side_effect {
2353                    b"null".to_vec()
2354                } else {
2355                    serde_json::to_vec(val).expect("improve this error path")
2356                };
2357                self.runtime_var_db
2358                    .set_var(&var.backing_var, is_secret, val);
2359            }
2360
2361            pub fn write_all<T>(
2362                &mut self,
2363                vars: impl IntoIterator<Item = ClaimedWriteVar<T>>,
2364                val: &T,
2365            ) where
2366                T: Serialize + DeserializeOwned,
2367            {
2368                for var in vars {
2369                    self.write(var, val)
2370                }
2371            }
2372
2373            pub fn read<T: ReadVarValue>(&mut self, var: T) -> T::Value {
2374                var.read_value(self)
2375            }
2376
2377            pub(crate) fn get_var(&mut self, var: &str, is_side_effect: bool) -> Vec<u8> {
2378                let (v, is_secret) = self.runtime_var_db.get_var(var);
2379                self.has_read_secret |= is_secret && !is_side_effect;
2380                v
2381            }
2382
2383            /// DANGEROUS: Set the value of _Global_ Environment Variable (GitHub Actions only).
2384            ///
2385            /// It is up to the caller to ensure that the variable does not get
2386            /// unintentionally overwritten or used.
2387            ///
2388            /// This method should be used rarely and with great care!
2389            pub fn dangerous_gh_set_global_env_var(
2390                &mut self,
2391                var: String,
2392                gh_env_var: String,
2393            ) -> anyhow::Result<()> {
2394                if !matches!(self.backend, FlowBackend::Github) {
2395                    return Err(anyhow::anyhow!(
2396                        "dangerous_set_gh_env_var can only be used on GitHub Actions"
2397                    ));
2398                }
2399
2400                let gh_env_file_path = std::env::var("GITHUB_ENV")?;
2401                let mut gh_env_file = fs_err::OpenOptions::new()
2402                    .append(true)
2403                    .open(gh_env_file_path)?;
2404                let gh_env_var_assignment = format!(
2405                    r#"{}<<EOF
2406{}
2407EOF
2408"#,
2409                    gh_env_var, var
2410                );
2411                std::io::Write::write_all(&mut gh_env_file, gh_env_var_assignment.as_bytes())?;
2412
2413                Ok(())
2414            }
2415        }
2416    }
2417}
2418
2419/// The base underlying implementation of all FlowNode variants.
2420///
2421/// Do not implement this directly! Use the `new_flow_node!` family of macros
2422/// instead!
2423pub trait FlowNodeBase {
2424    type Request: Serialize + DeserializeOwned;
2425
2426    fn imports(&mut self, ctx: &mut ImportCtx<'_>);
2427    fn emit(
2428        &mut self,
2429        config_bytes: Vec<Box<[u8]>>,
2430        requests: Vec<Self::Request>,
2431        ctx: &mut NodeCtx<'_>,
2432    ) -> anyhow::Result<()>;
2433
2434    /// A noop method that all human-written impls of `FlowNodeBase` are
2435    /// required to implement.
2436    ///
2437    /// By implementing this method, you're stating that you "know what you're
2438    /// doing" by having this manual impl.
2439    fn i_know_what_im_doing_with_this_manual_impl(&mut self);
2440}
2441
2442pub mod erased {
2443    use crate::node::FlowNodeBase;
2444    use crate::node::NodeCtx;
2445    use crate::node::user_facing::*;
2446
2447    pub struct ErasedNode<N: FlowNodeBase>(pub N);
2448
2449    impl<N: FlowNodeBase> ErasedNode<N> {
2450        pub fn from_node(node: N) -> Self {
2451            Self(node)
2452        }
2453    }
2454
2455    impl<N> FlowNodeBase for ErasedNode<N>
2456    where
2457        N: FlowNodeBase,
2458    {
2459        // FIXME: this should be using type-erased serde
2460        type Request = Box<[u8]>;
2461
2462        fn imports(&mut self, ctx: &mut ImportCtx<'_>) {
2463            self.0.imports(ctx)
2464        }
2465
2466        fn emit(
2467            &mut self,
2468            config_bytes: Vec<Box<[u8]>>,
2469            requests: Vec<Box<[u8]>>,
2470            ctx: &mut NodeCtx<'_>,
2471        ) -> anyhow::Result<()> {
2472            let mut converted_requests = Vec::new();
2473            for req in requests {
2474                converted_requests.push(serde_json::from_slice(&req)?)
2475            }
2476
2477            self.0.emit(config_bytes, converted_requests, ctx)
2478        }
2479
2480        fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2481    }
2482}
2483
2484/// Cheap handle to a registered [`FlowNode`]
2485#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2486pub struct NodeHandle(std::any::TypeId);
2487
2488impl Ord for NodeHandle {
2489    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2490        self.modpath().cmp(other.modpath())
2491    }
2492}
2493
2494impl PartialOrd for NodeHandle {
2495    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2496        Some(self.cmp(other))
2497    }
2498}
2499
2500impl std::fmt::Debug for NodeHandle {
2501    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2502        std::fmt::Debug::fmt(&self.try_modpath(), f)
2503    }
2504}
2505
2506impl NodeHandle {
2507    pub fn from_type<N: FlowNodeBase + 'static>() -> NodeHandle {
2508        NodeHandle(std::any::TypeId::of::<N>())
2509    }
2510
2511    pub fn from_modpath(modpath: &str) -> NodeHandle {
2512        node_luts::erased_node_by_modpath().get(modpath).unwrap().0
2513    }
2514
2515    pub fn try_from_modpath(modpath: &str) -> Option<NodeHandle> {
2516        node_luts::erased_node_by_modpath()
2517            .get(modpath)
2518            .map(|(s, _)| *s)
2519    }
2520
2521    pub fn new_erased_node(&self) -> Box<dyn FlowNodeBase<Request = Box<[u8]>>> {
2522        let ctor = node_luts::erased_node_by_typeid().get(self).unwrap();
2523        ctor()
2524    }
2525
2526    pub fn modpath(&self) -> &'static str {
2527        node_luts::modpath_by_node_typeid().get(self).unwrap()
2528    }
2529
2530    pub fn try_modpath(&self) -> Option<&'static str> {
2531        node_luts::modpath_by_node_typeid().get(self).cloned()
2532    }
2533
2534    /// Return a dummy NodeHandle, which will panic if `new_erased_node` is ever
2535    /// called on it.
2536    pub fn dummy() -> NodeHandle {
2537        NodeHandle(std::any::TypeId::of::<()>())
2538    }
2539}
2540
2541pub fn list_all_registered_nodes() -> impl Iterator<Item = NodeHandle> {
2542    node_luts::modpath_by_node_typeid().keys().cloned()
2543}
2544
2545// Encapsulate these look up tables in their own module to limit the scope of
2546// the HashMap import.
2547//
2548// In general, using HashMap in flowey is a recipe for disaster, given that
2549// iterating through the hash-map will result in non-deterministic orderings,
2550// which can cause annoying ordering churn.
2551//
2552// That said, in this case, it's OK since the code using these LUTs won't ever
2553// iterate through the map.
2554//
2555// Why is the HashMap even necessary vs. a BTreeMap?
2556//
2557// Well... NodeHandle's `Ord` impl does a `modpath` comparison instead of a
2558// TypeId comparison, since TypeId will vary between compilations.
2559mod node_luts {
2560    use super::FlowNodeBase;
2561    use super::NodeHandle;
2562    use std::collections::HashMap;
2563    use std::sync::OnceLock;
2564
2565    pub(super) fn modpath_by_node_typeid() -> &'static HashMap<NodeHandle, &'static str> {
2566        static TYPEID_TO_MODPATH: OnceLock<HashMap<NodeHandle, &'static str>> = OnceLock::new();
2567
2568        TYPEID_TO_MODPATH.get_or_init(|| {
2569            let mut lookup = HashMap::new();
2570            for crate::node::private::FlowNodeMeta {
2571                module_path,
2572                ctor: _,
2573                typeid,
2574            } in crate::node::private::FLOW_NODES
2575            {
2576                let existing = lookup.insert(
2577                    NodeHandle(*typeid),
2578                    module_path
2579                        .strip_suffix("::_only_one_call_to_flowey_node_per_module")
2580                        .unwrap(),
2581                );
2582                // if this were to fire for an array where the key is a TypeId...
2583                // something has gone _terribly_ wrong
2584                assert!(existing.is_none())
2585            }
2586
2587            lookup
2588        })
2589    }
2590
2591    pub(super) fn erased_node_by_typeid()
2592    -> &'static HashMap<NodeHandle, fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>> {
2593        static LOOKUP: OnceLock<
2594            HashMap<NodeHandle, fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>>,
2595        > = OnceLock::new();
2596
2597        LOOKUP.get_or_init(|| {
2598            let mut lookup = HashMap::new();
2599            for crate::node::private::FlowNodeMeta {
2600                module_path: _,
2601                ctor,
2602                typeid,
2603            } in crate::node::private::FLOW_NODES
2604            {
2605                let existing = lookup.insert(NodeHandle(*typeid), *ctor);
2606                // if this were to fire for an array where the key is a TypeId...
2607                // something has gone _terribly_ wrong
2608                assert!(existing.is_none())
2609            }
2610
2611            lookup
2612        })
2613    }
2614
2615    pub(super) fn erased_node_by_modpath() -> &'static HashMap<
2616        &'static str,
2617        (
2618            NodeHandle,
2619            fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>,
2620        ),
2621    > {
2622        static MODPATH_LOOKUP: OnceLock<
2623            HashMap<
2624                &'static str,
2625                (
2626                    NodeHandle,
2627                    fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>,
2628                ),
2629            >,
2630        > = OnceLock::new();
2631
2632        MODPATH_LOOKUP.get_or_init(|| {
2633            let mut lookup = HashMap::new();
2634            for crate::node::private::FlowNodeMeta { module_path, ctor, typeid } in crate::node::private::FLOW_NODES {
2635                let existing = lookup.insert(module_path.strip_suffix("::_only_one_call_to_flowey_node_per_module").unwrap(), (NodeHandle(*typeid), *ctor));
2636                if existing.is_some() {
2637                    panic!("conflicting node registrations at {module_path}! please ensure there is a single node per module!")
2638                }
2639            }
2640            lookup
2641        })
2642    }
2643}
2644
2645#[doc(hidden)]
2646pub mod private {
2647    pub use linkme;
2648
2649    pub struct FlowNodeMeta {
2650        pub module_path: &'static str,
2651        pub ctor: fn() -> Box<dyn super::FlowNodeBase<Request = Box<[u8]>>>,
2652        pub typeid: std::any::TypeId,
2653    }
2654
2655    #[linkme::distributed_slice]
2656    pub static FLOW_NODES: [FlowNodeMeta] = [..];
2657
2658    // UNSAFETY: linkme uses manual link sections, which are unsafe.
2659    #[expect(unsafe_code)]
2660    #[linkme::distributed_slice(FLOW_NODES)]
2661    static DUMMY_FLOW_NODE: FlowNodeMeta = FlowNodeMeta {
2662        module_path: "<dummy>::_only_one_call_to_flowey_node_per_module",
2663        ctor: || unreachable!(),
2664        typeid: std::any::TypeId::of::<()>(),
2665    };
2666}
2667
2668#[doc(hidden)]
2669#[macro_export]
2670macro_rules! new_flow_node_base {
2671    (struct Node) => {
2672        /// (see module-level docs)
2673        #[non_exhaustive]
2674        pub struct Node;
2675
2676        mod _only_one_call_to_flowey_node_per_module {
2677            const _: () = {
2678                use $crate::node::private::linkme;
2679
2680                fn new_erased() -> Box<dyn $crate::node::FlowNodeBase<Request = Box<[u8]>>> {
2681                    Box::new($crate::node::erased::ErasedNode(super::Node))
2682                }
2683
2684                #[linkme::distributed_slice($crate::node::private::FLOW_NODES)]
2685                #[linkme(crate = linkme)]
2686                static FLOW_NODE: $crate::node::private::FlowNodeMeta =
2687                    $crate::node::private::FlowNodeMeta {
2688                        module_path: module_path!(),
2689                        ctor: new_erased,
2690                        typeid: std::any::TypeId::of::<super::Node>(),
2691                    };
2692            };
2693        }
2694    };
2695}
2696
2697/// A reusable unit of automation logic in flowey.
2698///
2699/// FlowNodes process requests, emit steps, and can depend on other nodes. They are
2700/// the building blocks for creating complex automation workflows.
2701///
2702/// # The Node/Request Pattern
2703///
2704/// Every node has an associated **Request** type that defines what the node can do.
2705/// Nodes receive a vector of requests and process them together, allowing for
2706/// aggregation and conflict resolution.
2707///
2708/// # Example: Basic FlowNode Implementation
2709///
2710/// ```rust,ignore
2711/// use flowey_core::node::*;
2712///
2713/// // Define the node
2714/// new_flow_node!(struct Node);
2715///
2716/// // Define requests using the flowey_request! macro
2717/// flowey_request! {
2718///     pub enum Request {
2719///         InstallRust(String),                    // Install specific version
2720///         EnsureInstalled(WriteVar<SideEffect>),  // Ensure it's installed
2721///         GetCargoHome(WriteVar<PathBuf>),        // Get CARGO_HOME path
2722///     }
2723/// }
2724///
2725/// impl FlowNode for Node {
2726///     type Request = Request;
2727///
2728///     fn imports(ctx: &mut ImportCtx<'_>) {
2729///         // Declare node dependencies
2730///         ctx.import::<other_node::Node>();
2731///     }
2732///
2733///     fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
2734///         // 1. Aggregate and validate requests
2735///         let mut version = None;
2736///         let mut ensure_installed = Vec::new();
2737///         let mut get_cargo_home = Vec::new();
2738///
2739///         for req in requests {
2740///             match req {
2741///                 Request::InstallRust(v) => {
2742///                     same_across_all_reqs("version", &mut version, v)?;
2743///                 }
2744///                 Request::EnsureInstalled(var) => ensure_installed.push(var),
2745///                 Request::GetCargoHome(var) => get_cargo_home.push(var),
2746///             }
2747///         }
2748///
2749///         let version = version.ok_or(anyhow::anyhow!("Version not specified"))?;
2750///
2751///         // 2. Emit steps to do the work
2752///         ctx.emit_rust_step("install rust", |ctx| {
2753///             let ensure_installed = ensure_installed.claim(ctx);
2754///             let get_cargo_home = get_cargo_home.claim(ctx);
2755///             move |rt| {
2756///                 // Install rust with the specified version
2757///                 // Write to all the output variables
2758///                 for var in ensure_installed {
2759///                     rt.write(var, &());
2760///                 }
2761///                 for var in get_cargo_home {
2762///                     rt.write(var, &PathBuf::from("/path/to/cargo"));
2763///                 }
2764///                 Ok(())
2765///             }
2766///         });
2767///
2768///         Ok(())
2769///     }
2770/// }
2771/// ```
2772///
2773/// # When to Use FlowNode vs SimpleFlowNode
2774///
2775/// **Use `FlowNode`** when you need to:
2776/// - Aggregate multiple requests and process them together
2777/// - Resolve conflicts between requests
2778/// - Perform complex request validation
2779///
2780/// **Use [`SimpleFlowNode`]** when:
2781/// - Each request can be processed independently
2782/// - No aggregation logic is needed
2783pub trait FlowNode {
2784    /// The request type that defines what operations this node can perform.
2785    ///
2786    /// Use the [`crate::flowey_request!`] macro to define this type.
2787    type Request: Serialize + DeserializeOwned;
2788
2789    /// A list of nodes that this node is capable of taking a dependency on.
2790    ///
2791    /// Attempting to take a dep on a node that wasn't imported via this method
2792    /// will result in an error during flow resolution time.
2793    ///
2794    /// * * *
2795    ///
2796    /// To put it bluntly: This is boilerplate.
2797    ///
2798    /// We (the flowey devs) are thinking about ways to avoid requiring this
2799    /// method, but do not have a good solution at this time.
2800    fn imports(ctx: &mut ImportCtx<'_>);
2801
2802    /// Given a set of incoming `requests`, emit various steps to run, set
2803    /// various dependencies, etc...
2804    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2805}
2806
2807#[macro_export]
2808macro_rules! new_flow_node {
2809    (struct Node) => {
2810        $crate::new_flow_node_base!(struct Node);
2811
2812        impl $crate::node::FlowNodeBase for Node
2813        where
2814            Node: FlowNode,
2815        {
2816            type Request = <Node as FlowNode>::Request;
2817
2818            fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2819                <Node as FlowNode>::imports(dep)
2820            }
2821
2822            fn emit(
2823                &mut self,
2824                _config_bytes: Vec<Box<[u8]>>,
2825                requests: Vec<Self::Request>,
2826                ctx: &mut $crate::node::NodeCtx<'_>,
2827            ) -> anyhow::Result<()> {
2828                <Node as FlowNode>::emit(requests, ctx)
2829            }
2830
2831            fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2832        }
2833    };
2834}
2835
2836/// A helper trait to streamline implementing [`FlowNode`] instances that only
2837/// ever operate on a single request at a time.
2838///
2839/// In essence, [`SimpleFlowNode`] handles the boilerplate (and rightward-drift)
2840/// of manually writing:
2841///
2842/// ```ignore
2843/// impl FlowNode for Node {
2844///     fn imports(dep: &mut ImportCtx<'_>) { ... }
2845///     fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) {
2846///         for req in requests {
2847///             Node::process_request(req, ctx)
2848///         }
2849///     }
2850/// }
2851/// ```
2852///
2853/// Nodes which accept a `struct Request` often fall into this pattern, whereas
2854/// nodes which accept a `enum Request` typically require additional logic to
2855/// aggregate / resolve incoming requests.
2856pub trait SimpleFlowNode {
2857    type Request: Serialize + DeserializeOwned;
2858
2859    /// A list of nodes that this node is capable of taking a dependency on.
2860    ///
2861    /// Attempting to take a dep on a node that wasn't imported via this method
2862    /// will result in an error during flow resolution time.
2863    ///
2864    /// * * *
2865    ///
2866    /// To put it bluntly: This is boilerplate.
2867    ///
2868    /// We (the flowey devs) are thinking about ways to avoid requiring this
2869    /// method, but do not have a good solution at this time.
2870    fn imports(ctx: &mut ImportCtx<'_>);
2871
2872    /// Process a single incoming `Self::Request`
2873    fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2874}
2875
2876#[macro_export]
2877macro_rules! new_simple_flow_node {
2878    (struct Node) => {
2879        $crate::new_flow_node_base!(struct Node);
2880
2881        impl $crate::node::FlowNodeBase for Node
2882        where
2883            Node: $crate::node::SimpleFlowNode,
2884        {
2885            type Request = <Node as $crate::node::SimpleFlowNode>::Request;
2886
2887            fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2888                <Node as $crate::node::SimpleFlowNode>::imports(dep)
2889            }
2890
2891            fn emit(
2892                &mut self,
2893                _config_bytes: Vec<Box<[u8]>>,
2894                requests: Vec<Self::Request>,
2895                ctx: &mut $crate::node::NodeCtx<'_>,
2896            ) -> anyhow::Result<()> {
2897                for req in requests {
2898                    <Node as $crate::node::SimpleFlowNode>::process_request(req, ctx)?
2899                }
2900
2901                Ok(())
2902            }
2903
2904            fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2905        }
2906    };
2907}
2908
2909/// A [`FlowNode`] variant that receives a typed, pre-merged config alongside
2910/// its requests.
2911///
2912/// Use this when a node has "config" values (e.g., version strings, feature
2913/// flags) that must agree across all callers AND are needed to emit outgoing
2914/// requests or steps.
2915///
2916/// The framework merges config from all callers (validating equality) and
2917/// delivers the finalized `Config` to `emit()`. The node never sees raw
2918/// config requests — they are handled by the infrastructure.
2919///
2920/// # Example
2921///
2922/// ```rust,ignore
2923/// flowey_config! {
2924///     pub struct Config {
2925///         pub version: Option<String>,
2926///     }
2927/// }
2928///
2929/// flowey_request! {
2930///     pub enum Request {
2931///         GetAzCopy(WriteVar<PathBuf>),
2932///     }
2933/// }
2934///
2935/// new_flow_node_with_config!(struct Node);
2936///
2937/// impl FlowNodeWithConfig for Node {
2938///     type Request = Request;
2939///     type Config = Config;
2940///
2941///     fn imports(ctx: &mut ImportCtx<'_>) { /* ... */ }
2942///
2943///     fn emit(
2944///         config: Config,
2945///         requests: Vec<Self::Request>,
2946///         ctx: &mut NodeCtx<'_>,
2947///     ) -> anyhow::Result<()> {
2948///         let version = config.version
2949///             .ok_or(anyhow::anyhow!("missing config: version"))?;
2950///         // ...
2951///         Ok(())
2952///     }
2953/// }
2954/// ```
2955pub trait FlowNodeWithConfig {
2956    /// The request type (action requests only — no config variants).
2957    type Request: Serialize + DeserializeOwned;
2958
2959    /// The config type generated by [`flowey_config!`](crate::flowey_config).
2960    ///
2961    /// Scalar fields are typically wrapped in `Option<T>`, and the node decides which
2962    /// options are treated as required vs optional. Configs may also include
2963    /// non-`Option` mergeable fields (for example, maps) that are combined according
2964    /// to the [`ConfigMerge`] implementation.
2965    type Config: ConfigMerge;
2966
2967    /// Declare node dependencies.
2968    fn imports(ctx: &mut ImportCtx<'_>);
2969
2970    /// Process requests with the merged config.
2971    fn emit(
2972        config: Self::Config,
2973        requests: Vec<Self::Request>,
2974        ctx: &mut NodeCtx<'_>,
2975    ) -> anyhow::Result<()>;
2976}
2977
2978#[macro_export]
2979macro_rules! new_flow_node_with_config {
2980    (struct Node) => {
2981        $crate::new_flow_node_base!(struct Node);
2982
2983        impl $crate::node::FlowNodeBase for Node
2984        where
2985            Node: $crate::node::FlowNodeWithConfig,
2986        {
2987            type Request = <Node as $crate::node::FlowNodeWithConfig>::Request;
2988
2989            fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2990                <Node as $crate::node::FlowNodeWithConfig>::imports(dep)
2991            }
2992
2993            fn emit(
2994                &mut self,
2995                config_bytes: Vec<Box<[u8]>>,
2996                requests: Vec<Self::Request>,
2997                ctx: &mut $crate::node::NodeCtx<'_>,
2998            ) -> anyhow::Result<()> {
2999                use $crate::node::ConfigMerge;
3000
3001                type C = <Node as $crate::node::FlowNodeWithConfig>::Config;
3002
3003                let mut merged = <C as Default>::default();
3004                for bytes in config_bytes {
3005                    let partial: C = serde_json::from_slice(&bytes)?;
3006                    merged.merge(partial)?;
3007                }
3008
3009                <Node as $crate::node::FlowNodeWithConfig>::emit(merged, requests, ctx)
3010            }
3011
3012            fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
3013        }
3014    };
3015}
3016
3017/// A "glue" trait which improves [`NodeCtx::req`] ergonomics, by tying a
3018/// particular `Request` type to its corresponding [`FlowNode`].
3019///
3020/// This trait should be autogenerated via [`flowey_request!`] - do not try to
3021/// implement it manually!
3022///
3023/// [`flowey_request!`]: crate::flowey_request
3024pub trait IntoRequest {
3025    type Node: FlowNodeBase;
3026    fn into_request(self) -> <Self::Node as FlowNodeBase>::Request;
3027
3028    /// By implementing this method manually, you're indicating that you know what you're
3029    /// doing,
3030    #[doc(hidden)]
3031    #[expect(nonstandard_style)]
3032    fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self);
3033}
3034
3035/// A "glue" trait for routing config to the correct node, analogous to
3036/// [`IntoRequest`].
3037///
3038/// This trait should be autogenerated via the `flowey_config!` macro - do not
3039/// try to implement it manually!
3040pub trait IntoConfig: Serialize {
3041    type Node: FlowNodeBase;
3042
3043    /// By implementing this method manually, you're indicating that you know what you're
3044    /// doing,
3045    #[doc(hidden)]
3046    #[expect(nonstandard_style)]
3047    fn do_not_manually_impl_this_trait__use_the_flowey_config_macro_instead(&mut self);
3048}
3049
3050/// Trait for merging config values. Implemented by the `flowey_config!`
3051/// macro on the generated `Config` type.
3052pub trait ConfigMerge: Serialize + DeserializeOwned + Default {
3053    /// Merge another config into this one. Fields that are already set
3054    /// must agree with the incoming values.
3055    fn merge(&mut self, other: Self) -> anyhow::Result<()>;
3056}
3057
3058/// Trait for merging a single config field. The `flowey_config!` macro calls
3059/// `ConfigField::merge_field` on each field during config merging.
3060///
3061/// Implemented for:
3062/// - `Option<T>`: first setter wins, subsequent must agree (`PartialEq`)
3063/// - `BTreeMap<K, V>`: per-key merge, each key's value must agree
3064pub trait ConfigField {
3065    fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()>;
3066}
3067
3068impl<T: PartialEq> ConfigField for Option<T> {
3069    fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()> {
3070        if let Some(new) = other {
3071            match self {
3072                None => *self = Some(new),
3073                Some(old) if *old == new => {}
3074                Some(_) => {
3075                    anyhow::bail!("config field `{field_name}` mismatch");
3076                }
3077            }
3078        }
3079        Ok(())
3080    }
3081}
3082
3083impl<K: Ord + std::fmt::Debug, V: PartialEq> ConfigField for BTreeMap<K, V> {
3084    fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()> {
3085        for (k, v) in other {
3086            use std::collections::btree_map::Entry;
3087            match self.entry(k) {
3088                Entry::Vacant(e) => {
3089                    e.insert(v);
3090                }
3091                Entry::Occupied(e) if *e.get() == v => {}
3092                Entry::Occupied(e) => {
3093                    anyhow::bail!("config field `{field_name}` mismatch for key {:?}", e.key(),);
3094                }
3095            }
3096        }
3097        Ok(())
3098    }
3099}
3100
3101#[doc(hidden)]
3102#[macro_export]
3103macro_rules! __flowey_request_inner {
3104    //
3105    // @emit_struct: emit structs for each variant of the request enum
3106    //
3107    (@emit_struct [$req:ident]
3108        $(#[$a:meta])*
3109        $variant:ident($($tt:tt)*),
3110        $($rest:tt)*
3111    ) => {
3112        $(#[$a])*
3113        #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3114        pub struct $variant($($tt)*);
3115
3116        impl IntoRequest for $variant {
3117            type Node = Node;
3118            fn into_request(self) -> $req {
3119                $req::$variant(self)
3120            }
3121            fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3122        }
3123
3124        $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3125    };
3126    (@emit_struct [$req:ident]
3127        $(#[$a:meta])*
3128        $variant:ident { $($tt:tt)* },
3129        $($rest:tt)*
3130    ) => {
3131        $(#[$a])*
3132        #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3133        pub struct $variant {
3134            $($tt)*
3135        }
3136
3137        impl IntoRequest for $variant {
3138            type Node = Node;
3139            fn into_request(self) -> $req {
3140                $req::$variant(self)
3141            }
3142            fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3143        }
3144
3145        $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3146    };
3147    (@emit_struct [$req:ident]
3148        $(#[$a:meta])*
3149        $variant:ident,
3150        $($rest:tt)*
3151    ) => {
3152        $(#[$a])*
3153        #[derive(Serialize, Deserialize)]
3154        pub struct $variant;
3155
3156        impl IntoRequest for $variant {
3157            type Node = Node;
3158            fn into_request(self) -> $req {
3159                $req::$variant(self)
3160            }
3161            fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3162        }
3163
3164        $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3165    };
3166    (@emit_struct [$req:ident]
3167    ) => {};
3168
3169    //
3170    // @emit_req_enum: build up root request enum
3171    //
3172    (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3173        $(#[$a:meta])*
3174        $variant:ident($($tt:tt)*),
3175        $($rest:tt)*
3176    ) => {
3177        $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3178    };
3179    (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3180        $(#[$a:meta])*
3181        $variant:ident { $($tt:tt)* },
3182        $($rest:tt)*
3183    ) => {
3184        $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3185    };
3186    (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3187        $(#[$a:meta])*
3188        $variant:ident,
3189        $($rest:tt)*
3190    ) => {
3191        $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3192    };
3193    (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3194    ) => {
3195        #[derive(Serialize, Deserialize)]
3196        pub enum $req {$(
3197            $(#[$prev_a])*
3198            $prev(self::req::$prev),
3199        )*}
3200
3201        impl IntoRequest for $req {
3202            type Node = Node;
3203            fn into_request(self) -> $req {
3204                self
3205            }
3206            fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3207        }
3208    };
3209}
3210
3211/// Declare a new `Request` type for the current `Node`.
3212///
3213/// ## `struct` and `enum` Requests
3214///
3215/// When wrapping a vanilla Rust `struct` and `enum` declaration, this macro
3216/// simply derives [`Serialize`], [`Deserialize`], and [`IntoRequest`] for the
3217/// type, and does nothing else.
3218///
3219/// ## `enum_struct` Requests
3220///
3221/// This macro also supports a special kind of `enum_struct` derive, which
3222/// allows declaring a Request enum where each variant is split off into its own
3223/// separate (named) `struct`.
3224///
3225/// e.g:
3226///
3227/// ```ignore
3228/// flowey_request! {
3229///     pub enum_struct Foo {
3230///         Bar,
3231///         Baz(pub usize),
3232///         Qux(pub String),
3233///     }
3234/// }
3235/// ```
3236///
3237/// will be expanded into:
3238///
3239/// ```ignore
3240/// #[derive(Serialize, Deserialize)]
3241/// pub enum Foo {
3242///    Bar(req::Bar),
3243///    Baz(req::Baz),
3244///    Qux(req::Qux),
3245/// }
3246///
3247/// pud mod req {
3248///     #[derive(Serialize, Deserialize)]
3249///     pub struct Bar;
3250///
3251///     #[derive(Serialize, Deserialize)]
3252///     pub struct Baz(pub usize);
3253///
3254///     #[derive(Serialize, Deserialize)]
3255///     pub struct Qux(pub String);
3256/// }
3257/// ```
3258#[macro_export]
3259macro_rules! flowey_request {
3260    (
3261        $(#[$root_a:meta])*
3262        pub enum_struct $req:ident {
3263            $($tt:tt)*
3264        }
3265    ) => {
3266        $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*),] $($tt)*);
3267        pub mod req {
3268            use super::*;
3269            $crate::__flowey_request_inner!(@emit_struct [$req] $($tt)*);
3270        }
3271    };
3272
3273    (
3274        $(#[$a:meta])*
3275        pub enum $req:ident {
3276            $($tt:tt)*
3277        }
3278    ) => {
3279        $(#[$a])*
3280        #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3281        pub enum $req {
3282            $($tt)*
3283        }
3284
3285        impl $crate::node::IntoRequest for $req {
3286            type Node = Node;
3287            fn into_request(self) -> $req {
3288                self
3289            }
3290            fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3291        }
3292    };
3293
3294    (
3295        $(#[$a:meta])*
3296        pub struct $req:ident {
3297            $($tt:tt)*
3298        }
3299    ) => {
3300        $(#[$a])*
3301        #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3302        pub struct $req {
3303            $($tt)*
3304        }
3305
3306        impl $crate::node::IntoRequest for $req {
3307            type Node = Node;
3308            fn into_request(self) -> $req {
3309                self
3310            }
3311            fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3312        }
3313    };
3314
3315    (
3316        $(#[$a:meta])*
3317        pub struct $req:ident($($tt:tt)*);
3318    ) => {
3319        $(#[$a])*
3320        #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3321        pub struct $req($($tt)*);
3322
3323        impl $crate::node::IntoRequest for $req {
3324            type Node = Node;
3325            fn into_request(self) -> $req {
3326                self
3327            }
3328            fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3329        }
3330    };
3331}
3332
3333/// Declare a config struct for a flowey node.
3334///
3335/// Fields should be `Option<T>` or `BTreeMap<K, V>`:
3336///
3337/// - `Option<T>` — callers set only the fields they care about. The first
3338///   caller to set a field wins; subsequent callers must agree on the same
3339///   value or merging will fail. The node decides which fields are required
3340///   vs optional in its `emit()`.
3341///
3342/// - `BTreeMap<K, V>` — callers contribute entries independently. Each key
3343///   may only be set once; if two callers set the same key, the values must
3344///   agree. Useful for per-variant or per-target configuration maps.
3345///
3346/// Generates:
3347/// - The `Config` struct with `Serialize`, `Deserialize`, `Default` derives
3348/// - `ConfigMerge` impl with field-level equality merging
3349/// - `IntoConfig` impl tying it to `Node`
3350///
3351/// # Example
3352///
3353/// ```rust,ignore
3354/// flowey_config! {
3355///     pub struct Config {
3356///         pub version: Option<String>,
3357///         pub auto_install: Option<bool>,
3358///         pub target_flags: BTreeMap<String, String>,
3359///     }
3360/// }
3361/// ```
3362///
3363/// Callers send config via:
3364/// ```rust,ignore
3365/// ctx.config(node::Config {
3366///     version: Some("10.31.0".into()),
3367///     ..Default::default()
3368/// });
3369/// ```
3370#[macro_export]
3371macro_rules! flowey_config {
3372    (
3373        $(#[$meta:meta])*
3374        pub struct $Config:ident {
3375            $(
3376                $(#[$field_meta:meta])*
3377                pub $field:ident : $ty:ty
3378            ),* $(,)?
3379        }
3380    ) => {
3381        $(#[$meta])*
3382        #[derive(
3383            $crate::reexports::Serialize,
3384            $crate::reexports::Deserialize,
3385            Default,
3386        )]
3387        pub struct $Config {
3388            $(
3389                $(#[$field_meta])*
3390                pub $field: $ty,
3391            )*
3392        }
3393
3394        impl $crate::node::ConfigMerge for $Config {
3395            fn merge(&mut self, other: Self) -> anyhow::Result<()> {
3396                $(
3397                    $crate::node::ConfigField::merge_field(
3398                        &mut self.$field,
3399                        stringify!($field),
3400                        other.$field,
3401                    )?;
3402                )*
3403                Ok(())
3404            }
3405        }
3406
3407        impl $crate::node::IntoConfig for $Config {
3408            type Node = Node;
3409
3410            fn do_not_manually_impl_this_trait__use_the_flowey_config_macro_instead(&mut self) {}
3411        }
3412    };
3413}
3414
3415/// Construct a command to run via the flowey shell.
3416///
3417/// This is a wrapper around [`xshell::cmd!`] that returns a [`FloweyCmd`]
3418/// instead of a raw [`xshell::Cmd`]. The [`FloweyCmd`] applies any
3419/// [`CommandWrapperKind`] configured on the shell at execution time, making it
3420/// possible to transparently wrap commands (e.g. in `nix-shell --pure`)
3421/// without touching every callsite.
3422///
3423/// [`FloweyCmd`]: crate::shell::FloweyCmd
3424/// [`CommandWrapperKind`]: crate::shell::CommandWrapperKind
3425///
3426/// # Example
3427///
3428/// ```ignore
3429/// flowey::shell_cmd!(rt, "cargo build --release").run()?;
3430/// ```
3431#[macro_export]
3432macro_rules! shell_cmd {
3433    ($rt:expr, $cmd:literal) => {{
3434        let flowey_sh = &$rt.sh;
3435        #[expect(clippy::disallowed_macros)]
3436        flowey_sh.wrap($crate::reexports::xshell::cmd!(flowey_sh.xshell(), $cmd))
3437    }};
3438}