snix_glue/builtins/
mod.rs

1//! Contains builtins that deal with the store or builder.
2
3use std::rc::Rc;
4
5use crate::snix_store_io::SnixStoreIO;
6
7mod derivation;
8mod errors;
9mod fetchers;
10mod import;
11mod utils;
12
13pub use errors::{DerivationError, FetcherError, ImportError};
14
15/// Adds derivation-related builtins to the passed [snix_eval::EvaluationBuilder]:
16///
17/// * `derivation`
18/// * `derivationStrict`
19/// * `toFile`
20///
21/// As they need to interact with `known_paths`, we also need to pass in
22/// `known_paths`.
23pub fn add_derivation_builtins<'co, 'ro, 'env, IO>(
24    eval_builder: snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO>,
25    io: Rc<SnixStoreIO>,
26) -> snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO> {
27    eval_builder
28        .add_builtins(derivation::derivation_builtins::builtins(Rc::clone(&io)))
29        // Add the actual `builtins.derivation` from compiled Nix code
30        .add_src_builtin("derivation", include_str!("derivation.nix"))
31}
32
33/// Adds fetcher builtins to the passed [snix_eval::EvaluationBuilder]:
34///
35/// * `fetchurl`
36/// * `fetchTarball`
37/// * `fetchGit`
38pub fn add_fetcher_builtins<'co, 'ro, 'env, IO>(
39    eval_builder: snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO>,
40    io: Rc<SnixStoreIO>,
41) -> snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO> {
42    eval_builder.add_builtins(fetchers::fetcher_builtins::builtins(Rc::clone(&io)))
43}
44
45/// Adds import-related builtins to the passed [snix_eval::EvaluationBuilder]:
46///
47///
48/// * `filterSource`
49/// * `path`
50/// * `storePath`
51///
52/// As they need to interact with the store implementation, we pass [`SnixStoreIO`].
53/// Due to #176, some IO still sidesteps `EvalIO` and accesses the filesystem directly.
54pub fn add_import_builtins<'co, 'ro, 'env, IO>(
55    eval_builder: snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO>,
56    io: Rc<SnixStoreIO>,
57) -> snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO> {
58    eval_builder.add_builtins(import::import_builtins(io))
59}
60
61#[cfg(test)]
62mod tests {
63    use std::{fs, rc::Rc, sync::Arc};
64
65    use crate::snix_store_io::SnixStoreIO;
66
67    use super::{add_derivation_builtins, add_fetcher_builtins, add_import_builtins};
68    use clap::Parser;
69    use nix_compat::store_path::hash_placeholder;
70    use rstest::rstest;
71    use snix_build::buildservice::DummyBuildService;
72    use snix_eval::{EvalIO, EvaluationResult};
73    use snix_store::utils::{ServiceUrlsMemory, construct_services};
74    use tempfile::TempDir;
75
76    /// evaluates a given nix expression and returns the result.
77    /// Takes care of setting up the evaluator so it knows about the
78    // `derivation` builtin.
79    fn eval(str: &str) -> EvaluationResult {
80        // We assemble a complete store in memory.
81        let runtime = tokio::runtime::Runtime::new().expect("Failed to build a Tokio runtime");
82        let (blob_service, directory_service, path_info_service, nar_calculation_service) = runtime
83            .block_on(async {
84                construct_services(ServiceUrlsMemory::parse_from(std::iter::empty::<&str>())).await
85            })
86            .expect("Failed to construct store services in memory");
87
88        let io = Rc::new(SnixStoreIO::new(
89            blob_service,
90            directory_service,
91            path_info_service,
92            nar_calculation_service.into(),
93            Arc::<DummyBuildService>::default(),
94            runtime.handle().clone(),
95            Vec::new(),
96        ));
97
98        let mut eval_builder = snix_eval::Evaluation::builder(io.clone() as Rc<dyn EvalIO>);
99        eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&io));
100        eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&io));
101        eval_builder = add_import_builtins(eval_builder, io);
102        let eval = eval_builder.build();
103
104        // run the evaluation itself.
105        eval.evaluate(str, None)
106    }
107
108    #[test]
109    fn derivation() {
110        let result = eval(
111            r#"(derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux";}).outPath"#,
112        );
113
114        assert!(result.errors.is_empty(), "expect evaluation to succeed");
115        let value = result.value.expect("must be some");
116
117        match value {
118            snix_eval::Value::String(s) => {
119                assert_eq!(*s, "/nix/store/xpcvxsx5sw4rbq666blz6sxqlmsqphmr-foo",);
120            }
121            _ => panic!("unexpected value type: {value:?}"),
122        }
123    }
124
125    /// a derivation with an empty name is an error.
126    #[test]
127    fn derivation_empty_name_fail() {
128        let result = eval(
129            r#"(derivation { name = ""; builder = "/bin/sh"; system = "x86_64-linux";}).outPath"#,
130        );
131
132        assert!(!result.errors.is_empty(), "expect evaluation to fail");
133    }
134
135    /// construct some calls to builtins.derivation and compare produced output
136    /// paths.
137    #[rstest]
138    #[case::r_sha256(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/17wgs52s7kcamcyin4ja58njkf91ipq8-foo")]
139    #[case::r_sha256_other_name(r#"(builtins.derivation { name = "foo2"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/gi0p8vd635vpk1nq029cz3aa3jkhar5k-foo2")]
140    #[case::r_sha1(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha1"; outputHash = "sha1-VUCRC+16gU5lcrLYHlPSUyx0Y/Q="; }).outPath"#, "/nix/store/p5sammmhpa84ama7ymkbgwwzrilva24x-foo")]
141    #[case::r_md5(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "md5"; outputHash = "md5-07BzhNET7exJ6qYjitX/AA=="; }).outPath"#, "/nix/store/gmmxgpy1jrzs86r5y05wy6wiy2m15xgi-foo")]
142    #[case::r_sha512(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha512"; outputHash = "sha512-DPkYCnZKuoY6Z7bXLwkYvBMcZ3JkLLLc5aNPCnAvlHDdwr8SXBIZixmVwjPDS0r9NGxUojNMNQqUilG26LTmtg=="; }).outPath"#, "/nix/store/lfi2bfyyap88y45mfdwi4j99gkaxaj19-foo")]
143    #[case::r_sha256_base16(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "4374173a8cbe88de152b609f96f46e958bcf65762017474eec5a05ec2bd61530"; }).outPath"#, "/nix/store/17wgs52s7kcamcyin4ja58njkf91ipq8-foo")]
144    #[case::r_sha256_nixbase32(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "0c0msqmyq1asxi74f5r0frjwz2wmdvs9d7v05caxx25yihx1fx23"; }).outPath"#, "/nix/store/17wgs52s7kcamcyin4ja58njkf91ipq8-foo")]
145    #[case::r_sha256_base64(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/17wgs52s7kcamcyin4ja58njkf91ipq8-foo")]
146    #[case::r_sha256_base64_nopad(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "sha256-fgIr3TyFGDAXP5+qoAaiMKDg/a1MlT6Fv/S/DaA24S8="; }).outPath"#, "/nix/store/xm1l9dx4zgycv9qdhcqqvji1z88z534b-foo")]
147    #[case::sha256(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "flat"; outputHashAlgo = "sha256"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/q4pkwkxdib797fhk22p0k3g1q32jmxvf-foo")]
148    #[case::sha256_other_name(r#"(builtins.derivation { name = "foo2"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "flat"; outputHashAlgo = "sha256"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/znw17xlmx9r6gw8izjkqxkl6s28sza4l-foo2")]
149    #[case::sha1(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "flat"; outputHashAlgo = "sha1"; outputHash = "sha1-VUCRC+16gU5lcrLYHlPSUyx0Y/Q="; }).outPath"#, "/nix/store/zgpnjjmga53d8srp8chh3m9fn7nnbdv6-foo")]
150    #[case::md5(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "flat"; outputHashAlgo = "md5"; outputHash = "md5-07BzhNET7exJ6qYjitX/AA=="; }).outPath"#, "/nix/store/jfhcwnq1852ccy9ad9nakybp2wadngnd-foo")]
151    #[case::sha512(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "flat"; outputHashAlgo = "sha512"; outputHash = "sha512-DPkYCnZKuoY6Z7bXLwkYvBMcZ3JkLLLc5aNPCnAvlHDdwr8SXBIZixmVwjPDS0r9NGxUojNMNQqUilG26LTmtg=="; }).outPath"#, "/nix/store/as736rr116ian9qzg457f96j52ki8bm3-foo")]
152    #[case::r_sha256_outputhashalgo_omitted(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/17wgs52s7kcamcyin4ja58njkf91ipq8-foo")]
153    #[case::r_sha256_outputhashalgo_and_outputhashmode_omitted(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/q4pkwkxdib797fhk22p0k3g1q32jmxvf-foo")]
154    #[case::outputhash_omitted(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; }).outPath"#, "/nix/store/xpcvxsx5sw4rbq666blz6sxqlmsqphmr-foo")]
155    #[case::multiple_outputs(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; outputs = ["foo" "bar"]; system = "x86_64-linux"; }).outPath"#, "/nix/store/hkwdinvz2jpzgnjy9lv34d2zxvclj4s3-foo-foo")]
156    #[case::args(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; args = ["--foo" "42" "--bar"]; system = "x86_64-linux"; }).outPath"#, "/nix/store/365gi78n2z7vwc1bvgb98k0a9cqfp6as-foo")]
157    #[case::full(r#"
158                   let
159                     bar = builtins.derivation {
160                       name = "bar";
161                       builder = ":";
162                       system = ":";
163                       outputHash = "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba";
164                       outputHashAlgo = "sha256";
165                       outputHashMode = "recursive";
166                     };
167                   in
168                   (builtins.derivation {
169                     name = "foo";
170                     builder = ":";
171                     system = ":";
172                     inherit bar;
173                   }).outPath
174        "#, "/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo")]
175    #[case::pass_as_file(r#"(builtins.derivation { "name" = "foo"; passAsFile = ["bar"]; bar = "baz"; system = ":"; builder = ":";}).outPath"#, "/nix/store/25gf0r1ikgmh4vchrn8qlc4fnqlsa5a1-foo")]
176    // __ignoreNulls = true, but nothing set to null
177    #[case::ignore_nulls_true_no_arg_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = true; }).drvPath"#, "/nix/store/xa96w6d7fxrlkk60z1fmx2ffp2wzmbqx-foo.drv")]
178    #[case::ignore_nulls_true_no_arg_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = true; }).outPath"#, "/nix/store/pk2agn9za8r9bxsflgh1y7fyyrmwcqkn-foo")]
179    // __ignoreNulls = true, with a null arg, same paths
180    #[case::ignore_nulls_true_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = true; ignoreme = null; }).drvPath"#, "/nix/store/xa96w6d7fxrlkk60z1fmx2ffp2wzmbqx-foo.drv")]
181    #[case::ignore_nulls_true_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = true; ignoreme = null; }).outPath"#, "/nix/store/pk2agn9za8r9bxsflgh1y7fyyrmwcqkn-foo")]
182    // __ignoreNulls = false
183    #[case::ignore_nulls_false_no_arg_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = false; }).drvPath"#, "/nix/store/xa96w6d7fxrlkk60z1fmx2ffp2wzmbqx-foo.drv")]
184    #[case::ignore_nulls_false_no_arg_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = false; }).outPath"#, "/nix/store/pk2agn9za8r9bxsflgh1y7fyyrmwcqkn-foo")]
185    // __ignoreNulls = false, with a null arg
186    #[case::ignore_nulls_fales_arg_path_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = false; foo = null; }).drvPath"#, "/nix/store/xwkwbajfiyhdqmksrbzm0s4g4ib8d4ms-foo.drv")]
187    #[case::ignore_nulls_fales_arg_path_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = false; foo = null; }).outPath"#, "/nix/store/2n2jqm6l7r2ahi19m58pl896ipx9cyx6-foo")]
188    // structured attrs set to false will render an empty string inside env
189    #[case::structured_attrs_false_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = false; foo = "bar"; }).drvPath"#, "/nix/store/qs39krwr2lsw6ac910vqx4pnk6m63333-foo.drv")]
190    #[case::structured_attrs_false_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = false; foo = "bar"; }).outPath"#, "/nix/store/9yy3764rdip3fbm8ckaw4j9y7vh4d231-foo")]
191    // simple structured attrs
192    #[case::structured_attrs_simple_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = true; foo = "bar"; }).drvPath"#, "/nix/store/k6rlb4k10cb9iay283037ml1nv3xma2f-foo.drv")]
193    #[case::structured_attrs_simple_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = true; foo = "bar"; }).outPath"#, "/nix/store/6lmv3hyha1g4cb426iwjyifd7nrdv1xn-foo")]
194    // structured attrs with outputsCheck
195    #[case::structured_attrs_output_checks_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = true; foo = "bar"; outputChecks = {out = {maxClosureSize = 256 * 1024 * 1024; disallowedRequisites = [ "dev" ];};}; }).drvPath"#, "/nix/store/fx9qzpchh5wchchhy39bwsml978d6wp1-foo.drv")]
196    #[case::structured_attrs_output_checks_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = true; foo = "bar"; outputChecks = {out = {maxClosureSize = 256 * 1024 * 1024; disallowedRequisites = [ "dev" ];};}; }).outPath"#, "/nix/store/pcywah1nwym69rzqdvpp03sphfjgyw1l-foo")]
197    // structured attrs and __ignoreNulls. ignoreNulls is inactive (so foo ends up in __json, yet __ignoreNulls itself is not present.
198    #[case::structured_attrs_and_ignore_nulls_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = false; foo = null; __structuredAttrs = true; }).drvPath"#, "/nix/store/rldskjdcwa3p7x5bqy3r217va1jsbjsc-foo.drv")]
199    // structured attrs, setting outputs.
200    #[case::structured_attrs_outputs_drvpath(r#"(builtins.derivation { name = "test"; system = "aarch64-linux"; builder = "/bin/sh"; __structuredAttrs = true; outputs = [ "out"]; }).drvPath"#, "/nix/store/6sgawp30zibsh525p7c948xxd22y2ngy-test.drv")]
201    fn test_outpath(#[case] code: &str, #[case] expected_path: &str) {
202        let value = eval(code).value.expect("must succeed");
203
204        match value {
205            snix_eval::Value::String(s) => {
206                assert_eq!(*s, expected_path);
207            }
208            _ => panic!("unexpected value type: {value:?}"),
209        }
210    }
211
212    /// construct some calls to builtins.derivation that should be rejected
213    #[rstest]
214    #[case::invalid_outputhash(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "sha256-00"; }).outPath"#)]
215    #[case::sha1_and_sha256(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha1"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#)]
216    #[case::duplicate_output_names(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; outputs = ["foo" "foo"]; system = "x86_64-linux"; }).outPath"#)]
217    fn test_outpath_invalid(#[case] code: &str) {
218        let resp = eval(code);
219        assert!(resp.value.is_none(), "Value should be None");
220        assert!(
221            !resp.errors.is_empty(),
222            "There should have been some errors"
223        );
224    }
225
226    /// Construct two FODs with the same name, and same known output (but
227    /// slightly different recipe), ensure they have the same output hash.
228    #[test]
229    fn test_fod_outpath() {
230        let code = r#"
231          (builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath ==
232          (builtins.derivation { name = "foo"; builder = "/bin/aa"; system = "x86_64-linux"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath
233        "#;
234
235        let value = eval(code).value.expect("must succeed");
236        match value {
237            snix_eval::Value::Bool(v) => {
238                assert!(v);
239            }
240            _ => panic!("unexpected value type: {value:?}"),
241        }
242    }
243
244    /// Construct two FODs with the same name, and same known output (but
245    /// slightly different recipe), ensure they have the same output hash.
246    #[test]
247    fn test_fod_outpath_different_name() {
248        let code = r#"
249          (builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath ==
250          (builtins.derivation { name = "foo"; builder = "/bin/aa"; system = "x86_64-linux"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath
251        "#;
252
253        let value = eval(code).value.expect("must succeed");
254        match value {
255            snix_eval::Value::Bool(v) => {
256                assert!(v);
257            }
258            _ => panic!("unexpected value type: {value:?}"),
259        }
260    }
261
262    /// Construct two derivations with the same parameters except one of them lost a context string
263    /// for a dependency, causing the loss of an element in the `inputDrvs` derivation. Therefore,
264    /// making `outPath` different.
265    #[test]
266    fn test_unsafe_discard_string_context() {
267        let code = r#"
268        let
269            dep = builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; };
270        in
271          (builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; env = "${dep}"; }).outPath !=
272          (builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; env = "${builtins.unsafeDiscardStringContext dep}"; }).outPath
273        "#;
274
275        let value = eval(code).value.expect("must succeed");
276        match value {
277            snix_eval::Value::Bool(v) => {
278                assert!(v);
279            }
280            _ => panic!("unexpected value type: {value:?}"),
281        }
282    }
283
284    /// Construct an attribute set that coerces to a derivation and verify that the return type is
285    /// a string.
286    #[test]
287    fn test_unsafe_discard_string_context_of_coercible() {
288        let code = r#"
289        let
290            dep = builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; };
291            attr = { __toString = _: dep; };
292        in
293            builtins.typeOf (builtins.unsafeDiscardStringContext attr) == "string"
294        "#;
295
296        let value = eval(code).value.expect("must succeed");
297        match value {
298            snix_eval::Value::Bool(v) => {
299                assert!(v);
300            }
301            _ => panic!("unexpected value type: {value:?}"),
302        }
303    }
304
305    #[rstest]
306    #[case::input_in_args(r#"
307                   let
308                     bar = builtins.derivation {
309                       name = "bar";
310                       builder = ":";
311                       system = ":";
312                       outputHash = "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba";
313                       outputHashAlgo = "sha256";
314                       outputHashMode = "recursive";
315                     };
316                   in
317                   (builtins.derivation {
318                     name = "foo";
319                     builder = ":";
320                     args = [ "${bar}" ];
321                     system = ":";
322                   }).drvPath
323        "#, "/nix/store/50yl2gmmljyl0lzyrp1mcyhn53vhjhkd-foo.drv")]
324    fn test_inputs_derivation_from_context(#[case] code: &str, #[case] expected_drvpath: &str) {
325        let eval_result = eval(code);
326
327        let value = eval_result.value.expect("must succeed");
328
329        match value {
330            snix_eval::Value::String(s) => {
331                assert_eq!(*s, expected_drvpath);
332            }
333
334            _ => panic!("unexpected value type: {value:?}"),
335        };
336    }
337
338    #[test]
339    fn builtins_placeholder_hashes() {
340        assert_eq!(
341            hash_placeholder("out").as_str(),
342            "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"
343        );
344
345        assert_eq!(
346            hash_placeholder("").as_str(),
347            "/171rf4jhx57xqz3p7swniwkig249cif71pa08p80mgaf0mqz5bmr"
348        );
349    }
350
351    /// constructs calls to builtins.derivation that should succeed, but produce warnings
352    #[rstest]
353    #[case::r_sha256_wrong_padding(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "sha256-fgIr3TyFGDAXP5+qoAaiMKDg/a1MlT6Fv/S/DaA24S8===="; }).outPath"#, "/nix/store/xm1l9dx4zgycv9qdhcqqvji1z88z534b-foo")]
354    fn builtins_derivation_hash_wrong_padding_warn(
355        #[case] code: &str,
356        #[case] expected_path: &str,
357    ) {
358        let eval_result = eval(code);
359
360        let value = eval_result.value.expect("must succeed");
361
362        match value {
363            snix_eval::Value::String(s) => {
364                assert_eq!(*s, expected_path);
365            }
366            _ => panic!("unexpected value type: {value:?}"),
367        }
368
369        assert!(
370            !eval_result.warnings.is_empty(),
371            "warnings should not be empty"
372        );
373    }
374
375    /// Invokes `builtins.filterSource` on various carefully-crated subdirs, and
376    /// ensures the resulting store paths matches what Nix produces.
377    /// @fixtures is replaced to the fixtures directory.
378    #[rstest]
379    #[cfg(target_family = "unix")]
380    #[case::complicated_filter_nothing(
381        r#"(builtins.filterSource (p: t: true) @fixtures)"#,
382        "/nix/store/bqh6kd0x3vps2rzagzpl7qmbbgnx19cp-import_fixtures"
383    )]
384    #[case::complicated_filter_everything(
385        r#"(builtins.filterSource (p: t: false) @fixtures)"#,
386        "/nix/store/giq6czz24lpjg97xxcxk6rg950lcpib1-import_fixtures"
387    )]
388    #[case::simple_dir_with_one_file_filter_dirs(
389        r#"(builtins.filterSource (p: t: t != "directory") @fixtures/a_dir)"#,
390        "/nix/store/8vbqaxapywkvv1hacdja3pi075r14d43-a_dir"
391    )]
392    #[case::simple_dir_with_one_file_filter_files(
393        r#"(builtins.filterSource (p: t: t != "regular") @fixtures/a_dir)"#,
394        "/nix/store/zphlqc93s2iq4xm393l06hzf8hp85r4z-a_dir"
395    )]
396    #[case::simple_dir_with_one_file_filter_symlinks(
397        r#"(builtins.filterSource (p: t: t != "symlink") @fixtures/a_dir)"#,
398        "/nix/store/8vbqaxapywkvv1hacdja3pi075r14d43-a_dir"
399    )]
400    #[case::simple_dir_with_one_file_filter_nothing(
401        r#"(builtins.filterSource (p: t: true) @fixtures/a_dir)"#,
402        "/nix/store/8vbqaxapywkvv1hacdja3pi075r14d43-a_dir"
403    )]
404    #[case::simple_dir_with_one_file_filter_everything(
405        r#"(builtins.filterSource (p: t: false) @fixtures/a_dir)"#,
406        "/nix/store/zphlqc93s2iq4xm393l06hzf8hp85r4z-a_dir"
407    )]
408    #[case::simple_dir_with_one_dir_filter_dirs(
409        r#"builtins.filterSource (p: t: t != "directory") @fixtures/b_dir"#,
410        "/nix/store/xzsfzdgrxg93icaamjm8zq1jq6xvf2fz-b_dir"
411    )]
412    #[case::simple_dir_with_one_dir_filter_files(
413        r#"builtins.filterSource (p: t: t != "regular") @fixtures/b_dir"#,
414        "/nix/store/8rjx64mm7173xp60rahv7cl3ixfkv3rf-b_dir"
415    )]
416    #[case::simple_dir_with_one_dir_filter_symlinks(
417        r#"builtins.filterSource (p: t: t != "symlink") @fixtures/b_dir"#,
418        "/nix/store/8rjx64mm7173xp60rahv7cl3ixfkv3rf-b_dir"
419    )]
420    #[case::simple_dir_with_one_dir_filter_nothing(
421        r#"builtins.filterSource (p: t: true) @fixtures/b_dir"#,
422        "/nix/store/8rjx64mm7173xp60rahv7cl3ixfkv3rf-b_dir"
423    )]
424    #[case::simple_dir_with_one_dir_filter_everything(
425        r#"builtins.filterSource (p: t: false) @fixtures/b_dir"#,
426        "/nix/store/xzsfzdgrxg93icaamjm8zq1jq6xvf2fz-b_dir"
427    )]
428    #[case::simple_dir_with_one_symlink_to_file_filter_dirs(
429        r#"builtins.filterSource (p: t: t != "directory") @fixtures/c_dir"#,
430        "/nix/store/riigfmmzzrq65zqiffcjk5sbqr9c9h09-c_dir"
431    )]
432    #[case::simple_dir_with_one_symlink_to_file_filter_files(
433        r#"builtins.filterSource (p: t: t != "regular") @fixtures/c_dir"#,
434        "/nix/store/riigfmmzzrq65zqiffcjk5sbqr9c9h09-c_dir"
435    )]
436    #[case::simple_dir_with_one_symlink_to_file_filter_symlinks(
437        r#"builtins.filterSource (p: t: t != "symlink") @fixtures/c_dir"#,
438        "/nix/store/y5g1fz04vzjvf422q92qmv532axj5q26-c_dir"
439    )]
440    #[case::simple_dir_with_one_symlink_to_file_filter_nothing(
441        r#"builtins.filterSource (p: t: true) @fixtures/c_dir"#,
442        "/nix/store/riigfmmzzrq65zqiffcjk5sbqr9c9h09-c_dir"
443    )]
444    #[case::simple_dir_with_one_symlink_to_file_filter_everything(
445        r#"builtins.filterSource (p: t: false) @fixtures/c_dir"#,
446        "/nix/store/y5g1fz04vzjvf422q92qmv532axj5q26-c_dir"
447    )]
448    #[case::simple_dir_with_dangling_symlink_filter_dirs(
449        r#"builtins.filterSource (p: t: t != "directory") @fixtures/d_dir"#,
450        "/nix/store/f2d1aixwiqy4lbzrd040ala2s4m2z199-d_dir"
451    )]
452    #[case::simple_dir_with_dangling_symlink_filter_files(
453        r#"builtins.filterSource (p: t: t != "regular") @fixtures/d_dir"#,
454        "/nix/store/f2d1aixwiqy4lbzrd040ala2s4m2z199-d_dir"
455    )]
456    #[case::simple_dir_with_dangling_symlink_filter_symlinks(
457        r#"builtins.filterSource (p: t: t != "symlink") @fixtures/d_dir"#,
458        "/nix/store/7l371xax8kknhpska4wrmyll1mzlhzvl-d_dir"
459    )]
460    #[case::simple_dir_with_dangling_symlink_filter_nothing(
461        r#"builtins.filterSource (p: t: true) @fixtures/d_dir"#,
462        "/nix/store/f2d1aixwiqy4lbzrd040ala2s4m2z199-d_dir"
463    )]
464    #[case::simple_dir_with_dangling_symlink_filter_everything(
465        r#"builtins.filterSource (p: t: false) @fixtures/d_dir"#,
466        "/nix/store/7l371xax8kknhpska4wrmyll1mzlhzvl-d_dir"
467    )]
468    #[case::simple_symlinked_dir_with_one_file_filter_dirs(
469        r#"builtins.filterSource (p: t: t != "directory") @fixtures/symlink_to_a_dir"#,
470        "/nix/store/apmdprm8fwl2zrjpbyfcd99zrnhvf47q-symlink_to_a_dir"
471    )]
472    #[case::simple_symlinked_dir_with_one_file_filter_files(
473        r#"builtins.filterSource (p: t: t != "regular") @fixtures/symlink_to_a_dir"#,
474        "/nix/store/apmdprm8fwl2zrjpbyfcd99zrnhvf47q-symlink_to_a_dir"
475    )]
476    #[case::simple_symlinked_dir_with_one_file_filter_symlinks(
477        r#"builtins.filterSource (p: t: t != "symlink") @fixtures/symlink_to_a_dir"#,
478        "/nix/store/apmdprm8fwl2zrjpbyfcd99zrnhvf47q-symlink_to_a_dir"
479    )]
480    #[case::simple_symlinked_dir_with_one_file_filter_nothing(
481        r#"builtins.filterSource (p: t: true) @fixtures/symlink_to_a_dir"#,
482        "/nix/store/apmdprm8fwl2zrjpbyfcd99zrnhvf47q-symlink_to_a_dir"
483    )]
484    #[case::simple_symlinked_dir_with_one_file_filter_everything(
485        r#"builtins.filterSource (p: t: false) @fixtures/symlink_to_a_dir"#,
486        "/nix/store/apmdprm8fwl2zrjpbyfcd99zrnhvf47q-symlink_to_a_dir"
487    )]
488    fn builtins_filter_source_succeed(#[case] code: &str, #[case] expected_outpath: &str) {
489        // populate the fixtures dir
490        let temp = TempDir::new().expect("create temporary directory");
491        let p = temp.path().join("import_fixtures");
492
493        // create the fixtures directory.
494        // We produce them at runtime rather than shipping it inside the source
495        // tree, as git can't model certain things - like directories without any
496        // items.
497        {
498            fs::create_dir(&p).expect("creating import_fixtures");
499
500            // `/a_dir` contains an empty `a_file` file
501            fs::create_dir(p.join("a_dir")).expect("creating /a_dir");
502            fs::write(p.join("a_dir").join("a_file"), "").expect("creating /a_dir/a_file");
503
504            // `/a_file` is an empty file
505            fs::write(p.join("a_file"), "").expect("creating /a_file");
506
507            // `/b_dir` contains an empty "a_dir" directory
508            fs::create_dir_all(p.join("b_dir").join("a_dir")).expect("creating /b_dir/a_dir");
509
510            // `/c_dir` contains a `symlink_to_a_file` symlink, pointing to `../a_dir/a_file`.
511            fs::create_dir(p.join("c_dir")).expect("creating /c_dir");
512            std::os::unix::fs::symlink(
513                "../a_dir/a_file",
514                p.join("c_dir").join("symlink_to_a_file"),
515            )
516            .expect("creating /c_dir/symlink_to_a_file");
517
518            // `/d_dir` contains a `dangling_symlink`, pointing to `a_dir/a_file`,
519            // which does not exist.
520            fs::create_dir(p.join("d_dir")).expect("creating /d_dir");
521            std::os::unix::fs::symlink("a_dir/a_file", p.join("d_dir").join("dangling_symlink"))
522                .expect("creating /d_dir/dangling_symlink");
523
524            // `/symlink_to_a_dir` is a symlink to `a_dir`, which exists.
525            std::os::unix::fs::symlink("a_dir", p.join("symlink_to_a_dir"))
526                .expect("creating /symlink_to_a_dir");
527        }
528
529        // replace @fixtures with the temporary path containing the fixtures
530        let code_replaced = code.replace("@fixtures", &p.to_string_lossy());
531
532        let eval_result = eval(&code_replaced);
533
534        let value = eval_result.value.expect("must succeed");
535
536        match value {
537            snix_eval::Value::String(s) => {
538                assert_eq!(expected_outpath, s.as_bstr());
539            }
540            _ => panic!("unexpected value type: {value:?}"),
541        }
542
543        assert!(eval_result.errors.is_empty(), "errors should be empty");
544    }
545
546    /// Space is an illegal character, but if we specify a name without spaces, it's ok.
547    #[rstest]
548    #[case::rename_success(
549        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = true; })"#,
550        true
551    )]
552    #[case::rename_with_spaces_fail(
553        r#"(builtins.path { name = "invalid name"; path = @fixtures + "/te st"; recursive = true; })"#,
554        false
555    )]
556    fn builtins_path_recursive_rename(#[case] code: &str, #[case] success: bool) {
557        // populate the fixtures dir
558        let temp = TempDir::new().expect("create temporary directory");
559        let p = temp.path().join("import_fixtures");
560
561        // create the fixtures directory.
562        // We produce them at runtime rather than shipping it inside the source
563        // tree, as git can't model certain things - like directories without any
564        // items.
565        {
566            fs::create_dir(&p).expect("creating import_fixtures");
567            fs::write(p.join("te st"), "").expect("creating `/te st`");
568        }
569        // replace @fixtures with the temporary path containing the fixtures
570        let code_replaced = code.replace("@fixtures", &p.to_string_lossy());
571
572        let eval_result = eval(&code_replaced);
573
574        let value = eval_result.value;
575
576        if success {
577            match value.expect("expected successful evaluation on legal rename") {
578                snix_eval::Value::String(s) => {
579                    assert_eq!(
580                        "/nix/store/nd5z11x7zjqqz44rkbhc6v7yifdkn659-valid-name",
581                        s.as_bstr()
582                    );
583                }
584                v => panic!("unexpected value type: {v:?}"),
585            }
586        } else {
587            assert!(value.is_none(), "unexpected success on illegal store paths");
588        }
589    }
590
591    /// Space is an illegal character, but if we specify a name without spaces, it's ok.
592    #[rstest]
593    #[case::rename_success(
594        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = false; })"#,
595        true
596    )]
597    #[case::rename_with_spaces_fail(
598        r#"(builtins.path { name = "invalid name"; path = @fixtures + "/te st"; recursive = false; })"#,
599        false
600    )]
601    // The non-recursive variant passes explicitly `recursive = false;`
602    fn builtins_path_nonrecursive_rename(#[case] code: &str, #[case] success: bool) {
603        // populate the fixtures dir
604        let temp = TempDir::new().expect("create temporary directory");
605        let p = temp.path().join("import_fixtures");
606
607        // create the fixtures directory.
608        // We produce them at runtime rather than shipping it inside the source
609        // tree, as git can't model certain things - like directories without any
610        // items.
611        {
612            fs::create_dir(&p).expect("creating import_fixtures");
613            fs::write(p.join("te st"), "").expect("creating `/te st`");
614        }
615        // replace @fixtures with the temporary path containing the fixtures
616        let code_replaced = code.replace("@fixtures", &p.to_string_lossy());
617
618        let eval_result = eval(&code_replaced);
619
620        let value = eval_result.value;
621
622        if success {
623            match value.expect("expected successful evaluation on legal rename") {
624                snix_eval::Value::String(s) => {
625                    assert_eq!(
626                        "/nix/store/il2rmfbqgs37rshr8w7x64hd4d3b4bsa-valid-name",
627                        s.as_bstr()
628                    );
629                }
630                v => panic!("unexpected value type: {v:?}"),
631            }
632        } else {
633            assert!(value.is_none(), "unexpected success on illegal store paths");
634        }
635    }
636
637    #[rstest]
638    #[case::flat_success(
639        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = false; sha256 = "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="; })"#,
640        true
641    )]
642    #[case::flat_fail(
643        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = false; sha256 = "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY="; })"#,
644        false
645    )]
646    #[case::recursive_success(
647        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = true; sha256 = "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY="; })"#,
648        true
649    )]
650    #[case::recursive_fail(
651        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = true; sha256 = "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="; })"#,
652        false
653    )]
654    fn builtins_path_fod_locking(#[case] code: &str, #[case] exp_success: bool) {
655        // populate the fixtures dir
656        let temp = TempDir::new().expect("create temporary directory");
657        let p = temp.path().join("import_fixtures");
658
659        // create the fixtures directory.
660        // We produce them at runtime rather than shipping it inside the source
661        // tree, as git can't model certain things - like directories without any
662        // items.
663        {
664            fs::create_dir(&p).expect("creating import_fixtures");
665            fs::write(p.join("te st"), "").expect("creating `/te st`");
666        }
667        // replace @fixtures with the temporary path containing the fixtures
668        let code_replaced = code.replace("@fixtures", &p.to_string_lossy());
669
670        let eval_result = eval(&code_replaced);
671
672        let value = eval_result.value;
673
674        if exp_success {
675            assert!(
676                value.is_some(),
677                "expected successful evaluation on legal rename and valid FOD sha256"
678            );
679        } else {
680            assert!(value.is_none(), "unexpected success on invalid FOD sha256");
681        }
682    }
683
684    #[rstest]
685    #[case(
686        r#"(builtins.path { name = "valid-path"; path = @fixtures + "/te st dir"; filter = _: _: true; })"#,
687        "/nix/store/i28jmi4fwym4fw3flkrkp2mdxx50pdy0-valid-path"
688    )]
689    #[case(
690        r#"(builtins.path { name = "valid-path"; path = @fixtures + "/te st dir"; filter = _: _: false; })"#,
691        "/nix/store/pwza2ij9gk1fmzhbjnynmfv2mq2sgcap-valid-path"
692    )]
693    fn builtins_path_filter(#[case] code: &str, #[case] expected_outpath: &str) {
694        // populate the fixtures dir
695        let temp = TempDir::new().expect("create temporary directory");
696        let p = temp.path().join("import_fixtures");
697
698        // create the fixtures directory.
699        // We produce them at runtime rather than shipping it inside the source
700        // tree, as git can't model certain things - like directories without any
701        // items.
702        {
703            fs::create_dir(&p).expect("creating import_fixtures");
704            fs::create_dir(p.join("te st dir")).expect("creating `/te st dir`");
705            fs::write(p.join("te st dir").join("test"), "").expect("creating `/te st dir/test`");
706        }
707        // replace @fixtures with the temporary path containing the fixtures
708        let code_replaced = code.replace("@fixtures", &p.to_string_lossy());
709
710        let eval_result = eval(&code_replaced);
711
712        let value = eval_result.value.expect("must succeed");
713
714        match value {
715            snix_eval::Value::String(s) => {
716                assert_eq!(expected_outpath, s.as_bstr());
717            }
718            _ => panic!("unexpected value type: {value:?}"),
719        }
720
721        assert!(eval_result.errors.is_empty(), "errors should be empty");
722    }
723
724    // All tests filter out some unsupported (not representable in castore) nodes, confirming
725    // invalid, but filtered-out nodes don't prevent ingestion of a path.
726    #[rstest]
727    #[cfg(target_family = "unix")]
728    // There is a set of invalid filetypes.
729    // We write various filter functions filtering them out, but usually leaving
730    // some behind.
731    // In case there's still invalid filetypes left after the filtering, we
732    // expect the evaluation to fail.
733    #[case::fail_kept_unknowns(
734        r#"(builtins.filterSource (p: t: t == "unknown") @fixtures)"#,
735        false
736    )]
737    // We filter all invalid filetypes, so the evaluation has to succeed.
738    #[case::succeed_filter_unknowns(
739        r#"(builtins.filterSource (p: t: t != "unknown") @fixtures)"#,
740        true
741    )]
742    #[case::fail_kept_charnode(
743        r#"(builtins.filterSource (p: t: (builtins.baseNameOf p) != "a_charnode") @fixtures)"#,
744        false
745    )]
746    #[case::fail_kept_socket(
747        r#"(builtins.filterSource (p: t: (builtins.baseNameOf p) != "a_socket") @fixtures)"#,
748        false
749    )]
750    #[case::fail_kept_fifo(
751        r#"(builtins.filterSource (p: t: (builtins.baseNameOf p) != "a_fifo") @fixtures)"#,
752        false
753    )]
754    fn builtins_filter_source_unsupported_files(#[case] code: &str, #[case] exp_success: bool) {
755        use nix::errno::Errno;
756        use nix::sys::stat;
757        use nix::unistd;
758        use std::os::unix::net::UnixListener;
759        use tempfile::TempDir;
760
761        // We prepare a directory containing some unsupported file nodes:
762        // - character device
763        // - socket
764        // - FIFO
765        // and we run the evaluation inside that CWD.
766        //
767        // block devices cannot be tested because we don't have the right permissions.
768        let temp = TempDir::with_prefix("foo").expect("Failed to create a temporary directory");
769
770        // read, write, execute to the owner.
771        unistd::mkfifo(&temp.path().join("a_fifo"), stat::Mode::S_IRWXU)
772            .expect("Failed to create the FIFO");
773
774        UnixListener::bind(temp.path().join("a_socket")).expect("Failed to create the socket");
775
776        stat::mknod(
777            &temp.path().join("a_charnode"),
778            stat::SFlag::S_IFCHR,
779            stat::Mode::S_IRWXU,
780            0,
781        )
782        .inspect_err(|e| {
783            if *e == Errno::EPERM {
784                eprintln!(
785                    "\
786Missing permissions to create a character device node with mknod(2).
787Please run this test as root or set CAP_MKNOD."
788                );
789            }
790        })
791        .expect("Failed to create a character device node");
792
793        let code_replaced = code.replace("@fixtures", &temp.path().to_string_lossy());
794        let eval_result = eval(&code_replaced);
795
796        if exp_success {
797            assert!(
798                eval_result.value.is_some(),
799                "unexpected failure on a directory of unsupported file types but all filtered: {:?}",
800                eval_result.errors
801            );
802        } else {
803            assert!(
804                eval_result.value.is_none(),
805                "unexpected success on unsupported file type ingestion: {:?}",
806                eval_result.value
807            );
808        }
809    }
810}