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}