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