Skip to main content

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}