nix_bindings_flake/
lib.rs

1use std::{ffi::CString, os::raw::c_char, ptr::NonNull};
2
3use anyhow::{Context as _, Result};
4use nix_bindings_expr::eval_state::EvalState;
5use nix_bindings_fetchers::FetchersSettings;
6use nix_bindings_flake_sys as raw;
7use nix_bindings_util::{
8    context::{self, Context},
9    result_string_init,
10    string_return::{callback_get_result_string, callback_get_result_string_data},
11};
12
13/// Store settings for the flakes feature.
14pub struct FlakeSettings {
15    pub(crate) ptr: *mut raw::flake_settings,
16}
17impl Drop for FlakeSettings {
18    fn drop(&mut self) {
19        unsafe {
20            raw::flake_settings_free(self.ptr);
21        }
22    }
23}
24impl FlakeSettings {
25    pub fn new() -> Result<Self> {
26        let mut ctx = Context::new();
27        let s = unsafe { context::check_call!(raw::flake_settings_new(&mut ctx)) }?;
28        Ok(FlakeSettings { ptr: s })
29    }
30    fn add_to_eval_state_builder(
31        &self,
32        builder: &mut nix_bindings_expr::eval_state::EvalStateBuilder,
33    ) -> Result<()> {
34        let mut ctx = Context::new();
35        unsafe {
36            context::check_call!(raw::flake_settings_add_to_eval_state_builder(
37                &mut ctx,
38                self.ptr,
39                builder.raw_ptr()
40            ))
41        }?;
42        Ok(())
43    }
44}
45
46pub trait EvalStateBuilderExt {
47    /// Configures the eval state to provide flakes features such as `builtins.getFlake`.
48    fn flakes(
49        self,
50        settings: &FlakeSettings,
51    ) -> Result<nix_bindings_expr::eval_state::EvalStateBuilder>;
52}
53impl EvalStateBuilderExt for nix_bindings_expr::eval_state::EvalStateBuilder {
54    /// Configures the eval state to provide flakes features such as `builtins.getFlake`.
55    fn flakes(
56        mut self,
57        settings: &FlakeSettings,
58    ) -> Result<nix_bindings_expr::eval_state::EvalStateBuilder> {
59        settings.add_to_eval_state_builder(&mut self)?;
60        Ok(self)
61    }
62}
63
64/// Parameters for parsing a flake reference.
65pub struct FlakeReferenceParseFlags {
66    pub(crate) ptr: NonNull<raw::flake_reference_parse_flags>,
67}
68impl Drop for FlakeReferenceParseFlags {
69    fn drop(&mut self) {
70        unsafe {
71            raw::flake_reference_parse_flags_free(self.ptr.as_ptr());
72        }
73    }
74}
75impl FlakeReferenceParseFlags {
76    pub fn new(settings: &FlakeSettings) -> Result<Self> {
77        let mut ctx = Context::new();
78        let ptr = unsafe {
79            context::check_call!(raw::flake_reference_parse_flags_new(&mut ctx, settings.ptr))
80        }?;
81        let ptr = NonNull::new(ptr)
82            .context("flake_reference_parse_flags_new unexpectedly returned null")?;
83        Ok(FlakeReferenceParseFlags { ptr })
84    }
85    /// Sets the [base directory](https://nix.dev/manual/nix/latest/glossary#gloss-base-directory)
86    /// for resolving local flake references.
87    pub fn set_base_directory(&mut self, base_directory: &str) -> Result<()> {
88        let mut ctx = Context::new();
89        unsafe {
90            context::check_call!(raw::flake_reference_parse_flags_set_base_directory(
91                &mut ctx,
92                self.ptr.as_ptr(),
93                base_directory.as_ptr() as *const c_char,
94                base_directory.len()
95            ))
96        }?;
97        Ok(())
98    }
99}
100
101pub struct FlakeReference {
102    pub(crate) ptr: NonNull<raw::flake_reference>,
103}
104impl Drop for FlakeReference {
105    fn drop(&mut self) {
106        unsafe {
107            raw::flake_reference_free(self.ptr.as_ptr());
108        }
109    }
110}
111impl FlakeReference {
112    /// Parse a flake reference from a string.
113    /// The string must be a valid flake reference, such as `github:owner/repo`.
114    /// It may also be suffixed with a `#` and a fragment, such as `github:owner/repo#something`,
115    /// in which case, the returned string will contain the fragment.
116    pub fn parse_with_fragment(
117        fetch_settings: &FetchersSettings,
118        flake_settings: &FlakeSettings,
119        flags: &FlakeReferenceParseFlags,
120        reference: &str,
121    ) -> Result<(FlakeReference, String)> {
122        let mut ctx = Context::new();
123        let mut r = result_string_init!();
124        let mut ptr: *mut raw::flake_reference = std::ptr::null_mut();
125        unsafe {
126            context::check_call!(raw::flake_reference_and_fragment_from_string(
127                &mut ctx,
128                fetch_settings.raw_ptr(),
129                flake_settings.ptr,
130                flags.ptr.as_ptr(),
131                reference.as_ptr() as *const c_char,
132                reference.len(),
133                // pointer to ptr
134                &mut ptr,
135                Some(callback_get_result_string),
136                callback_get_result_string_data(&mut r)
137            ))
138        }?;
139        let ptr = NonNull::new(ptr)
140            .context("flake_reference_and_fragment_from_string unexpectedly returned null")?;
141        Ok((FlakeReference { ptr }, r?))
142    }
143}
144
145/// Parameters that affect the locking of a flake.
146pub struct FlakeLockFlags {
147    pub(crate) ptr: *mut raw::flake_lock_flags,
148}
149impl Drop for FlakeLockFlags {
150    fn drop(&mut self) {
151        unsafe {
152            raw::flake_lock_flags_free(self.ptr);
153        }
154    }
155}
156impl FlakeLockFlags {
157    pub fn new(settings: &FlakeSettings) -> Result<Self> {
158        let mut ctx = Context::new();
159        let s = unsafe { context::check_call!(raw::flake_lock_flags_new(&mut ctx, settings.ptr)) }?;
160        Ok(FlakeLockFlags { ptr: s })
161    }
162    /// Configures [LockedFlake::lock] to make incremental changes to the lock file as needed. Changes are written to file.
163    pub fn set_mode_write_as_needed(&mut self) -> Result<()> {
164        let mut ctx = Context::new();
165        unsafe {
166            context::check_call!(raw::flake_lock_flags_set_mode_write_as_needed(
167                &mut ctx, self.ptr
168            ))
169        }?;
170        Ok(())
171    }
172    /// Make [LockedFlake::lock] check if the lock file is up to date. If not, an error is returned.
173    pub fn set_mode_check(&mut self) -> Result<()> {
174        let mut ctx = Context::new();
175        unsafe { context::check_call!(raw::flake_lock_flags_set_mode_check(&mut ctx, self.ptr)) }?;
176        Ok(())
177    }
178    /// Like `set_mode_write_as_needed`, but does not write to the lock file.
179    pub fn set_mode_virtual(&mut self) -> Result<()> {
180        let mut ctx = Context::new();
181        unsafe {
182            context::check_call!(raw::flake_lock_flags_set_mode_virtual(&mut ctx, self.ptr))
183        }?;
184        Ok(())
185    }
186    /// Adds an input override to the lock file that will be produced. The [LockedFlake::lock] operation will not write to the lock file.
187    pub fn add_input_override(
188        &mut self,
189        override_path: &str,
190        override_ref: &FlakeReference,
191    ) -> Result<()> {
192        let mut ctx = Context::new();
193        unsafe {
194            context::check_call!(raw::flake_lock_flags_add_input_override(
195                &mut ctx,
196                self.ptr,
197                CString::new(override_path)
198                    .context("Failed to create CString for override_path")?
199                    .as_ptr(),
200                override_ref.ptr.as_ptr()
201            ))
202        }?;
203        Ok(())
204    }
205}
206
207pub struct LockedFlake {
208    pub(crate) ptr: NonNull<raw::locked_flake>,
209}
210impl Drop for LockedFlake {
211    fn drop(&mut self) {
212        unsafe {
213            raw::locked_flake_free(self.ptr.as_ptr());
214        }
215    }
216}
217impl LockedFlake {
218    pub fn lock(
219        fetch_settings: &FetchersSettings,
220        flake_settings: &FlakeSettings,
221        eval_state: &EvalState,
222        flags: &FlakeLockFlags,
223        flake_ref: &FlakeReference,
224    ) -> Result<LockedFlake> {
225        let mut ctx = Context::new();
226        let ptr = unsafe {
227            context::check_call!(raw::flake_lock(
228                &mut ctx,
229                fetch_settings.raw_ptr(),
230                flake_settings.ptr,
231                eval_state.raw_ptr(),
232                flags.ptr,
233                flake_ref.ptr.as_ptr()
234            ))
235        }?;
236        let ptr = NonNull::new(ptr).context("flake_lock unexpectedly returned null")?;
237        Ok(LockedFlake { ptr })
238    }
239
240    /// Returns the outputs of the flake - the result of calling the `outputs` attribute.
241    pub fn outputs(
242        &self,
243        flake_settings: &FlakeSettings,
244        eval_state: &mut EvalState,
245    ) -> Result<nix_bindings_expr::value::Value> {
246        let mut ctx = Context::new();
247        unsafe {
248            let r = context::check_call!(raw::locked_flake_get_output_attrs(
249                &mut ctx,
250                flake_settings.ptr,
251                eval_state.raw_ptr(),
252                self.ptr.as_ptr()
253            ))?;
254            Ok(nix_bindings_expr::value::__private::raw_value_new(r))
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use nix_bindings_expr::eval_state::{gc_register_my_thread, EvalStateBuilder};
262    use nix_bindings_store::store::Store;
263
264    use super::*;
265    use std::sync::Once;
266
267    static INIT: Once = Once::new();
268
269    fn init() {
270        // Only set experimental-features once to minimize the window where
271        // concurrent Nix operations might read the setting while it's being modified
272        INIT.call_once(|| {
273            nix_bindings_expr::eval_state::init().unwrap();
274            nix_bindings_util::settings::set("experimental-features", "flakes").unwrap();
275        });
276    }
277
278    #[test]
279    fn flake_settings_getflake_exists() {
280        init();
281        let gc_registration = gc_register_my_thread();
282        let store = Store::open(None, []).unwrap();
283        let mut eval_state = EvalStateBuilder::new(store)
284            .unwrap()
285            .flakes(&FlakeSettings::new().unwrap())
286            .unwrap()
287            .build()
288            .unwrap();
289
290        let v = eval_state
291            .eval_from_string("builtins?getFlake", "<test>")
292            .unwrap();
293
294        let b = eval_state.require_bool(&v).unwrap();
295
296        assert!(b);
297
298        drop(gc_registration);
299    }
300
301    #[test]
302    fn flake_lock_load_flake() {
303        init();
304        let gc_registration = gc_register_my_thread();
305        let store = Store::open(None, []).unwrap();
306        let fetchers_settings = FetchersSettings::new().unwrap();
307        let flake_settings = FlakeSettings::new().unwrap();
308        let mut eval_state = EvalStateBuilder::new(store)
309            .unwrap()
310            .flakes(&flake_settings)
311            .unwrap()
312            .build()
313            .unwrap();
314
315        let tmp_dir = tempfile::tempdir().unwrap();
316
317        // Create flake.nix
318        let flake_nix = tmp_dir.path().join("flake.nix");
319        std::fs::write(
320            &flake_nix,
321            r#"
322{
323    outputs = { ... }: {
324        hello = "potato";
325    };
326}
327        "#,
328        )
329        .unwrap();
330
331        let flake_lock_flags = FlakeLockFlags::new(&flake_settings).unwrap();
332
333        let (flake_ref, fragment) = FlakeReference::parse_with_fragment(
334            &fetchers_settings,
335            &flake_settings,
336            &FlakeReferenceParseFlags::new(&flake_settings).unwrap(),
337            &format!("path:{}#subthing", tmp_dir.path().display()),
338        )
339        .unwrap();
340
341        assert_eq!(fragment, "subthing");
342
343        let locked_flake = LockedFlake::lock(
344            &fetchers_settings,
345            &flake_settings,
346            &eval_state,
347            &flake_lock_flags,
348            &flake_ref,
349        )
350        .unwrap();
351
352        let outputs = locked_flake
353            .outputs(&flake_settings, &mut eval_state)
354            .unwrap();
355
356        let hello = eval_state.require_attrs_select(&outputs, "hello").unwrap();
357        let hello = eval_state.require_string(&hello).unwrap();
358
359        assert_eq!(hello, "potato");
360
361        drop(fetchers_settings);
362        drop(tmp_dir);
363        drop(gc_registration);
364    }
365
366    #[test]
367    fn flake_lock_load_flake_with_flags() {
368        init();
369        let gc_registration = gc_register_my_thread();
370        let store = Store::open(None, []).unwrap();
371        let fetchers_settings = FetchersSettings::new().unwrap();
372        let flake_settings = FlakeSettings::new().unwrap();
373        let mut eval_state = EvalStateBuilder::new(store)
374            .unwrap()
375            .flakes(&flake_settings)
376            .unwrap()
377            .build()
378            .unwrap();
379
380        let tmp_dir = tempfile::tempdir().unwrap();
381
382        let flake_dir_a = tmp_dir.path().join("a");
383        let flake_dir_b = tmp_dir.path().join("b");
384        let flake_dir_c = tmp_dir.path().join("c");
385
386        std::fs::create_dir_all(&flake_dir_a).unwrap();
387        std::fs::create_dir_all(&flake_dir_b).unwrap();
388        std::fs::create_dir_all(&flake_dir_c).unwrap();
389
390        let flake_dir_a_str = flake_dir_a.to_str().unwrap();
391        let flake_dir_c_str = flake_dir_c.to_str().unwrap();
392        assert!(!flake_dir_a_str.is_empty());
393        assert!(!flake_dir_c_str.is_empty());
394
395        // a
396        std::fs::write(
397            tmp_dir.path().join("a/flake.nix"),
398            r#"
399            {
400                inputs.b.url = "@flake_dir_b@";
401                outputs = { b, ... }: {
402                    hello = b.hello;
403                };
404            }
405            "#
406            .replace("@flake_dir_b@", flake_dir_b.to_str().unwrap()),
407        )
408        .unwrap();
409
410        // b
411        std::fs::write(
412            tmp_dir.path().join("b/flake.nix"),
413            r#"
414            {
415                outputs = { ... }: {
416                    hello = "BOB";
417                };
418            }
419            "#,
420        )
421        .unwrap();
422
423        // c
424        std::fs::write(
425            tmp_dir.path().join("c/flake.nix"),
426            r#"
427            {
428                outputs = { ... }: {
429                    hello = "Claire";
430                };
431            }
432            "#,
433        )
434        .unwrap();
435
436        let mut flake_lock_flags = FlakeLockFlags::new(&flake_settings).unwrap();
437
438        let mut flake_reference_parse_flags =
439            FlakeReferenceParseFlags::new(&flake_settings).unwrap();
440
441        flake_reference_parse_flags
442            .set_base_directory(tmp_dir.path().to_str().unwrap())
443            .unwrap();
444
445        let (flake_ref_a, fragment) = FlakeReference::parse_with_fragment(
446            &fetchers_settings,
447            &flake_settings,
448            &flake_reference_parse_flags,
449            &format!("path:{}", &flake_dir_a_str),
450        )
451        .unwrap();
452
453        assert_eq!(fragment, "");
454
455        // Step 1: Do not update (check), fails
456
457        flake_lock_flags.set_mode_check().unwrap();
458
459        let locked_flake = LockedFlake::lock(
460            &fetchers_settings,
461            &flake_settings,
462            &eval_state,
463            &flake_lock_flags,
464            &flake_ref_a,
465        );
466        // Has not been locked and would need to write a lock file.
467        assert!(locked_flake.is_err());
468        let saved_err = match locked_flake {
469            Ok(_) => panic!("Expected error, but got Ok"),
470            Err(e) => e,
471        };
472
473        // Step 2: Update but do not write, succeeds
474        flake_lock_flags.set_mode_virtual().unwrap();
475
476        let locked_flake = LockedFlake::lock(
477            &fetchers_settings,
478            &flake_settings,
479            &eval_state,
480            &flake_lock_flags,
481            &flake_ref_a,
482        )
483        .unwrap();
484
485        let outputs = locked_flake
486            .outputs(&flake_settings, &mut eval_state)
487            .unwrap();
488
489        let hello = eval_state.require_attrs_select(&outputs, "hello").unwrap();
490        let hello = eval_state.require_string(&hello).unwrap();
491
492        assert_eq!(hello, "BOB");
493
494        // Step 3: The lock was not written, so Step 1 would fail again
495
496        flake_lock_flags.set_mode_check().unwrap();
497
498        let locked_flake = LockedFlake::lock(
499            &fetchers_settings,
500            &flake_settings,
501            &eval_state,
502            &flake_lock_flags,
503            &flake_ref_a,
504        );
505        // Has not been locked and would need to write a lock file.
506        assert!(locked_flake.is_err());
507        match locked_flake {
508            Ok(_) => panic!("Expected error, but got Ok"),
509            Err(e) => {
510                assert_eq!(e.to_string(), saved_err.to_string());
511            }
512        };
513
514        // Step 4: Update and write, succeeds
515
516        flake_lock_flags.set_mode_write_as_needed().unwrap();
517
518        let locked_flake = LockedFlake::lock(
519            &fetchers_settings,
520            &flake_settings,
521            &eval_state,
522            &flake_lock_flags,
523            &flake_ref_a,
524        )
525        .unwrap();
526
527        let outputs = locked_flake
528            .outputs(&flake_settings, &mut eval_state)
529            .unwrap();
530        let hello = eval_state.require_attrs_select(&outputs, "hello").unwrap();
531        let hello = eval_state.require_string(&hello).unwrap();
532        assert_eq!(hello, "BOB");
533
534        // Step 5: Lock was written, so Step 1 succeeds
535
536        flake_lock_flags.set_mode_check().unwrap();
537
538        let locked_flake = LockedFlake::lock(
539            &fetchers_settings,
540            &flake_settings,
541            &eval_state,
542            &flake_lock_flags,
543            &flake_ref_a,
544        )
545        .unwrap();
546
547        let outputs = locked_flake
548            .outputs(&flake_settings, &mut eval_state)
549            .unwrap();
550        let hello = eval_state.require_attrs_select(&outputs, "hello").unwrap();
551        let hello = eval_state.require_string(&hello).unwrap();
552        assert_eq!(hello, "BOB");
553
554        // Step 6: Lock with override, do not write
555
556        // This shouldn't matter; write_as_needed will be overridden
557        flake_lock_flags.set_mode_write_as_needed().unwrap();
558
559        let (flake_ref_c, fragment) = FlakeReference::parse_with_fragment(
560            &fetchers_settings,
561            &flake_settings,
562            &flake_reference_parse_flags,
563            &format!("path:{}", &flake_dir_c_str),
564        )
565        .unwrap();
566        assert_eq!(fragment, "");
567
568        flake_lock_flags
569            .add_input_override("b", &flake_ref_c)
570            .unwrap();
571
572        let locked_flake = LockedFlake::lock(
573            &fetchers_settings,
574            &flake_settings,
575            &eval_state,
576            &flake_lock_flags,
577            &flake_ref_a,
578        )
579        .unwrap();
580
581        let outputs = locked_flake
582            .outputs(&flake_settings, &mut eval_state)
583            .unwrap();
584        let hello = eval_state.require_attrs_select(&outputs, "hello").unwrap();
585        let hello = eval_state.require_string(&hello).unwrap();
586        assert_eq!(hello, "Claire");
587
588        // Can't delete overrides, so trash it
589        let mut flake_lock_flags = FlakeLockFlags::new(&flake_settings).unwrap();
590
591        // Step 7: Override was not written; lock still points to b
592
593        flake_lock_flags.set_mode_check().unwrap();
594
595        let locked_flake = LockedFlake::lock(
596            &fetchers_settings,
597            &flake_settings,
598            &eval_state,
599            &flake_lock_flags,
600            &flake_ref_a,
601        )
602        .unwrap();
603
604        let outputs = locked_flake
605            .outputs(&flake_settings, &mut eval_state)
606            .unwrap();
607        let hello = eval_state.require_attrs_select(&outputs, "hello").unwrap();
608        let hello = eval_state.require_string(&hello).unwrap();
609        assert_eq!(hello, "BOB");
610
611        drop(fetchers_settings);
612        drop(tmp_dir);
613        drop(gc_registration);
614    }
615}