Skip to main content

nix_bindings/
flake.rs

1//! Nix flake support.
2//!
3//! Types:
4//!
5//! - [`FlakeSettings`]: global flake configuration; pass to
6//!   [`EvalStateBuilder::with_flake_settings`](crate::EvalStateBuilder::with_flake_settings).
7//! - [`FetchersSettings`]: fetcher configuration required by
8//!   [`FlakeReference::parse`] and [`LockedFlake::lock`].
9//! - [`FlakeReferenceParseFlags`]: optional flags controlling how a flake
10//!   reference string is parsed.
11//! - [`LockFlags`]: controls locking behaviour (check, virtual,
12//!   write-as-needed, input overrides).
13//! - [`FlakeReference`]: an unresolved reference to a flake; produced by
14//!   [`FlakeReference::parse`].
15//! - [`LockedFlake`]: a fully locked flake; produced by [`LockedFlake::lock`].
16//!   Call [`LockedFlake::output_attrs`] to obtain the flake's output attribute
17//!   set.
18
19use std::{ffi::CString, ptr::NonNull, sync::Arc};
20
21use crate::{Context, Error, EvalState, Result, Value, check_err, sys};
22
23/// Configuration for the Nix flake subsystem.
24///
25/// This enables flake evaluation features in the Nix evaluator (such as
26/// `builtins.getFlake`). Obtain a `FlakeSettings` and pass it to
27/// [`EvalStateBuilder::with_flake_settings`](crate::EvalStateBuilder::with_flake_settings)
28/// before building the [`EvalState`].
29///
30/// # Example
31///
32/// ```no_run
33/// use std::sync::Arc;
34///
35/// use nix_bindings::{Context, EvalStateBuilder, Store, flake::FlakeSettings};
36///
37/// fn main() -> Result<(), Box<dyn std::error::Error>> {
38///   let ctx = Arc::new(Context::new()?);
39///   let store = Arc::new(Store::open(&ctx, None)?);
40///   let flake_settings = FlakeSettings::new(&ctx)?;
41///   let state = EvalStateBuilder::new(&store)?
42///     .with_flake_settings(&flake_settings)?
43///     .build()?;
44///
45///   Ok(())
46/// }
47/// ```
48pub struct FlakeSettings {
49  pub(crate) inner: NonNull<sys::nix_flake_settings>,
50  _context:         Arc<Context>,
51}
52
53impl FlakeSettings {
54  /// Create a new set of flake settings with default values.
55  ///
56  /// # Errors
57  ///
58  /// Returns an error if the underlying allocation fails.
59  pub fn new(context: &Arc<Context>) -> Result<Self> {
60    // SAFETY: context is valid
61    let ptr = unsafe { sys::nix_flake_settings_new(context.as_ptr()) };
62
63    let inner = NonNull::new(ptr).ok_or(Error::NullPointer)?;
64
65    Ok(FlakeSettings {
66      inner,
67      _context: Arc::clone(context),
68    })
69  }
70
71  /// Get the raw flake settings pointer.
72  pub(crate) unsafe fn as_ptr(&self) -> *mut sys::nix_flake_settings {
73    self.inner.as_ptr()
74  }
75}
76
77impl Drop for FlakeSettings {
78  fn drop(&mut self) {
79    // SAFETY: We own the settings and they are valid until drop
80    unsafe {
81      sys::nix_flake_settings_free(self.inner.as_ptr());
82    }
83  }
84}
85
86// SAFETY: `FlakeSettings` owns its `nix_flake_settings*` and uses the
87// `Arc<Context>` purely for lifetime extension. The settings object
88// holds plain configuration values with no thread affinity. `Sync` is
89// NOT implemented: every method that consults the settings goes through
90// `Context`'s racy error buffer.
91unsafe impl Send for FlakeSettings {}
92
93/// Fetcher configuration.
94///
95/// This is required by [`FlakeReference::parse`] and [`LockedFlake::lock`].
96/// Create one with [`FetchersSettings::new`] and keep it alive for the
97/// duration of any flake operations that need it.
98pub struct FetchersSettings {
99  inner:    NonNull<sys::nix_fetchers_settings>,
100  _context: Arc<Context>,
101}
102
103impl FetchersSettings {
104  /// Create new fetcher settings with default values.
105  ///
106  /// # Errors
107  ///
108  /// Returns an error if the underlying allocation fails.
109  pub fn new(context: &Arc<Context>) -> Result<Self> {
110    // SAFETY: context is valid
111    let ptr = unsafe { sys::nix_fetchers_settings_new(context.as_ptr()) };
112    let inner = NonNull::new(ptr).ok_or(Error::NullPointer)?;
113    Ok(FetchersSettings {
114      inner,
115      _context: Arc::clone(context),
116    })
117  }
118
119  pub(crate) unsafe fn as_ptr(&self) -> *mut sys::nix_fetchers_settings {
120    self.inner.as_ptr()
121  }
122}
123
124impl Drop for FetchersSettings {
125  fn drop(&mut self) {
126    // SAFETY: We own the settings and they are valid until drop
127    unsafe {
128      sys::nix_fetchers_settings_free(self.inner.as_ptr());
129    }
130  }
131}
132
133// SAFETY: `FetchersSettings` is an opaque pointer to plain configuration
134// values, kept alive by `Arc<Context>`. The C object has no thread
135// affinity. `Sync` is NOT implemented for the same reason as
136// `FlakeSettings`: any call into it routes through `Context`'s racy
137// error buffer.
138unsafe impl Send for FetchersSettings {}
139
140/// Flags that control how a flake reference string is parsed.
141///
142/// Create one with [`FlakeReferenceParseFlags::new`] then optionally call
143/// [`set_base_directory`](Self::set_base_directory) before passing it to
144/// [`FlakeReference::parse`].
145pub struct FlakeReferenceParseFlags {
146  inner:    NonNull<sys::nix_flake_reference_parse_flags>,
147  _context: Arc<Context>,
148}
149
150impl FlakeReferenceParseFlags {
151  /// Create new parse flags with default values.
152  ///
153  /// # Errors
154  ///
155  /// Returns an error if the underlying allocation fails.
156  pub fn new(
157    context: &Arc<Context>,
158    flake_settings: &FlakeSettings,
159  ) -> Result<Self> {
160    // SAFETY: context and flake_settings are valid
161    let ptr = unsafe {
162      sys::nix_flake_reference_parse_flags_new(
163        context.as_ptr(),
164        flake_settings.as_ptr(),
165      )
166    };
167    let inner = NonNull::new(ptr).ok_or(Error::NullPointer)?;
168    Ok(FlakeReferenceParseFlags {
169      inner,
170      _context: Arc::clone(context),
171    })
172  }
173
174  /// Set the base directory used when resolving relative flake references.
175  ///
176  /// # Errors
177  ///
178  /// Returns an error if the C API call fails.
179  pub fn set_base_directory(self, dir: &str) -> Result<Self> {
180    let bytes = dir.as_bytes();
181    // SAFETY: context, flags, and dir bytes are valid
182    unsafe {
183      check_err(
184        self._context.as_ptr(),
185        sys::nix_flake_reference_parse_flags_set_base_directory(
186          self._context.as_ptr(),
187          self.inner.as_ptr(),
188          bytes.as_ptr().cast(),
189          bytes.len(),
190        ),
191      )?;
192    }
193    Ok(self)
194  }
195
196  pub(crate) unsafe fn as_ptr(
197    &self,
198  ) -> *mut sys::nix_flake_reference_parse_flags {
199    self.inner.as_ptr()
200  }
201}
202
203impl Drop for FlakeReferenceParseFlags {
204  fn drop(&mut self) {
205    // SAFETY: We own the flags and they are valid until drop
206    unsafe {
207      sys::nix_flake_reference_parse_flags_free(self.inner.as_ptr());
208    }
209  }
210}
211
212// SAFETY: `FlakeReferenceParseFlags` is a small flags struct kept alive
213// by `Arc<Context>` plus `Arc<FlakeSettings>`. Both Arcs are themselves
214// `Send` for our types (see their notes). The C object has no thread
215// affinity. `Sync` is NOT implemented: `set_base_directory` mutates
216// state through `Context`'s racy error buffer.
217unsafe impl Send for FlakeReferenceParseFlags {}
218
219/// Lock-file update strategy for [`LockFlags::set_mode`].
220///
221/// The three modes are mutually exclusive at the C-API level. Picking one
222/// writes into the underlying `nix_flake_lock_flags` slot, so chaining
223/// modes is meaningless. Exposing them as an enum makes that obvious at
224/// the call site.
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum LockMode {
227  /// Require the lock file to be up-to-date; fail if it needs updating.
228  Check,
229  /// Update the lock file in memory only; do not write it to disk.
230  Virtual,
231  /// Update and write the lock file to disk if it needs updating.
232  WriteAsNeeded,
233}
234
235/// Flags controlling the lock-file update strategy for [`LockedFlake::lock`].
236///
237/// Holds an Arc to the originating [`FlakeSettings`] so the settings cannot
238/// be dropped while these flags are alive.
239pub struct LockFlags {
240  inner:     NonNull<sys::nix_flake_lock_flags>,
241  _context:  Arc<Context>,
242  _settings: Arc<FlakeSettings>,
243}
244
245impl LockFlags {
246  /// Create new lock flags with default values.
247  ///
248  /// # Errors
249  ///
250  /// Returns an error if the underlying allocation fails.
251  pub fn new(
252    context: &Arc<Context>,
253    flake_settings: &Arc<FlakeSettings>,
254  ) -> Result<Self> {
255    // SAFETY: context and flake_settings are valid
256    let ptr = unsafe {
257      sys::nix_flake_lock_flags_new(context.as_ptr(), flake_settings.as_ptr())
258    };
259    let inner = NonNull::new(ptr).ok_or(Error::NullPointer)?;
260    Ok(LockFlags {
261      inner,
262      _context: Arc::clone(context),
263      _settings: Arc::clone(flake_settings),
264    })
265  }
266
267  /// Set the lock-file update strategy.
268  ///
269  /// # Errors
270  ///
271  /// Returns an error if the C API call fails.
272  pub fn set_mode(self, mode: LockMode) -> Result<Self> {
273    // SAFETY: context and flags are valid
274    unsafe {
275      let err = match mode {
276        LockMode::Check => {
277          sys::nix_flake_lock_flags_set_mode_check(
278            self._context.as_ptr(),
279            self.inner.as_ptr(),
280          )
281        },
282        LockMode::Virtual => {
283          sys::nix_flake_lock_flags_set_mode_virtual(
284            self._context.as_ptr(),
285            self.inner.as_ptr(),
286          )
287        },
288        LockMode::WriteAsNeeded => {
289          sys::nix_flake_lock_flags_set_mode_write_as_needed(
290            self._context.as_ptr(),
291            self.inner.as_ptr(),
292          )
293        },
294      };
295      check_err(self._context.as_ptr(), err)?;
296    }
297    Ok(self)
298  }
299
300  /// Override a specific input with an alternative flake reference.
301  ///
302  /// `input_path` identifies the input (e.g. `"nixpkgs"`).
303  ///
304  /// # Errors
305  ///
306  /// Returns an error if the C API call fails.
307  pub fn add_input_override(
308    self,
309    input_path: &str,
310    flake_ref: &FlakeReference,
311  ) -> Result<Self> {
312    let path_c = CString::new(input_path)?;
313    // SAFETY: context, flags, path_c, and flake_ref are valid
314    unsafe {
315      check_err(
316        self._context.as_ptr(),
317        sys::nix_flake_lock_flags_add_input_override(
318          self._context.as_ptr(),
319          self.inner.as_ptr(),
320          path_c.as_ptr(),
321          flake_ref.inner.as_ptr(),
322        ),
323      )?;
324    }
325    Ok(self)
326  }
327
328  pub(crate) unsafe fn as_ptr(&self) -> *mut sys::nix_flake_lock_flags {
329    self.inner.as_ptr()
330  }
331}
332
333impl Drop for LockFlags {
334  fn drop(&mut self) {
335    // SAFETY: We own the flags and they are valid until drop
336    unsafe {
337      sys::nix_flake_lock_flags_free(self.inner.as_ptr());
338    }
339  }
340}
341
342// SAFETY: `LockFlags` is a small mode-and-overrides struct kept alive
343// by `Arc<Context>` plus `Arc<FlakeSettings>`. Same move-only contract
344// as `FlakeReferenceParseFlags`. `Sync` is NOT implemented:
345// `set_mode` and `add_input_override` mutate through `Context`'s racy
346// error buffer.
347unsafe impl Send for LockFlags {}
348
349/// Callback that collects a string returned from the Nix C API via a pointer
350/// and length pair into an `Option<String>` stored in `user_data`.
351unsafe extern "C" fn collect_fragment_cb(
352  start: *const std::os::raw::c_char,
353  n: std::os::raw::c_uint,
354  user_data: *mut std::os::raw::c_void,
355) {
356  let result = unsafe { &mut *(user_data as *mut Option<String>) };
357  if !start.is_null() {
358    let bytes =
359      unsafe { std::slice::from_raw_parts(start.cast::<u8>(), n as usize) };
360    *result = std::str::from_utf8(bytes).ok().map(|s| s.to_owned());
361  }
362}
363
364/// An unresolved flake reference.
365///
366/// Obtain one via [`FlakeReference::parse`], then pass it to
367/// [`LockedFlake::lock`] (or [`LockFlags::add_input_override`]).
368pub struct FlakeReference {
369  inner:    NonNull<sys::nix_flake_reference>,
370  _context: Arc<Context>,
371}
372
373impl FlakeReference {
374  /// Parse a flake reference string into a [`FlakeReference`].
375  ///
376  /// Returns both the parsed reference and any fragment that followed a `#`
377  /// in the input string. For references without a fragment the second
378  /// element is an empty string.
379  ///
380  /// # Errors
381  ///
382  /// Returns an error if the C API call fails or returns a null pointer.
383  pub fn parse(
384    context: &Arc<Context>,
385    fetch_settings: &FetchersSettings,
386    flake_settings: &FlakeSettings,
387    parse_flags: &FlakeReferenceParseFlags,
388    s: &str,
389  ) -> Result<(Self, String)> {
390    let bytes = s.as_bytes();
391
392    let mut out_ptr: *mut sys::nix_flake_reference = std::ptr::null_mut();
393    let mut fragment: Option<String> = None;
394
395    // SAFETY: all arguments are valid; we capture the fragment via callback
396    let err = unsafe {
397      sys::nix_flake_reference_and_fragment_from_string(
398        context.as_ptr(),
399        fetch_settings.as_ptr(),
400        flake_settings.as_ptr(),
401        parse_flags.as_ptr(),
402        bytes.as_ptr().cast(),
403        bytes.len(),
404        &mut out_ptr as *mut *mut sys::nix_flake_reference,
405        Some(collect_fragment_cb),
406        &mut fragment as *mut Option<String> as *mut std::os::raw::c_void,
407      )
408    };
409
410    check_err(unsafe { context.as_ptr() }, err)?;
411
412    let inner = NonNull::new(out_ptr).ok_or(Error::NullPointer)?;
413
414    let frag = fragment.unwrap_or_default();
415
416    Ok((
417      FlakeReference {
418        inner,
419        _context: Arc::clone(context),
420      },
421      frag,
422    ))
423  }
424}
425
426impl Drop for FlakeReference {
427  fn drop(&mut self) {
428    // SAFETY: We own the reference and it is valid until drop
429    unsafe {
430      sys::nix_flake_reference_free(self.inner.as_ptr());
431    }
432  }
433}
434
435// SAFETY: `FlakeReference` wraps a parsed but unresolved reference value
436// owned outright via `nix_flake_reference*`, kept alive by
437// `Arc<Context>`. Resolution happens later through `LockedFlake::lock`,
438// which calls into `Context`; sending the unresolved value to another
439// thread before locking is sound. `Sync` is NOT implemented because
440// `lock` and `add_input_override` mutate through `Context`'s error
441// buffer.
442unsafe impl Send for FlakeReference {}
443
444/// A fully locked flake.
445///
446/// Obtain one via [`LockedFlake::lock`], then call
447/// [`output_attrs`](LockedFlake::output_attrs) to get the attribute set of
448/// flake outputs.
449pub struct LockedFlake {
450  inner:    NonNull<sys::nix_locked_flake>,
451  _context: Arc<Context>,
452}
453
454impl LockedFlake {
455  /// Lock a flake, resolving and pinning all inputs.
456  ///
457  /// # Errors
458  ///
459  /// Returns an error if the C API call fails or returns a null pointer.
460  pub fn lock(
461    context: &Arc<Context>,
462    fetch_settings: &FetchersSettings,
463    flake_settings: &FlakeSettings,
464    eval_state: &EvalState,
465    lock_flags: &LockFlags,
466    flake_ref: &FlakeReference,
467  ) -> Result<Self> {
468    // SAFETY: all arguments are valid
469    let ptr = unsafe {
470      sys::nix_flake_lock(
471        context.as_ptr(),
472        fetch_settings.as_ptr(),
473        flake_settings.as_ptr(),
474        eval_state.as_ptr(),
475        lock_flags.as_ptr(),
476        flake_ref.inner.as_ptr(),
477      )
478    };
479
480    let inner = NonNull::new(ptr).ok_or(Error::NullPointer)?;
481
482    Ok(LockedFlake {
483      inner,
484      _context: Arc::clone(context),
485    })
486  }
487
488  /// Get the output attributes of this locked flake as a Nix value.
489  ///
490  /// The returned [`Value`] is tied to the lifetime of `eval_state`.
491  ///
492  /// # Errors
493  ///
494  /// Returns an error if the C API call fails.
495  pub fn output_attrs<'s>(
496    &self,
497    flake_settings: &FlakeSettings,
498    eval_state: &'s EvalState,
499  ) -> Result<Value<'s>> {
500    // SAFETY: all pointers are valid
501    let ptr = unsafe {
502      sys::nix_locked_flake_get_output_attrs(
503        self._context.as_ptr(),
504        flake_settings.as_ptr(),
505        eval_state.as_ptr(),
506        self.inner.as_ptr(),
507      )
508    };
509
510    let inner = std::ptr::NonNull::new(ptr).ok_or(Error::NullPointer)?;
511
512    Ok(Value {
513      inner,
514      state: eval_state,
515    })
516  }
517}
518
519impl Drop for LockedFlake {
520  fn drop(&mut self) {
521    // SAFETY: We own the locked flake and it is valid until drop
522    unsafe {
523      sys::nix_locked_flake_free(self.inner.as_ptr());
524    }
525  }
526}
527
528// SAFETY: `LockedFlake` owns its `nix_locked_flake*` and keeps the
529// context alive via `Arc<Context>`. The locked-flake value is immutable
530// once produced; calling `output_attrs` only reads from it but still
531// routes through `Context`'s error buffer, which is why `Sync` is NOT
532// implemented.
533unsafe impl Send for LockedFlake {}
534
535#[cfg(test)]
536mod tests {
537  use std::sync::Arc;
538
539  use serial_test::serial;
540
541  use super::*;
542  use crate::{Context, EvalStateBuilder, Store};
543
544  fn make_state(ctx: &Arc<Context>) -> (Arc<Store>, EvalState) {
545    let store = Arc::new(Store::open(ctx, None).expect("Failed to open store"));
546    let flake_settings =
547      FlakeSettings::new(ctx).expect("Failed to create flake settings");
548    let state = EvalStateBuilder::new(&store)
549      .expect("Failed to create builder")
550      .with_flake_settings(&flake_settings)
551      .expect("Failed to apply flake settings")
552      .build()
553      .expect("Failed to build state");
554    (store, state)
555  }
556
557  #[test]
558  #[serial]
559  fn test_flake_settings_new() {
560    let ctx = Arc::new(Context::new().expect("Failed to create context"));
561    let _settings =
562      FlakeSettings::new(&ctx).expect("Failed to create flake settings");
563  }
564
565  #[test]
566  #[serial]
567  fn test_flake_settings_with_eval_state() {
568    let ctx = Arc::new(Context::new().expect("Failed to create context"));
569    make_state(&ctx);
570  }
571
572  #[test]
573  #[serial]
574  fn test_fetchers_settings_new() {
575    let ctx = Arc::new(Context::new().expect("Failed to create context"));
576    let _s =
577      FetchersSettings::new(&ctx).expect("Failed to create fetcher settings");
578  }
579
580  #[test]
581  #[serial]
582  fn test_flake_reference_parse_flags_new() {
583    let ctx = Arc::new(Context::new().expect("Failed to create context"));
584    let settings = Arc::new(
585      FlakeSettings::new(&ctx).expect("Failed to create flake settings"),
586    );
587    let _f = FlakeReferenceParseFlags::new(&ctx, &settings)
588      .expect("Failed to create parse flags");
589  }
590
591  #[test]
592  #[serial]
593  fn test_flake_reference_parse_flags_set_base_directory() {
594    let ctx = Arc::new(Context::new().expect("Failed to create context"));
595    let settings = Arc::new(
596      FlakeSettings::new(&ctx).expect("Failed to create flake settings"),
597    );
598    let _f = FlakeReferenceParseFlags::new(&ctx, &settings)
599      .expect("Failed to create parse flags")
600      .set_base_directory("/tmp")
601      .expect("Failed to set base directory");
602  }
603
604  #[test]
605  #[serial]
606  fn test_lock_flags_new() {
607    let ctx = Arc::new(Context::new().expect("Failed to create context"));
608    let settings = Arc::new(
609      FlakeSettings::new(&ctx).expect("Failed to create flake settings"),
610    );
611    let _f =
612      LockFlags::new(&ctx, &settings).expect("Failed to create lock flags");
613  }
614
615  #[test]
616  #[serial]
617  fn test_lock_flags_set_modes() {
618    let ctx = Arc::new(Context::new().expect("Failed to create context"));
619    let settings = Arc::new(
620      FlakeSettings::new(&ctx).expect("Failed to create flake settings"),
621    );
622    let _check = LockFlags::new(&ctx, &settings)
623      .expect("create")
624      .set_mode(LockMode::Check)
625      .expect("set Check");
626    let _virtual = LockFlags::new(&ctx, &settings)
627      .expect("create")
628      .set_mode(LockMode::Virtual)
629      .expect("set Virtual");
630    let _write = LockFlags::new(&ctx, &settings)
631      .expect("create")
632      .set_mode(LockMode::WriteAsNeeded)
633      .expect("set WriteAsNeeded");
634  }
635}