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}