Skip to main content

gen_types/
derivation.rs

1//! Typed [`Derivation`] — the renderer-side output.
2//!
3//! Every renderer (`gen-nix-incremental`, `gen-nix-bulk`, `gen-bazel`,
4//! `gen-buck`) emits these. Cache backends key on
5//! `Derivation.cache_key`. Build systems consume `Derivation.build`.
6//!
7//! Per `theory/NIX-AST.md`: the Nix renderer never `format!()`s nix
8//! syntax — it emits a typed AST that pretty-prints deterministically.
9//! Other build systems get their own typed AST module under their
10//! renderer crate.
11
12use crate::lockfile::ContentHash;
13use crate::{PackageId, Version};
14use serde::{Deserialize, Serialize};
15
16/// One derivation in the renderer's output graph. Build system
17/// agnostic.
18#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
19pub struct Derivation {
20    pub name: String,
21    pub version: Version,
22    /// Source package this derivation was emitted for (one-to-one).
23    pub package_id: PackageId,
24    /// Inputs the build needs (resolved at the renderer's layer).
25    pub inputs: Vec<DerivationRef>,
26    /// Ordered build steps the build system executes.
27    pub build: BuildScript,
28    /// Canonical hash for cache-backend lookup. Computed by the
29    /// renderer over the derivation's content-addressed inputs +
30    /// build script — so a cache backend can answer "has this
31    /// already been built?" without consulting the source.
32    pub cache_key: ContentHash,
33}
34
35/// Reference to another derivation by its cache key. Renderers
36/// resolve these into per-build-system specific shapes (Nix store
37/// paths, Bazel targets, …).
38#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
39pub struct DerivationRef {
40    pub name: String,
41    pub cache_key: ContentHash,
42}
43
44/// Typed build script — a sequence of [`BuildStep`]s + the
45/// environment they share.
46#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
47pub struct BuildScript {
48    pub steps: Vec<BuildStep>,
49    /// Environment variables exported across every step.
50    #[serde(default)]
51    pub env: indexmap::IndexMap<String, String>,
52}
53
54/// One phase of the build script.
55#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
56pub struct BuildStep {
57    pub kind: BuildStepKind,
58    pub command: BuildCommand,
59}
60
61/// Typed enum of standard build phases. Adapters / renderers don't
62/// invent new variants; they pick from this list. Pre/PostX hooks
63/// are the only place adapter-specific logic surfaces.
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "PascalCase")]
66pub enum BuildStepKind {
67    Fetch,
68    PreUnpack,
69    Unpack,
70    PostUnpack,
71    Patch,
72    PreConfigure,
73    Configure,
74    PostConfigure,
75    PreBuild,
76    Build,
77    PostBuild,
78    PreCheck,
79    Check,
80    PostCheck,
81    PreInstall,
82    Install,
83    PostInstall,
84    /// Custom adapter-defined phase. Engine treats it as opaque +
85    /// runs it at the position the adapter requested. Name is the
86    /// adapter's chosen label (e.g. `"strip"`, `"sign"`,
87    /// `"cargo:rustc-link-lib"`).
88    Custom {
89        name: String,
90    },
91}
92
93/// Declarative build command — NOT raw shell. Renderers translate
94/// to the build system's preferred shape (Nix derivation phases,
95/// Bazel `genrule`, …).
96#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(tag = "kind", rename_all = "kebab-case")]
98pub enum BuildCommand {
99    /// A typed cargo invocation: `cargo build --release` etc.
100    Cargo {
101        subcommand: String,
102        args: Vec<String>,
103    },
104    /// A typed npm/pnpm/yarn invocation.
105    Npm {
106        manager: String,
107        subcommand: String,
108        args: Vec<String>,
109    },
110    /// A typed pip invocation.
111    Pip {
112        subcommand: String,
113        args: Vec<String>,
114    },
115    /// A typed gem invocation.
116    Gem {
117        subcommand: String,
118        args: Vec<String>,
119    },
120    /// A typed go invocation.
121    Go {
122        subcommand: String,
123        args: Vec<String>,
124    },
125    /// A typed `make`-shaped target.
126    Make {
127        target: String,
128        env: indexmap::IndexMap<String, String>,
129    },
130    /// A typed file copy step (Fetch / Install phase).
131    Copy { from: String, to: String },
132    /// A typed mkdir step.
133    Mkdir { path: String },
134    /// A typed symlink step.
135    Symlink { target: String, link_name: String },
136    /// Adapter-supplied native command — used as the last resort
137    /// when the typed variants don't cover it. Renderer is
138    /// expected to translate, not pass through raw.
139    Native { program: String, args: Vec<String> },
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::Registry;
146
147    #[test]
148    fn derivation_round_trips_through_serde() {
149        let d = Derivation {
150            name: "serde-1.0.228".into(),
151            version: Version::new(1, 0, 228),
152            package_id: PackageId {
153                name: "serde".into(),
154                version: Version::new(1, 0, 228),
155                registry: Registry::CratesIo,
156            },
157            inputs: vec![],
158            build: BuildScript {
159                steps: vec![BuildStep {
160                    kind: BuildStepKind::Build,
161                    command: BuildCommand::Cargo {
162                        subcommand: "build".into(),
163                        args: vec!["--release".into()],
164                    },
165                }],
166                env: indexmap::IndexMap::new(),
167            },
168            cache_key: ContentHash::of(b"snapshot"),
169        };
170        let j = serde_json::to_string(&d).unwrap();
171        let parsed: Derivation = serde_json::from_str(&j).unwrap();
172        assert_eq!(d, parsed);
173    }
174
175    #[test]
176    fn build_command_variants_round_trip() {
177        let commands = vec![
178            BuildCommand::Cargo {
179                subcommand: "build".into(),
180                args: vec![],
181            },
182            BuildCommand::Npm {
183                manager: "pnpm".into(),
184                subcommand: "install".into(),
185                args: vec![],
186            },
187            BuildCommand::Copy {
188                from: "/src".into(),
189                to: "/out".into(),
190            },
191        ];
192        for c in commands {
193            let j = serde_json::to_string(&c).unwrap();
194            let parsed: BuildCommand = serde_json::from_str(&j).unwrap();
195            assert_eq!(c, parsed);
196        }
197    }
198}