tvix_glue/builtins/
mod.rs

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