Skip to main content

guix_scheme/
module.rs

1//! Thin typed layer over guix package/service module files.
2
3use crate::record::{RecordView, RecordViewMut};
4use crate::Error;
5use scheme_edit::{string_lit, sym, Document, Node};
6
7/// A parsed guix module file (e.g. `gnu/packages/*.scm`), lossless.
8#[derive(Debug)]
9pub struct ModuleFile {
10    doc: Document,
11}
12
13impl ModuleFile {
14    pub fn parse(src: &str) -> Result<Self, Error> {
15        Ok(Self {
16            doc: Document::parse(src)?,
17        })
18    }
19
20    /// Names bound by top-level `(define-public <sym> ...)` forms whose
21    /// value contains a `(package ...)` / `(package/inherit ...)` node,
22    /// including under wrappers like `let`. Packages behind quoted or
23    /// non-list contexts are not matched.
24    pub fn package_names(&self) -> Vec<String> {
25        self.doc
26            .forms()
27            .filter(|n| n.head_symbol() == Some("define-public"))
28            .filter(|n| find_package(n).is_some())
29            .filter_map(|n| n.data_child(1)?.as_symbol().map(str::to_owned))
30            .collect()
31    }
32
33    pub fn package(&self, sym: &str) -> Option<PackageView<'_>> {
34        let form = self.define_public_form(sym)?;
35        let pkg = find_package(form)?;
36        Some(PackageView {
37            view: RecordView::new(pkg)?,
38        })
39    }
40
41    pub fn package_mut(&mut self, sym: &str) -> Option<PackageViewMut<'_>> {
42        let (form_idx, path) = {
43            let (i, form) = self
44                .doc
45                .forms()
46                .enumerate()
47                .find(|(_, n)| is_define_public_of(n, sym))?;
48            let mut path = Vec::new();
49            if !find_package_path(form, &mut path) {
50                return None;
51            }
52            (i, path)
53        };
54        let mut node = self.doc.forms_mut().nth(form_idx)?;
55        for idx in path {
56            node = node.data_child_mut(idx)?;
57        }
58        Some(PackageViewMut {
59            view: RecordViewMut::new(node)?,
60        })
61    }
62
63    /// Names of top-level `define-record-type*` forms, e.g.
64    /// `<avahi-configuration>`.
65    pub fn record_type_names(&self) -> Vec<String> {
66        self.doc
67            .forms()
68            .filter(|n| n.head_symbol() == Some("define-record-type*"))
69            .filter_map(|n| n.data_child(1)?.as_symbol().map(str::to_owned))
70            .collect()
71    }
72
73    /// Read-only view over a top-level `define-record-type*` form,
74    /// looked up by its type name (e.g. `<avahi-configuration>`).
75    pub fn record_type(&self, name: &str) -> Option<RecordTypeView<'_>> {
76        let node = self.doc.forms().find(|n| {
77            n.head_symbol() == Some("define-record-type*")
78                && n.data_child(1).and_then(Node::as_symbol) == Some(name)
79        })?;
80        Some(RecordTypeView { node })
81    }
82
83    fn define_public_form(&self, sym: &str) -> Option<&Node> {
84        self.doc.forms().find(|n| is_define_public_of(n, sym))
85    }
86}
87
88/// `(define-record-type* <name> ctor make-ctor pred? (field getter
89/// [(default expr)]) ...)`. Clauses other than `(default ...)` (e.g.
90/// `thunked`, `sanitize`) are ignored.
91pub struct RecordTypeView<'a> {
92    node: &'a Node,
93}
94
95impl<'a> RecordTypeView<'a> {
96    pub fn name(&self) -> Option<&'a str> {
97        self.node.data_child(1)?.as_symbol()
98    }
99
100    /// The syntactic constructor symbol.
101    pub fn constructor(&self) -> Option<&'a str> {
102        self.node.data_child(2)?.as_symbol()
103    }
104
105    pub fn predicate(&self) -> Option<&'a str> {
106        self.node.data_child(4)?.as_symbol()
107    }
108
109    pub fn fields(&self) -> Vec<RecordFieldView<'a>> {
110        self.node
111            .list_nodes()
112            .skip(5)
113            .filter(|n| n.head_symbol().is_some())
114            .map(|node| RecordFieldView { node })
115            .collect()
116    }
117}
118
119pub struct RecordFieldView<'a> {
120    node: &'a Node,
121}
122
123impl<'a> RecordFieldView<'a> {
124    pub fn name(&self) -> Option<&'a str> {
125        self.node.data_child(0)?.as_symbol()
126    }
127
128    pub fn getter(&self) -> Option<&'a str> {
129        self.node.data_child(1)?.as_symbol()
130    }
131
132    /// The default expression as source text, `None` without a
133    /// `(default ...)` clause.
134    pub fn default(&self) -> Option<String> {
135        let clause = self
136            .node
137            .list_nodes()
138            .skip(2)
139            .find(|n| n.head_symbol() == Some("default"))?;
140        Some(clause.data_child(1)?.to_source())
141    }
142}
143
144impl std::fmt::Display for ModuleFile {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        self.doc.fmt(f)
147    }
148}
149
150/// Read-only view over a `(package ...)` node.
151pub struct PackageView<'a> {
152    view: RecordView<'a>,
153}
154
155impl PackageView<'_> {
156    pub fn name(&self) -> Option<String> {
157        self.view.string_field("name")
158    }
159
160    /// `None` when the version is a computed expression, as in
161    /// `(git-version ...)` / `(string-append ...)`.
162    pub fn version(&self) -> Option<String> {
163        self.view.string_field("version")
164    }
165
166    pub fn synopsis(&self) -> Option<String> {
167        self.view.string_field("synopsis")
168    }
169
170    pub fn home_page(&self) -> Option<String> {
171        self.view.string_field("home-page")
172    }
173
174    /// The base32 string under `source -> origin -> sha256 -> base32`.
175    pub fn origin_sha256_base32(&self) -> Option<String> {
176        let origin = self
177            .view
178            .field("source")
179            .filter(|n| n.head_symbol() == Some("origin"))?;
180        let sha = RecordView::new(origin)?.field("sha256")?;
181        if sha.head_symbol() != Some("base32") {
182            return None;
183        }
184        sha.data_child(1)?.as_string_lit()
185    }
186
187    /// Symbols in modern `(inputs (list a b c))` style. Non-symbol
188    /// entries (calls like `(librsvg-for-system)`, quasiquoted pairs)
189    /// are skipped; other styles yield an empty vec.
190    pub fn inputs(&self) -> Vec<String> {
191        self.input_list("inputs")
192    }
193
194    pub fn native_inputs(&self) -> Vec<String> {
195        self.input_list("native-inputs")
196    }
197
198    pub fn propagated_inputs(&self) -> Vec<String> {
199        self.input_list("propagated-inputs")
200    }
201
202    fn input_list(&self, field: &str) -> Vec<String> {
203        let Some(v) = self.view.field(field) else {
204            return Vec::new();
205        };
206        if v.head_symbol() != Some("list") {
207            return Vec::new();
208        }
209        v.list_nodes()
210            .skip(1)
211            .filter_map(|n| n.as_symbol().map(str::to_owned))
212            .collect()
213    }
214}
215
216/// Mutable view over a `(package ...)` node.
217pub struct PackageViewMut<'a> {
218    view: RecordViewMut<'a>,
219}
220
221impl PackageViewMut<'_> {
222    pub fn set_field(&mut self, name: &str, value: Node) {
223        self.view.set_field(name, value);
224    }
225
226    pub fn field_mut(&mut self, name: &str) -> Option<&mut Node> {
227        self.view.field_mut(name)
228    }
229
230    pub fn set_version(&mut self, version: &str) {
231        self.view.set_field("version", string_lit(version));
232    }
233
234    /// Replaces the base32 string in place. `false` when the package
235    /// does not have the `source -> origin -> sha256 -> base32` shape.
236    pub fn set_origin_sha256_base32(&mut self, hash: &str) -> bool {
237        let Some(origin) = self.view.field_mut("source") else {
238            return false;
239        };
240        if origin.head_symbol() != Some("origin") {
241            return false;
242        }
243        let Some(mut ov) = RecordViewMut::new(origin) else {
244            return false;
245        };
246        let Some(sha) = ov.field_mut("sha256") else {
247            return false;
248        };
249        if sha.head_symbol() != Some("base32") {
250            return false;
251        }
252        sha.replace_child(1, string_lit(hash))
253    }
254
255    /// Swaps a symbol in list-style inputs (also native/propagated).
256    pub fn replace_input(&mut self, old: &str, new: &str) -> bool {
257        for field in ["inputs", "native-inputs", "propagated-inputs"] {
258            if let Some(v) = self.view.field_mut(field) {
259                if v.head_symbol() == Some("list") {
260                    if let Some(idx) = v.position_of(|n| n.as_symbol() == Some(old)) {
261                        return v.replace_child(idx, sym(new));
262                    }
263                }
264            }
265        }
266        false
267    }
268}
269
270fn is_define_public_of(form: &Node, name: &str) -> bool {
271    form.head_symbol() == Some("define-public")
272        && form.data_child(1).and_then(Node::as_symbol) == Some(name)
273}
274
275fn find_package(node: &Node) -> Option<&Node> {
276    for child in node.list_nodes().skip(1) {
277        if matches!(child.head_symbol(), Some("package" | "package/inherit")) {
278            return Some(child);
279        }
280        if child.head_symbol().is_some() {
281            if let Some(p) = find_package(child) {
282                return Some(p);
283            }
284        }
285    }
286    None
287}
288
289/// Data-index path from NODE down to the first package node, for the
290/// mutable descent in `package_mut`.
291fn find_package_path(node: &Node, path: &mut Vec<usize>) -> bool {
292    for (i, child) in node.list_nodes().enumerate().skip(1) {
293        if matches!(child.head_symbol(), Some("package" | "package/inherit")) {
294            path.push(i);
295            return true;
296        }
297        if child.head_symbol().is_some() {
298            path.push(i);
299            if find_package_path(child, path) {
300                return true;
301            }
302            path.pop();
303        }
304    }
305    false
306}