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 { name: String },
89}
90
91/// Declarative build command — NOT raw shell. Renderers translate
92/// to the build system's preferred shape (Nix derivation phases,
93/// Bazel `genrule`, …).
94#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(tag = "kind", rename_all = "kebab-case")]
96pub enum BuildCommand {
97    /// A typed cargo invocation: `cargo build --release` etc.
98    Cargo {
99        subcommand: String,
100        args: Vec<String>,
101    },
102    /// A typed npm/pnpm/yarn invocation.
103    Npm {
104        manager: String,
105        subcommand: String,
106        args: Vec<String>,
107    },
108    /// A typed pip invocation.
109    Pip {
110        subcommand: String,
111        args: Vec<String>,
112    },
113    /// A typed gem invocation.
114    Gem {
115        subcommand: String,
116        args: Vec<String>,
117    },
118    /// A typed go invocation.
119    Go {
120        subcommand: String,
121        args: Vec<String>,
122    },
123    /// A typed `make`-shaped target.
124    Make { target: String, env: indexmap::IndexMap<String, String> },
125    /// A typed file copy step (Fetch / Install phase).
126    Copy { from: String, to: String },
127    /// A typed mkdir step.
128    Mkdir { path: String },
129    /// A typed symlink step.
130    Symlink { target: String, link_name: String },
131    /// Adapter-supplied native command — used as the last resort
132    /// when the typed variants don't cover it. Renderer is
133    /// expected to translate, not pass through raw.
134    Native {
135        program: String,
136        args: Vec<String>,
137    },
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::Registry;
144
145    #[test]
146    fn derivation_round_trips_through_serde() {
147        let d = Derivation {
148            name: "serde-1.0.228".into(),
149            version: Version::new(1, 0, 228),
150            package_id: PackageId {
151                name: "serde".into(),
152                version: Version::new(1, 0, 228),
153                registry: Registry::CratesIo,
154            },
155            inputs: vec![],
156            build: BuildScript {
157                steps: vec![BuildStep {
158                    kind: BuildStepKind::Build,
159                    command: BuildCommand::Cargo {
160                        subcommand: "build".into(),
161                        args: vec!["--release".into()],
162                    },
163                }],
164                env: indexmap::IndexMap::new(),
165            },
166            cache_key: ContentHash::of(b"snapshot"),
167        };
168        let j = serde_json::to_string(&d).unwrap();
169        let parsed: Derivation = serde_json::from_str(&j).unwrap();
170        assert_eq!(d, parsed);
171    }
172
173    #[test]
174    fn build_command_variants_round_trip() {
175        let commands = vec![
176            BuildCommand::Cargo {
177                subcommand: "build".into(),
178                args: vec![],
179            },
180            BuildCommand::Npm {
181                manager: "pnpm".into(),
182                subcommand: "install".into(),
183                args: vec![],
184            },
185            BuildCommand::Copy {
186                from: "/src".into(),
187                to: "/out".into(),
188            },
189        ];
190        for c in commands {
191            let j = serde_json::to_string(&c).unwrap();
192            let parsed: BuildCommand = serde_json::from_str(&j).unwrap();
193            assert_eq!(c, parsed);
194        }
195    }
196}