snix_build_glue/build_state.rs
1use std::{cell::RefCell, io, sync::Arc};
2
3use futures::{StreamExt, TryStreamExt as _};
4use nix_compat::{
5 nixhash::CAHash,
6 store_path::{StorePath, StorePathRef},
7};
8use snix_build::buildservice::{BuildService, BuildUpdate};
9use snix_castore::{
10 blobservice::BlobService,
11 directoryservice::{DirectoryService, traversal::descend_to},
12};
13use snix_store::{
14 nar::NarCalculationService, path_info::PathInfo, pathinfoservice::PathInfoService,
15};
16use tracing::{Level, Span, instrument, warn};
17use tracing_indicatif::span_ext::IndicatifSpanExt;
18use url::Url;
19
20use crate::{builder, fetchers::Fetcher, known_paths::KnownPaths};
21
22pub struct BuildState {
23 // This is public so helper functions can interact with the stores directly.
24 pub blob_service: Arc<dyn BlobService>,
25 pub directory_service: Arc<dyn DirectoryService>,
26 pub path_info_service: Arc<dyn PathInfoService>,
27 pub nar_calculation_service: Arc<dyn NarCalculationService>,
28
29 #[allow(dead_code)]
30 build_service: Arc<dyn BuildService>,
31
32 #[allow(clippy::type_complexity)]
33 pub fetcher: Fetcher<
34 Arc<dyn BlobService>,
35 Arc<dyn DirectoryService>,
36 Arc<dyn PathInfoService>,
37 Arc<dyn NarCalculationService>,
38 >,
39
40 // Paths known how to produce, by building or fetching.
41 pub known_paths: RefCell<KnownPaths>,
42}
43
44impl BuildState {
45 pub fn new(
46 blob_service: Arc<dyn BlobService>,
47 directory_service: Arc<dyn DirectoryService>,
48 path_info_service: Arc<dyn PathInfoService>,
49 nar_calculation_service: Arc<dyn NarCalculationService>,
50 build_service: Arc<dyn BuildService>,
51 hashed_mirrors: Vec<Url>,
52 ) -> Self {
53 Self {
54 blob_service: blob_service.clone(),
55 directory_service: directory_service.clone(),
56 path_info_service: path_info_service.clone(),
57 nar_calculation_service: nar_calculation_service.clone(),
58 build_service,
59 fetcher: Fetcher::new(
60 blob_service,
61 directory_service,
62 path_info_service,
63 nar_calculation_service,
64 hashed_mirrors,
65 ),
66 known_paths: Default::default(),
67 }
68 }
69
70 /// for a given [StorePath] and additional [snix_castore::Path] inside the store path,
71 /// look up the [PathInfo], and if it exists, and then uses
72 /// [descend_to] to return the [snix_castore::Node] specified by `sub_path`.
73 ///
74 /// In case there is no PathInfo yet, we check "self.known_paths" (learnt by
75 /// the evaluator)
76 /// - If there's a fetch, we do that and return the resulting PathInfo.
77 /// - If there's a Derivation, we build it and return the resulting
78 /// PathInfo.
79 /// To build it, we need to do a function call to ourselves with all
80 /// inputs of that Derivation, which will trigger fetches / builds of
81 /// inputs, recursively.
82 ///
83 /// This should be replaced with a proper scheduler knowing about a partial
84 /// subgraph at some point, because this design doesn't allow concurrent
85 /// builds yet.
86 #[instrument(skip(self, store_path), fields(store_path=%store_path, indicatif.pb_show=tracing::field::Empty), ret(level = Level::TRACE), err(level = Level::TRACE))]
87 pub async fn store_path_to_path_info(
88 &self,
89 store_path: &StorePathRef<'_>,
90 sub_path: &snix_castore::Path,
91 ) -> io::Result<Option<PathInfo>> {
92 Span::current().pb_set_message(&format!("Accessing {store_path}/{sub_path}"));
93
94 // Find the root node for the store_path.
95 // It asks the PathInfoService first, but in case there was a Derivation
96 // produced that would build it, fall back to triggering the build.
97 // To populate the input nodes, it might recursively trigger builds of
98 // its dependencies too.
99 let mut path_info = if let Some(path_info) = self
100 .path_info_service
101 .as_ref()
102 .get(*store_path.digest())
103 .await
104 .map_err(std::io::Error::other)?
105 {
106 path_info
107 } else {
108 // If there's no PathInfo found, this normally means we have to
109 // trigger the build (and insert into PathInfoService, after
110 // reference scanning).
111 // However, as Snix is (currently) not managing /nix/store itself,
112 // we return Ok(None) to let std_io take over.
113 // While reading from store paths that are not known to Snix during
114 // that evaluation clearly is an impurity, we still need to support
115 // it for things like <nixpkgs> pointing to a store path.
116 // In the future, these things will (need to) have PathInfo.
117 //
118 // The store path doesn't exist yet, so we need to fetch or build it.
119 // We check for fetches first, as we might have both native
120 // fetchers and FODs in KnownPaths, and prefer the former.
121 // This will also find [Fetch] synthesized from
122 // `builtin:fetchurl` Derivations.
123 let maybe_fetch = self
124 .known_paths
125 .borrow()
126 .get_fetch_for_output_path(store_path);
127 if let Some((name, fetch)) = maybe_fetch {
128 let (sp, path_info) = self
129 .fetcher
130 .ingest_and_persist(&name, fetch)
131 .await
132 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
133
134 debug_assert_eq!(
135 sp.to_absolute_path(),
136 store_path.to_absolute_path(),
137 "store path returned from fetcher must match store path we have in fetchers"
138 );
139
140 path_info
141 } else {
142 // Look up the derivation for this output path.
143 let (drv_path, drv) = {
144 let known_paths = self.known_paths.borrow();
145 if let Some(drv_path) = known_paths.get_drv_path_for_output_path(store_path) {
146 (
147 drv_path.to_owned(),
148 known_paths
149 .get_drv_by_drvpath(&drv_path.as_ref())
150 .unwrap()
151 .to_owned(),
152 )
153 } else {
154 warn!(store_path=%store_path, "no drv found");
155 // let StdIO take over
156 return Ok(None);
157 }
158 };
159 let span = Span::current();
160 span.pb_start();
161 span.pb_set_style(&snix_tracing::PB_SPINNER_STYLE);
162 span.pb_set_message(&format!("⏳Waiting for inputs {}", store_path));
163
164 // derivation_to_build_request needs castore nodes for all inputs.
165 // Provide them, which means, here is where we recursively build
166 // all dependencies.
167 let resolved_inputs = {
168 let known_paths = &self.known_paths.borrow();
169 builder::get_all_inputs(&drv, known_paths, |path| {
170 Box::pin(async move {
171 self.store_path_to_path_info(&path.as_ref(), snix_castore::Path::ROOT)
172 .await
173 })
174 })
175 }
176 .try_collect()
177 .await?;
178
179 // precompute the `ca` field in the PathInfo, so drv can be sent away owned.
180 let mut ca = drv.fod_digest().map(|fod_digest| {
181 CAHash::Nar(nix_compat::nixhash::NixHash::Sha256(fod_digest.into()))
182 });
183
184 // synthesize the build request.
185 let build_request = builder::derivation_into_build_request(drv, &resolved_inputs)?;
186
187 // Assemble a mapping table from needle back to store path, as well as a list of all outputs.
188 // The latter is a subset of the former.
189 // We need this to understand the response later.
190 let mut output_paths: Vec<StorePath> =
191 Vec::with_capacity(build_request.outputs.len());
192 let all_possible_refs: Vec<StorePath> = build_request
193 .outputs
194 .iter()
195 .map(|p| {
196 let sp = StorePath::from_bytes(
197 p.strip_prefix(&nix_compat::store_path::STORE_DIR_WITH_SLASH[1..])
198 .expect("output doesn't have expected store_dir prefix")
199 .as_bytes(),
200 )
201 .expect("Snix bug: cannot parse output as StorePath");
202 output_paths.push(sp.clone());
203
204 sp
205 })
206 .chain(resolved_inputs.keys().cloned())
207 .collect();
208
209 span.pb_set_message(&format!("🔨Building {}", store_path));
210
211 // Create a build, await its completion and return a list of output data.
212 struct ProducedOutput<'s> {
213 output_path: StorePathRef<'s>,
214 node: snix_castore::Node,
215 references: Vec<StorePath>,
216 }
217
218 let produced_outputs: Vec<ProducedOutput> = {
219 let mut produced_outputs: Vec<ProducedOutput> =
220 Vec::with_capacity(output_paths.len());
221 let mut build_updates = self.build_service.do_build(build_request);
222
223 while let Some(build_update) = build_updates.next().await {
224 match build_update {
225 BuildUpdate::ProducedOutput {
226 node,
227 idx,
228 refscan_needles,
229 } => {
230 let output_path = if idx >= output_paths.len() as u64 {
231 return Err(std::io::Error::other("invalid idx"))?;
232 } else {
233 output_paths[idx as usize].as_ref()
234 };
235
236 // ensure we only receive each ProducedOutput once.
237 if produced_outputs
238 .iter()
239 .any(|e| e.output_path == output_path)
240 {
241 return Err(std::io::Error::other(
242 "received output multiple times",
243 ));
244 }
245
246 produced_outputs.push(ProducedOutput {
247 output_path,
248 node,
249 references: {
250 let mut references =
251 Vec::with_capacity(refscan_needles.len());
252
253 // Map each output needle index back into a store path.
254 for needle_idx in refscan_needles {
255 let output = all_possible_refs
256 .get(needle_idx as usize)
257 .ok_or(std::io::Error::other("invalid needle_idx"))?
258 .clone();
259 references.push(output);
260 }
261
262 // Produce references sorted by name for consistency with nix narinfos
263 references.sort();
264 references
265 },
266 })
267 }
268 BuildUpdate::ProducedStdout(_) | BuildUpdate::ProducedStderr(_) => {
269 // no output printed for now
270 }
271 BuildUpdate::BuildFailure(build_failure) => {
272 Err(std::io::Error::other(build_failure))?
273 }
274 }
275 }
276
277 // ensure all outputs have been produced.
278 if produced_outputs.len() != output_paths.len() {
279 return Err(std::io::Error::other("did not receive all outputs"));
280 }
281
282 produced_outputs
283 };
284 // FUTUREWORK: more validation?
285
286 let mut out_path_info: Option<PathInfo> = None;
287
288 // For each produced output, insert a PathInfo.
289 // FUTUREWORK: check references for cycles and upload in the correct order.
290 for ProducedOutput {
291 output_path,
292 node,
293 references,
294 } in produced_outputs
295 {
296 // calculate the nar representation
297 let (nar_size, nar_sha256) = self
298 .nar_calculation_service
299 .calculate_nar(&node)
300 .await
301 .map_err(std::io::Error::other)?;
302
303 // assemble the PathInfo to persist
304 let path_info = PathInfo {
305 store_path: output_path.to_owned(),
306 node,
307 references,
308 nar_size,
309 nar_sha256,
310 signatures: vec![],
311 deriver: Some(
312 StorePath::from_name_and_digest_fixed(
313 drv_path
314 .name()
315 .strip_suffix(".drv")
316 .expect("missing .drv suffix"),
317 *drv_path.digest(),
318 )
319 .expect("Snix bug: StorePath without .drv suffix must be valid"),
320 ),
321 // CA derivations only have one output, so this only runs once.
322 ca: ca.take(),
323 };
324
325 self.path_info_service
326 .put(path_info.clone())
327 .await
328 .map_err(std::io::Error::other)?;
329
330 if *store_path == output_path {
331 out_path_info = Some(path_info);
332 }
333 }
334
335 out_path_info.ok_or(io::Error::other("build didn't produce store path"))?
336 }
337 };
338
339 // now with the root_node and sub_path, descend to the node requested.
340 Ok(
341 descend_to(&self.directory_service, path_info.node.clone(), sub_path)
342 .await
343 .map_err(std::io::Error::other)?
344 .map(|node| {
345 path_info.node = node;
346 path_info
347 }),
348 )
349 }
350}