Skip to main content

sui_spec/
profile.rs

1//! Typed border for nix-env / nix profile generations.
2//!
3//! A profile is `/nix/var/nix/profiles/<name>` — a symlink to the
4//! current generation, with sibling `<name>-<N>-link` symlinks for
5//! historical generations.  `nix-env -i`, `nix profile install`,
6//! `home-manager activate` all maintain generations.  This module
7//! names the structure.
8
9use serde::{Deserialize, Serialize};
10use tatara_lisp::DeriveTataraDomain;
11
12use crate::SpecError;
13
14#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
15#[tatara(keyword = "defprofile-format")]
16pub struct ProfileFormat {
17    pub name: String,
18    pub kind: ProfileKind,
19    /// Pattern for generation-N symlinks (cppnix uses
20    /// `<profile>-<N>-link`).
21    #[serde(rename = "generationLinkPattern")]
22    pub generation_link_pattern: String,
23    /// Pattern for the manifest file (cppnix profile.* nests a
24    /// JSON manifest after Nix 2.4).
25    #[serde(rename = "manifestPath")]
26    pub manifest_path: String,
27}
28
29#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum ProfileKind {
31    /// `/nix/var/nix/profiles/<name>` — system-level profile (root
32    /// profile, system profile).
33    System,
34    /// `~/.nix-profile/` (legacy) or
35    /// `~/.local/state/nix/profiles/profile/` (per-user 2.4+).
36    User,
37    /// nix-shell / nix develop ephemeral profile.  Cleaned at exit.
38    Ephemeral,
39}
40
41pub const CANONICAL_PROFILE_LISP: &str = include_str!("../specs/profile.lisp");
42
43/// Compile every authored profile format.
44///
45/// # Errors
46///
47/// Returns an error if the Lisp source fails to parse.
48pub fn load_canonical() -> Result<Vec<ProfileFormat>, SpecError> {
49    crate::loader::load_all::<ProfileFormat>(CANONICAL_PROFILE_LISP)
50}
51
52// ── M3.0 generation-link helpers ───────────────────────────────────
53
54/// Compute the generation-link path for a profile + generation
55/// number, substituting the format's `<profile>` and `<N>`
56/// placeholders.
57#[must_use]
58pub fn generation_link(format: &ProfileFormat, profile: &str, n: u32) -> String {
59    format
60        .generation_link_pattern
61        .replace("<profile>", profile)
62        .replace("<N>", &n.to_string())
63}
64
65/// Compute the next generation link given the current generation
66/// number.  Convenience wrapper around [`generation_link`].
67#[must_use]
68pub fn next_generation(format: &ProfileFormat, profile: &str, current: u32) -> String {
69    generation_link(format, profile, current + 1)
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn canonical_profile_parses() {
78        let formats = load_canonical().unwrap();
79        assert!(!formats.is_empty());
80    }
81
82    // ── M3.0 generation-link tests ─────────────────────────────
83
84    fn system_fmt() -> ProfileFormat {
85        load_canonical().unwrap().into_iter()
86            .find(|f| f.name == "cppnix-system-profile").unwrap()
87    }
88
89    #[test]
90    fn generation_link_substitutes_placeholders() {
91        let f = system_fmt();
92        let link = generation_link(&f, "system", 42);
93        assert_eq!(link, "system-42-link");
94    }
95
96    #[test]
97    fn next_generation_increments() {
98        let f = system_fmt();
99        let next = next_generation(&f, "system", 41);
100        assert_eq!(next, "system-42-link");
101    }
102
103    #[test]
104    fn three_profile_kinds_present() {
105        let formats = load_canonical().unwrap();
106        let kinds: std::collections::HashSet<ProfileKind> =
107            formats.iter().map(|f| f.kind).collect();
108        for required in [ProfileKind::System, ProfileKind::User, ProfileKind::Ephemeral] {
109            assert!(kinds.contains(&required), "missing profile kind {required:?}");
110        }
111    }
112}