nix_bindings_store/
store.rs

1use anyhow::{bail, Error, Result};
2use nix_bindings_store_sys as raw;
3use nix_bindings_util::context::Context;
4use nix_bindings_util::string_return::{
5    callback_get_result_string, callback_get_result_string_data,
6};
7use nix_bindings_util::{check_call, result_string_init};
8use nix_bindings_util_sys as raw_util;
9#[cfg(nix_at_least = "2.33.0pre")]
10use std::collections::BTreeMap;
11use std::collections::HashMap;
12use std::ffi::{c_char, CString};
13use std::ptr::null_mut;
14use std::ptr::NonNull;
15use std::sync::{Arc, LazyLock, Mutex, Weak};
16
17#[cfg(nix_at_least = "2.33.0pre")]
18use crate::derivation::Derivation;
19use crate::path::StorePath;
20
21/* TODO make Nix itself thread safe */
22static INIT: LazyLock<Result<()>> = LazyLock::new(|| unsafe {
23    check_call!(raw::libstore_init(&mut Context::new()))?;
24    Ok(())
25});
26
27struct StoreRef {
28    inner: NonNull<raw::Store>,
29}
30impl StoreRef {
31    /// # Safety
32    ///
33    /// The returned pointer is only valid as long as the `StoreRef` is alive.
34    pub unsafe fn ptr(&self) -> *mut raw::Store {
35        self.inner.as_ptr()
36    }
37}
38impl Drop for StoreRef {
39    fn drop(&mut self) {
40        unsafe {
41            raw::store_free(self.inner.as_ptr());
42        }
43    }
44}
45unsafe impl Send for StoreRef {}
46/// Unlike pointers in general, operations on raw::Store are thread safe and it is therefore safe to share them between threads.
47unsafe impl Sync for StoreRef {}
48
49/// A [Weak] reference to a store.
50pub struct StoreWeak {
51    inner: Weak<StoreRef>,
52}
53impl StoreWeak {
54    /// Upgrade the weak reference to a proper [Store].
55    ///
56    /// If no normal reference to the [Store] is around anymore elsewhere, this fails by returning `None`.
57    pub fn upgrade(&self) -> Option<Store> {
58        self.inner.upgrade().map(|inner| Store {
59            inner,
60            context: Context::new(),
61        })
62    }
63}
64
65/// Protects against https://github.com/NixOS/nix/issues/11979 (unless different parameters are passed, in which case it's up to luck, but you do get your own parameters as you asked for).
66type StoreCacheMap = HashMap<(Option<String>, Vec<(String, String)>), StoreWeak>;
67
68static STORE_CACHE: LazyLock<Arc<Mutex<StoreCacheMap>>> =
69    LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
70
71#[cfg(nix_at_least = "2.33.0pre")]
72unsafe extern "C" fn callback_get_result_store_path_set(
73    _context: *mut raw_util::c_context,
74    user_data: *mut std::os::raw::c_void,
75    store_path: *const raw::StorePath,
76) {
77    let ret = user_data as *mut Vec<StorePath>;
78    let ret: &mut Vec<StorePath> = &mut *ret;
79
80    let store_path = raw::store_path_clone(store_path);
81
82    let store_path =
83        NonNull::new(store_path).expect("nix_store_parse_path returned a null pointer");
84    let store_path = StorePath::new_raw(store_path);
85    ret.push(store_path);
86}
87
88#[cfg(nix_at_least = "2.33.0pre")]
89fn callback_get_result_store_path_set_data(vec: &mut Vec<StorePath>) -> *mut std::os::raw::c_void {
90    vec as *mut Vec<StorePath> as *mut std::os::raw::c_void
91}
92
93pub struct Store {
94    inner: Arc<StoreRef>,
95    /* An error context to reuse. This way we don't have to allocate them for each store operation. */
96    context: Context,
97}
98impl Store {
99    /// Open a store.
100    ///
101    /// See [`nix_bindings_store_sys::store_open`] for more information.
102    #[doc(alias = "nix_store_open")]
103    pub fn open<'a, 'b>(
104        url: Option<&str>,
105        params: impl IntoIterator<Item = (&'a str, &'b str)>,
106    ) -> Result<Self> {
107        let params = params
108            .into_iter()
109            .map(|(k, v)| (k.to_owned(), v.to_owned()))
110            .collect::<Vec<(String, String)>>();
111        let params2 = params.clone();
112        let mut store_cache = STORE_CACHE
113            .lock()
114            .map_err(|_| Error::msg("Failed to lock store cache. This should never happen."))?;
115        match store_cache.entry((url.map(Into::into), params)) {
116            std::collections::hash_map::Entry::Occupied(mut e) => {
117                if let Some(store) = e.get().upgrade() {
118                    Ok(store)
119                } else {
120                    let store = Self::open_uncached(
121                        url,
122                        params2.iter().map(|(k, v)| (k.as_str(), v.as_str())),
123                    )?;
124                    e.insert(store.weak_ref());
125                    Ok(store)
126                }
127            }
128            std::collections::hash_map::Entry::Vacant(e) => {
129                let store = Self::open_uncached(
130                    url,
131                    params2.iter().map(|(k, v)| (k.as_str(), v.as_str())),
132                )?;
133                e.insert(store.weak_ref());
134                Ok(store)
135            }
136        }
137    }
138    fn open_uncached<'a, 'b>(
139        url: Option<&str>,
140        params: impl IntoIterator<Item = (&'a str, &'b str)>,
141    ) -> Result<Self> {
142        let x = INIT.as_ref();
143        match x {
144            Ok(_) => {}
145            Err(e) => {
146                // Couldn't just clone the error, so we have to print it here.
147                bail!("nix_libstore_init error: {}", e);
148            }
149        }
150
151        let mut context: Context = Context::new();
152
153        let uri_cstring = match url {
154            Some(url) => Some(CString::new(url)?),
155            None => None,
156        };
157        let uri_ptr = uri_cstring
158            .as_ref()
159            .map(|s| s.as_ptr())
160            .unwrap_or(null_mut());
161
162        // this intermediate value must be here and must not be moved
163        // because it owns the data the `*const c_char` pointers point to.
164        let params: Vec<(CString, CString)> = params
165            .into_iter()
166            .map(|(k, v)| Ok((CString::new(k)?, CString::new(v)?))) // to do. context
167            .collect::<Result<_>>()?;
168        // this intermediate value owns the data the `*mut *const c_char` pointer points to.
169        let mut params: Vec<_> = params
170            .iter()
171            .map(|(k, v)| [k.as_ptr(), v.as_ptr()])
172            .collect();
173        // this intermediate value owns the data the `*mut *mut *const c_char` pointer points to.
174        let mut params: Vec<*mut *const c_char> = params
175            .iter_mut()
176            .map(|t| t.as_mut_ptr())
177            .chain(std::iter::once(null_mut())) // signal the end of the array
178            .collect();
179
180        let store =
181            unsafe { check_call!(raw::store_open(&mut context, uri_ptr, params.as_mut_ptr())) }?;
182        if store.is_null() {
183            panic!("nix_c_store_open returned a null pointer without an error");
184        }
185        let store = Store {
186            inner: Arc::new(StoreRef {
187                inner: NonNull::new(store).unwrap(),
188            }),
189            context,
190        };
191        Ok(store)
192    }
193
194    /// # Safety
195    ///
196    /// The returned pointer is only valid as long as the `Store` is alive.
197    pub unsafe fn raw_ptr(&self) -> *mut raw::Store {
198        self.inner.ptr()
199    }
200
201    #[doc(alias = "nix_store_get_uri")]
202    pub fn get_uri(&mut self) -> Result<String> {
203        let mut r = result_string_init!();
204        unsafe {
205            check_call!(raw::store_get_uri(
206                &mut self.context,
207                self.inner.ptr(),
208                Some(callback_get_result_string),
209                callback_get_result_string_data(&mut r)
210            ))
211        }?;
212        r
213    }
214
215    #[cfg(nix_at_least = "2.26")]
216    #[doc(alias = "nix_store_get_storedir")]
217    pub fn get_storedir(&mut self) -> Result<String> {
218        let mut r = result_string_init!();
219        unsafe {
220            check_call!(raw::store_get_storedir(
221                &mut self.context,
222                self.inner.ptr(),
223                Some(callback_get_result_string),
224                callback_get_result_string_data(&mut r)
225            ))
226        }?;
227        r
228    }
229
230    #[doc(alias = "nix_store_parse_path")]
231    pub fn parse_store_path(&mut self, path: &str) -> Result<StorePath> {
232        let path = CString::new(path)?;
233        unsafe {
234            let store_path = check_call!(raw::store_parse_path(
235                &mut self.context,
236                self.inner.ptr(),
237                path.as_ptr()
238            ))?;
239            let store_path =
240                NonNull::new(store_path).expect("nix_store_parse_path returned a null pointer");
241            Ok(StorePath::new_raw(store_path))
242        }
243    }
244
245    #[doc(alias = "nix_store_real_path")]
246    pub fn real_path(&mut self, path: &StorePath) -> Result<String> {
247        let mut r = result_string_init!();
248        unsafe {
249            check_call!(raw::store_real_path(
250                &mut self.context,
251                self.inner.ptr(),
252                path.as_ptr(),
253                Some(callback_get_result_string),
254                callback_get_result_string_data(&mut r)
255            ))
256        }?;
257        r
258    }
259
260    /// Parse a derivation from JSON.
261    ///
262    /// **Requires Nix 2.33 or later.**
263    ///
264    /// The JSON format follows the [Nix derivation JSON schema](https://nix.dev/manual/nix/latest/protocols/json/derivation.html).
265    /// Note that this format is experimental as of writing.
266    /// The derivation is not added to the store; use [`Store::add_derivation`] for that.
267    ///
268    /// # Parameters
269    /// - `json`: A JSON string representing the derivation
270    ///
271    /// # Returns
272    /// A [`Derivation`] object if parsing succeeds, or an error if the JSON is invalid
273    /// or malformed.
274    #[cfg(nix_at_least = "2.33.0pre")]
275    #[doc(alias = "nix_derivation_from_json")]
276    pub fn derivation_from_json(&mut self, json: &str) -> Result<Derivation> {
277        let json_cstr = CString::new(json)?;
278        unsafe {
279            let drv = check_call!(raw::derivation_from_json(
280                &mut self.context,
281                self.inner.ptr(),
282                json_cstr.as_ptr()
283            ))?;
284            let inner = NonNull::new(drv)
285                .ok_or_else(|| Error::msg("derivation_from_json returned null"))?;
286            Ok(Derivation::new_raw(inner))
287        }
288    }
289
290    /// Add a derivation to the store.
291    ///
292    /// **Requires Nix 2.33 or later.**
293    ///
294    /// This computes the store path for the derivation and registers it in the store.
295    /// The derivation itself is written to the store as a `.drv` file.
296    ///
297    /// # Parameters
298    /// - `drv`: The derivation to add
299    ///
300    /// # Returns
301    /// The store path of the derivation (ending in `.drv`).
302    #[cfg(nix_at_least = "2.33.0pre")]
303    #[doc(alias = "nix_add_derivation")]
304    pub fn add_derivation(&mut self, drv: &Derivation) -> Result<StorePath> {
305        unsafe {
306            let path = check_call!(raw::add_derivation(
307                &mut self.context,
308                self.inner.ptr(),
309                drv.inner.as_ptr()
310            ))?;
311            let path =
312                NonNull::new(path).ok_or_else(|| Error::msg("add_derivation returned null"))?;
313            Ok(StorePath::new_raw(path))
314        }
315    }
316
317    /// Build a derivation and return its outputs.
318    ///
319    /// **Requires Nix 2.33 or later.**
320    ///
321    /// This builds the derivation at the given store path and returns a map of output
322    /// names to their realized store paths. The derivation must already exist in the store
323    /// (see [`Store::add_derivation`]).
324    ///
325    /// # Parameters
326    /// - `path`: The store path of the derivation to build (typically ending in `.drv`)
327    ///
328    /// # Returns
329    /// A [`BTreeMap`] mapping output names (e.g., "out", "dev", "doc") to their store paths.
330    /// The map is ordered alphabetically by output name for deterministic iteration.
331    #[cfg(nix_at_least = "2.33.0pre")]
332    #[doc(alias = "nix_store_realise")]
333    pub fn realise(&mut self, path: &StorePath) -> Result<BTreeMap<String, StorePath>> {
334        let mut outputs = BTreeMap::new();
335        let userdata =
336            &mut outputs as *mut BTreeMap<String, StorePath> as *mut std::os::raw::c_void;
337
338        unsafe extern "C" fn callback(
339            userdata: *mut std::os::raw::c_void,
340            outname: *const c_char,
341            out_path: *const raw::StorePath,
342        ) {
343            let outputs = userdata as *mut BTreeMap<String, StorePath>;
344            let outputs = &mut *outputs;
345
346            let name = std::ffi::CStr::from_ptr(outname)
347                .to_string_lossy()
348                .into_owned();
349
350            let path = raw::store_path_clone(out_path);
351            let path = NonNull::new(path).expect("store_path_clone returned null");
352            let path = StorePath::new_raw(path);
353
354            outputs.insert(name, path);
355        }
356
357        unsafe {
358            check_call!(raw::store_realise(
359                &mut self.context,
360                self.inner.ptr(),
361                path.as_ptr(),
362                userdata,
363                Some(callback)
364            ))?;
365        }
366
367        Ok(outputs)
368    }
369
370    /// Get the closure of a specific store path.
371    ///
372    /// **Requires Nix 2.33 or later.**
373    ///
374    /// Computes the filesystem closure (dependency graph) of a store path, with options
375    /// to control the direction and which related paths to include.
376    ///
377    /// # Parameters
378    /// - `store_path`: The path to compute the closure from
379    /// - `flip_direction`: If false, compute the forward closure (paths referenced by this path).
380    ///   If true, compute the backward closure (paths that reference this path).
381    /// - `include_outputs`: When `flip_direction` is false: for any derivation in the closure, include its outputs.
382    ///   When `flip_direction` is true: for any output in the closure, include derivations that produce it.
383    /// - `include_derivers`: When `flip_direction` is false: for any output in the closure, include the derivation that produced it.
384    ///   When `flip_direction` is true: for any derivation in the closure, include its outputs.
385    ///
386    /// # Returns
387    /// A vector of store paths in the closure, in no particular order.
388    #[cfg(nix_at_least = "2.33.0pre")]
389    #[doc(alias = "nix_store_get_fs_closure")]
390    pub fn get_fs_closure(
391        &mut self,
392        store_path: &StorePath,
393        flip_direction: bool,
394        include_outputs: bool,
395        include_derivers: bool,
396    ) -> Result<Vec<StorePath>> {
397        let mut r = Vec::new();
398        unsafe {
399            check_call!(raw::store_get_fs_closure(
400                &mut self.context,
401                self.inner.ptr(),
402                store_path.as_ptr(),
403                flip_direction,
404                include_outputs,
405                include_derivers,
406                callback_get_result_store_path_set_data(&mut r),
407                Some(callback_get_result_store_path_set)
408            ))
409        }?;
410        Ok(r)
411    }
412
413    pub fn weak_ref(&self) -> StoreWeak {
414        StoreWeak {
415            inner: Arc::downgrade(&self.inner),
416        }
417    }
418}
419
420impl Clone for Store {
421    fn clone(&self) -> Self {
422        Store {
423            inner: self.inner.clone(),
424            context: Context::new(),
425        }
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use ctor::ctor;
432    use std::collections::HashMap;
433
434    use super::*;
435
436    #[ctor]
437    fn test_setup() {
438        // Initialize settings for tests
439        let _ = INIT.as_ref();
440
441        // Enable ca-derivations for all tests
442        nix_bindings_util::settings::set("experimental-features", "ca-derivations").ok();
443
444        // Disable build hooks to prevent test recursion
445        nix_bindings_util::settings::set("build-hook", "").ok();
446
447        // Set custom build dir for sandbox
448        if cfg!(target_os = "linux") {
449            nix_bindings_util::settings::set("sandbox-build-dir", "/custom-build-dir-for-test")
450                .ok();
451        }
452
453        std::env::set_var("_NIX_TEST_NO_SANDBOX", "1");
454
455        // Tests run offline
456        nix_bindings_util::settings::set("substituters", "").ok();
457    }
458
459    #[test]
460    fn none_works() {
461        let res = Store::open(None, HashMap::new());
462        res.unwrap();
463    }
464
465    #[test]
466    fn auto_works() {
467        // This is not actually a given.
468        // Maybe whatever is in NIX_REMOTE or nix.conf is really important.
469        let res = Store::open(Some("auto"), HashMap::new());
470        res.unwrap();
471    }
472
473    #[test]
474    fn invalid_uri_fails() {
475        let res = Store::open(Some("invalid://uri"), HashMap::new());
476        assert!(res.is_err());
477    }
478
479    #[test]
480    fn get_uri() {
481        let mut store = Store::open(None, HashMap::new()).unwrap();
482        let uri = store.get_uri().unwrap();
483        assert!(!uri.is_empty());
484        // must be ascii
485        assert!(uri.is_ascii());
486        // usually something like "daemon", but that's not something we can check here.
487        println!("uri: {}", uri);
488    }
489
490    #[test]
491    #[ignore] // Needs network access
492    fn get_uri_nixos_cache() {
493        let mut store = Store::open(Some("https://cache.nixos.org/"), HashMap::new()).unwrap();
494        let uri = store.get_uri().unwrap();
495        assert_eq!(uri, "https://cache.nixos.org");
496    }
497
498    #[test]
499    #[cfg(nix_at_least = "2.26" /* get_storedir */)]
500    fn parse_store_path_ok() {
501        let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap();
502        let store_dir = store.get_storedir().unwrap();
503        let store_path_string =
504            format!("{store_dir}/rdd4pnr4x9rqc9wgbibhngv217w2xvxl-bash-interactive-5.2p26");
505        let store_path = store.parse_store_path(store_path_string.as_str()).unwrap();
506        let real_store_path = store.real_path(&store_path).unwrap();
507        assert_eq!(store_path.name().unwrap(), "bash-interactive-5.2p26");
508        assert_eq!(real_store_path, store_path_string);
509    }
510
511    #[test]
512    fn parse_store_path_fail() {
513        let mut store = crate::store::Store::open(Some("dummy://"), []).unwrap();
514        let store_path_string = "bash-interactive-5.2p26".to_string();
515        let r = store.parse_store_path(store_path_string.as_str());
516        match r {
517            Err(e) => {
518                assert!(e.to_string().contains("bash-interactive-5.2p26"));
519            }
520            _ => panic!("Expected error"),
521        }
522    }
523
524    #[test]
525    fn weak_ref() {
526        let mut store = Store::open(None, HashMap::new()).unwrap();
527        let uri = store.get_uri().unwrap();
528        let weak = store.weak_ref();
529        let mut store2 = weak.upgrade().unwrap();
530        assert_eq!(store2.get_uri().unwrap(), uri);
531    }
532    #[test]
533    fn weak_ref_gone() {
534        let weak = {
535            // Concurrent tests calling Store::open will keep the weak reference to auto alive,
536            // so for this test we need to bypass the global cache.
537            let store = Store::open_uncached(None, HashMap::new()).unwrap();
538            store.weak_ref()
539        };
540        assert!(weak.upgrade().is_none());
541        assert!(weak.inner.upgrade().is_none());
542    }
543
544    #[cfg(nix_at_least = "2.33.0pre")]
545    fn create_temp_store() -> (Store, tempfile::TempDir) {
546        let temp_dir = tempfile::tempdir().unwrap();
547
548        let store_dir = temp_dir.path().join("store");
549        let state_dir = temp_dir.path().join("state");
550        let log_dir = temp_dir.path().join("log");
551
552        let store_dir_str = store_dir.to_str().unwrap();
553        let state_dir_str = state_dir.to_str().unwrap();
554        let log_dir_str = log_dir.to_str().unwrap();
555
556        let params = vec![
557            ("store", store_dir_str),
558            ("state", state_dir_str),
559            ("log", log_dir_str),
560        ];
561
562        let store = Store::open(Some("local"), params).unwrap();
563        (store, temp_dir)
564    }
565
566    fn current_system() -> Result<String> {
567        nix_bindings_util::settings::get("system")
568    }
569
570    #[cfg(nix_at_least = "2.33")]
571    fn create_test_derivation_json() -> serde_json::Value {
572        let system = current_system().unwrap_or_else(|_| {
573            // Fallback to Rust's platform detection
574            format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS)
575        });
576        serde_json::json!({
577            "args": ["-c", "echo $name foo > $out"],
578            "builder": "/bin/sh",
579            "env": {
580                "builder": "/bin/sh",
581                "name": "myname",
582                "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9",
583                "system": system
584            },
585            "inputs": {
586                "drvs": {},
587                "srcs": []
588            },
589            "name": "myname",
590            "outputs": {
591                "out": {
592                    "hashAlgo": "sha256",
593                    "method": "nar"
594                }
595            },
596            "system": system,
597            "version": 4
598        })
599    }
600
601    #[test]
602    #[cfg(nix_at_least = "2.33")]
603    fn derivation_from_json() {
604        let (mut store, temp_dir) = create_temp_store();
605        let drv_json = create_test_derivation_json();
606        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
607        // If we got here, parsing succeeded
608        drop(drv);
609        drop(store);
610        drop(temp_dir);
611    }
612
613    #[test]
614    #[cfg(nix_at_least = "2.33.0pre")]
615    fn derivation_from_invalid_json() {
616        let (mut store, temp_dir) = create_temp_store();
617        let result = store.derivation_from_json("not valid json");
618        assert!(result.is_err());
619        drop(store);
620        drop(temp_dir);
621    }
622
623    #[test]
624    #[cfg(nix_at_least = "2.33")]
625    fn derivation_to_json_round_trip() {
626        let (mut store, _temp_dir) = create_temp_store();
627        let original_value = create_test_derivation_json();
628
629        // Parse JSON to Derivation
630        let drv = store
631            .derivation_from_json(&original_value.to_string())
632            .unwrap();
633
634        // Convert back to JSON
635        let round_trip_json = drv.to_json_string().unwrap();
636        let round_trip_value: serde_json::Value = serde_json::from_str(&round_trip_json).unwrap();
637
638        // Verify the round-trip JSON matches the original
639        assert_eq!(
640            original_value, round_trip_value,
641            "Round-trip JSON should match original.\nOriginal: {}\nRound-trip: {}",
642            original_value, round_trip_value
643        );
644    }
645
646    #[test]
647    #[cfg(nix_at_least = "2.33")]
648    fn add_derivation() {
649        let (mut store, temp_dir) = create_temp_store();
650        let drv_json = create_test_derivation_json();
651        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
652        let drv_path = store.add_derivation(&drv).unwrap();
653
654        // Verify we got a .drv path
655        let name = drv_path.name().unwrap();
656        assert!(name.ends_with(".drv"));
657
658        drop(store);
659        drop(temp_dir);
660    }
661
662    #[test]
663    #[cfg(nix_at_least = "2.33")]
664    fn realise() {
665        let (mut store, temp_dir) = create_temp_store();
666        let drv_json = create_test_derivation_json();
667        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
668        let drv_path = store.add_derivation(&drv).unwrap();
669
670        // Build the derivation
671        let outputs = store.realise(&drv_path).unwrap();
672
673        // Verify we got the expected output
674        assert!(outputs.contains_key("out"));
675        let out_path = &outputs["out"];
676        let out_name = out_path.name().unwrap();
677        assert_eq!(out_name, "myname");
678
679        drop(store);
680        drop(temp_dir);
681    }
682
683    #[cfg(nix_at_least = "2.33")]
684    fn create_multi_output_derivation_json() -> serde_json::Value {
685        let system = current_system()
686            .unwrap_or_else(|_| format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS));
687
688        serde_json::json!({
689            "version": 4,
690            "name": "multi-output-test",
691            "system": system,
692            "builder": "/bin/sh",
693            "args": ["-c", "echo a > $outa; echo b > $outb; echo c > $outc; echo d > $outd; echo e > $oute; echo f > $outf; echo g > $outg; echo h > $outh; echo i > $outi; echo j > $outj"],
694            "env": {
695                "builder": "/bin/sh",
696                "name": "multi-output-test",
697                "system": system,
698                "outf": "/1vkfzqpwk313b51x0xjyh5s7w1lx141mr8da3dr9wqz5aqjyr2fh",
699                "outd": "/1ypxifgmbzp5sd0pzsp2f19aq68x5215260z3lcrmy5fch567lpm",
700                "outi": "/1wmasjnqi12j1mkjbxazdd0qd0ky6dh1qry12fk8qyp5kdamhbdx",
701                "oute": "/1f9r2k1s168js509qlw8a9di1qd14g5lqdj5fcz8z7wbqg11qp1f",
702                "outh": "/1rkx1hmszslk5nq9g04iyvh1h7bg8p92zw0hi4155hkjm8bpdn95",
703                "outc": "/1rj4nsf9pjjqq9jsq58a2qkwa7wgvgr09kgmk7mdyli6h1plas4w",
704                "outb": "/1p7i1dxifh86xq97m5kgb44d7566gj7rfjbw7fk9iij6ca4akx61",
705                "outg": "/14f8qi0r804vd6a6v40ckylkk1i6yl6fm243qp6asywy0km535lc",
706                "outj": "/0gkw1366qklqfqb2lw1pikgdqh3cmi3nw6f1z04an44ia863nxaz",
707                "outa": "/039akv9zfpihrkrv4pl54f3x231x362bll9afblsgfqgvx96h198"
708            },
709            "inputs": {
710                "drvs": {},
711                "srcs": []
712            },
713            "outputs": {
714                "outd": { "hashAlgo": "sha256", "method": "nar" },
715                "outf": { "hashAlgo": "sha256", "method": "nar" },
716                "outg": { "hashAlgo": "sha256", "method": "nar" },
717                "outb": { "hashAlgo": "sha256", "method": "nar" },
718                "outc": { "hashAlgo": "sha256", "method": "nar" },
719                "outi": { "hashAlgo": "sha256", "method": "nar" },
720                "outj": { "hashAlgo": "sha256", "method": "nar" },
721                "outh": { "hashAlgo": "sha256", "method": "nar" },
722                "outa": { "hashAlgo": "sha256", "method": "nar" },
723                "oute": { "hashAlgo": "sha256", "method": "nar" }
724            }
725        })
726    }
727
728    #[test]
729    #[cfg(nix_at_least = "2.33")]
730    fn realise_multi_output_ordering() {
731        let (mut store, temp_dir) = create_temp_store();
732        let drv_json = create_multi_output_derivation_json();
733        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
734        let drv_path = store.add_derivation(&drv).unwrap();
735
736        // Build the derivation
737        let outputs = store.realise(&drv_path).unwrap();
738
739        // Verify outputs are complete (BTreeMap guarantees ordering)
740        let output_names: Vec<&String> = outputs.keys().collect();
741        let expected_order = vec![
742            "outa", "outb", "outc", "outd", "oute", "outf", "outg", "outh", "outi", "outj",
743        ];
744        assert_eq!(output_names, expected_order);
745
746        drop(store);
747        drop(temp_dir);
748    }
749
750    #[test]
751    #[cfg(nix_at_least = "2.33")]
752    fn realise_invalid_system() {
753        let (mut store, temp_dir) = create_temp_store();
754
755        // Create a derivation with an invalid system
756        let system = "bogus65-bogusos";
757        let drv_json = serde_json::json!({
758            "args": ["-c", "echo $name foo > $out"],
759            "builder": "/bin/sh",
760            "env": {
761                "builder": "/bin/sh",
762                "name": "myname",
763                "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9",
764                "system": system
765            },
766            "inputs": {
767                "drvs": {},
768                "srcs": []
769            },
770            "name": "myname",
771            "outputs": {
772                "out": {
773                    "hashAlgo": "sha256",
774                    "method": "nar"
775                }
776            },
777            "system": system,
778            "version": 4
779        });
780
781        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
782        let drv_path = store.add_derivation(&drv).unwrap();
783
784        // Try to build - should fail
785        let result = store.realise(&drv_path);
786        let err = match result {
787            Ok(_) => panic!("Build should fail with invalid system"),
788            Err(e) => e.to_string(),
789        };
790        assert!(
791            err.contains("required system or feature not available"),
792            "Error should mention system not available, got: {}",
793            err
794        );
795
796        drop(store);
797        drop(temp_dir);
798    }
799
800    #[test]
801    #[cfg(nix_at_least = "2.33")]
802    fn realise_builder_fails() {
803        let (mut store, temp_dir) = create_temp_store();
804
805        let system = current_system()
806            .unwrap_or_else(|_| format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS));
807
808        // Create a derivation where the builder exits with error
809        let drv_json = serde_json::json!({
810            "args": ["-c", "exit 1"],
811            "builder": "/bin/sh",
812            "env": {
813                "builder": "/bin/sh",
814                "name": "failing",
815                "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9",
816                "system": system
817            },
818            "inputs": {
819                "drvs": {},
820                "srcs": []
821            },
822            "name": "failing",
823            "outputs": {
824                "out": {
825                    "hashAlgo": "sha256",
826                    "method": "nar"
827                }
828            },
829            "system": system,
830            "version": 4
831        });
832
833        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
834        let drv_path = store.add_derivation(&drv).unwrap();
835
836        // Try to build - should fail
837        let result = store.realise(&drv_path);
838        let err = match result {
839            Ok(_) => panic!("Build should fail when builder exits with error"),
840            Err(e) => e.to_string(),
841        };
842        assert!(
843            err.contains("builder failed with exit code 1"),
844            "Error should mention builder failed with exit code, got: {}",
845            err
846        );
847
848        drop(store);
849        drop(temp_dir);
850    }
851
852    #[test]
853    #[cfg(nix_at_least = "2.33")]
854    fn realise_builder_no_output() {
855        let (mut store, temp_dir) = create_temp_store();
856
857        let system = current_system()
858            .unwrap_or_else(|_| format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS));
859
860        // Create a derivation where the builder succeeds but produces no output
861        let drv_json = serde_json::json!({
862            "args": ["-c", "true"],
863            "builder": "/bin/sh",
864            "env": {
865                "builder": "/bin/sh",
866                "name": "no-output",
867                "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9",
868                "system": system
869            },
870            "inputs": {
871                "drvs": {},
872                "srcs": []
873            },
874            "name": "no-output",
875            "outputs": {
876                "out": {
877                    "hashAlgo": "sha256",
878                    "method": "nar"
879                }
880            },
881            "system": system,
882            "version": 4
883        });
884
885        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
886        let drv_path = store.add_derivation(&drv).unwrap();
887
888        // Try to build - should fail
889        let result = store.realise(&drv_path);
890        let err = match result {
891            Ok(_) => panic!("Build should fail when builder produces no output"),
892            Err(e) => e.to_string(),
893        };
894        assert!(
895            err.contains("failed to produce output path"),
896            "Error should mention failed to produce output, got: {}",
897            err
898        );
899
900        drop(store);
901        drop(temp_dir);
902    }
903
904    #[test]
905    #[cfg(nix_at_least = "2.33")]
906    fn get_fs_closure_with_outputs() {
907        let (mut store, temp_dir) = create_temp_store();
908        let drv_json = create_test_derivation_json();
909        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
910        let drv_path = store.add_derivation(&drv).unwrap();
911
912        // Build the derivation to get the output path
913        let outputs = store.realise(&drv_path).unwrap();
914        let out_path = &outputs["out"];
915        let out_path_name = out_path.name().unwrap();
916
917        // Get closure with include_outputs=true
918        let closure = store.get_fs_closure(&drv_path, false, true, false).unwrap();
919
920        // The closure should contain at least the derivation and its output
921        assert!(
922            closure.len() >= 2,
923            "Closure should contain at least drv and output"
924        );
925
926        // Verify the output path is in the closure
927        let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name);
928        assert!(
929            out_in_closure,
930            "Output path should be in closure when include_outputs=true"
931        );
932
933        drop(store);
934        drop(temp_dir);
935    }
936
937    #[test]
938    #[cfg(nix_at_least = "2.33")]
939    fn get_fs_closure_without_outputs() {
940        let (mut store, temp_dir) = create_temp_store();
941        let drv_json = create_test_derivation_json();
942        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
943        let drv_path = store.add_derivation(&drv).unwrap();
944
945        // Build the derivation to get the output path
946        let outputs = store.realise(&drv_path).unwrap();
947        let out_path = &outputs["out"];
948        let out_path_name = out_path.name().unwrap();
949
950        // Get closure with include_outputs=false
951        let closure = store
952            .get_fs_closure(&drv_path, false, false, false)
953            .unwrap();
954
955        // Verify the output path is NOT in the closure
956        let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name);
957        assert!(
958            !out_in_closure,
959            "Output path should not be in closure when include_outputs=false"
960        );
961
962        drop(store);
963        drop(temp_dir);
964    }
965
966    #[test]
967    #[cfg(nix_at_least = "2.33")]
968    fn get_fs_closure_flip_direction() {
969        let (mut store, temp_dir) = create_temp_store();
970        let drv_json = create_test_derivation_json();
971        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
972        let drv_path = store.add_derivation(&drv).unwrap();
973
974        // Build the derivation to get the output path
975        let outputs = store.realise(&drv_path).unwrap();
976        let out_path = &outputs["out"];
977        let out_path_name = out_path.name().unwrap();
978
979        // Get closure with flip_direction=true (reverse dependencies)
980        let closure = store.get_fs_closure(&drv_path, true, true, false).unwrap();
981
982        // Verify the output path is NOT in the closure when direction is flipped
983        let out_in_closure = closure.iter().any(|p| p.name().unwrap() == out_path_name);
984        assert!(
985            !out_in_closure,
986            "Output path should not be in closure when flip_direction=true"
987        );
988
989        drop(store);
990        drop(temp_dir);
991    }
992
993    #[test]
994    #[cfg(nix_at_least = "2.33")]
995    fn get_fs_closure_include_derivers() {
996        let (mut store, temp_dir) = create_temp_store();
997        let drv_json = create_test_derivation_json();
998        let drv = store.derivation_from_json(&drv_json.to_string()).unwrap();
999        let drv_path = store.add_derivation(&drv).unwrap();
1000        let drv_path_name = drv_path.name().unwrap();
1001
1002        // Build the derivation to get the output path
1003        let outputs = store.realise(&drv_path).unwrap();
1004        let out_path = &outputs["out"];
1005
1006        // Get closure of the output path with include_derivers=true
1007        let closure = store.get_fs_closure(out_path, false, false, true).unwrap();
1008
1009        // Verify the derivation path is in the closure
1010        let drv_in_closure = closure.iter().any(|p| p.name().unwrap() == drv_path_name);
1011        assert!(
1012            drv_in_closure,
1013            "Derivation should be in closure when include_derivers=true"
1014        );
1015
1016        drop(store);
1017        drop(temp_dir);
1018    }
1019}