snix_cli/
lib.rs

1use std::path::PathBuf;
2use std::rc::Rc;
3
4use rustc_hash::FxHashMap;
5use smol_str::SmolStr;
6use snix_build::buildservice;
7use snix_eval::{
8    ErrorKind, EvalIO, EvalMode, GlobalsMap, SourceCode, Value,
9    builtins::impure_builtins,
10    observer::{DisassemblingObserver, TracingObserver},
11};
12use snix_glue::{
13    builtins::{add_derivation_builtins, add_fetcher_builtins, add_import_builtins},
14    configure_nix_path,
15    snix_io::SnixIO,
16    snix_store_io::SnixStoreIO,
17};
18use std::fmt::Write;
19use tracing::{Span, info_span};
20use tracing_indicatif::span_ext::IndicatifSpanExt;
21
22pub mod args;
23pub mod assignment;
24pub mod repl;
25
26pub use args::Args;
27pub use repl::Repl;
28
29pub fn init_io_handle(tokio_runtime: &tokio::runtime::Runtime, args: &Args) -> Rc<SnixStoreIO> {
30    let (blob_service, directory_service, path_info_service, nar_calculation_service) =
31        tokio_runtime
32            .block_on(snix_store::utils::construct_services(
33                args.service_addrs.clone(),
34            ))
35            .expect("unable to setup {blob|directory|pathinfo}service before interpreter setup");
36
37    let build_service = tokio_runtime
38        .block_on({
39            let blob_service = blob_service.clone();
40            let directory_service = directory_service.clone();
41            async move {
42                buildservice::from_addr(
43                    &args.build_service_addr,
44                    blob_service.clone(),
45                    directory_service.clone(),
46                )
47                .await
48            }
49        })
50        .expect("unable to setup buildservice before interpreter setup");
51
52    Rc::new(SnixStoreIO::new(
53        blob_service.clone(),
54        directory_service.clone(),
55        path_info_service,
56        nar_calculation_service.into(),
57        build_service.into(),
58        tokio_runtime.handle().clone(),
59        args.hashed_mirrors.clone(),
60    ))
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub enum AllowIncomplete {
65    Allow,
66    #[default]
67    RequireComplete,
68}
69
70impl AllowIncomplete {
71    fn allow(&self) -> bool {
72        matches!(self, Self::Allow)
73    }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct IncompleteInput;
78
79pub struct EvalResult {
80    value: Option<Value>,
81    globals: Rc<GlobalsMap>,
82}
83
84/// Interprets the given code snippet, printing out warnings and errors and returning the result
85#[allow(clippy::too_many_arguments)]
86pub fn evaluate<E: std::io::Write + Clone + Send>(
87    stderr: &mut E,
88    snix_store_io: Rc<SnixStoreIO>,
89    code: &str,
90    path: Option<PathBuf>,
91    args: &Args,
92    allow_incomplete: AllowIncomplete,
93    env: Option<&FxHashMap<SmolStr, Value>>,
94    globals: Option<Rc<GlobalsMap>>,
95    source_map: Option<SourceCode>,
96) -> Result<EvalResult, IncompleteInput> {
97    let mut eval_builder = snix_eval::Evaluation::builder(Box::new(SnixIO::new(
98        snix_store_io.clone() as Rc<dyn EvalIO>,
99    )) as Box<dyn EvalIO>)
100    .enable_import()
101    .env(env);
102
103    if args.strict {
104        eval_builder = eval_builder.mode(EvalMode::Strict);
105    }
106
107    match globals {
108        Some(globals) => {
109            eval_builder = eval_builder.with_globals(globals);
110        }
111        None => {
112            eval_builder = eval_builder.add_builtins(impure_builtins());
113            eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&snix_store_io));
114            eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&snix_store_io));
115            eval_builder = add_import_builtins(eval_builder, Rc::clone(&snix_store_io));
116        }
117    };
118    eval_builder = configure_nix_path(eval_builder, &args.nix_path());
119
120    if let Some(source_map) = source_map {
121        eval_builder = eval_builder.with_source_map(source_map);
122    }
123
124    let source_map = eval_builder.source_map().clone();
125
126    let mut compiler_observer = DisassemblingObserver::new(source_map.clone(), stderr.clone());
127    if args.dump_bytecode {
128        eval_builder.set_compiler_observer(Some(&mut compiler_observer));
129    }
130
131    let mut runtime_observer = TracingObserver::new(stderr.clone());
132    if args.trace_runtime {
133        if args.trace_runtime_timing {
134            runtime_observer.enable_timing()
135        }
136        eval_builder.set_runtime_observer(Some(&mut runtime_observer));
137    }
138
139    let eval = eval_builder.build();
140
141    let span = if !args.trace_runtime && !args.dump_bytecode {
142        info_span!("evaluate", indicatif.pb_show = tracing::field::Empty)
143    } else {
144        info_span!("evaluate", indicatif.pb_hide = tracing::field::Empty)
145    };
146    let (result, globals) = span.in_scope(|| {
147        let span = Span::current();
148
149        span.pb_set_message("Evaluating…");
150        span.pb_start();
151
152        let globals = eval.globals();
153        let result = eval.evaluate(code, path);
154        (result, globals)
155    });
156    drop(span);
157
158    if allow_incomplete.allow()
159        && result.errors.iter().any(|err| {
160            matches!(
161                &err.kind,
162                ErrorKind::ParseErrors(pes)
163                    if pes.iter().any(|pe| matches!(pe, rnix::parser::ParseError::UnexpectedEOF))
164            )
165        })
166    {
167        return Err(IncompleteInput);
168    }
169
170    if args.display_ast {
171        if let Some(ref expr) = result.expr {
172            writeln!(stderr, "AST: {}", snix_eval::pretty_print_expr(expr)).unwrap();
173        }
174    }
175
176    for error in &result.errors {
177        error.fancy_format_write(stderr);
178    }
179
180    if !args.no_warnings {
181        for warning in &result.warnings {
182            warning.fancy_format_write(stderr, &source_map);
183        }
184    }
185
186    if let Some(dumpdir) = &args.drv_dumpdir {
187        // Dump all known derivations files to `dumpdir`.
188        std::fs::create_dir_all(dumpdir).expect("failed to create drv dumpdir");
189        snix_store_io
190            .known_paths
191            .borrow()
192            .get_derivations()
193            // Skip already dumped derivations.
194            .filter(|(drv_path, _)| !dumpdir.join(drv_path.to_string()).exists())
195            .for_each(|(drv_path, drv)| {
196                std::fs::write(dumpdir.join(drv_path.to_string()), drv.to_aterm_bytes())
197                    .expect("failed to write drv to dumpdir");
198            })
199    }
200
201    Ok(EvalResult {
202        globals,
203        value: result.value,
204    })
205}
206
207pub struct InterpretResult {
208    output: String,
209    success: bool,
210    pub(crate) globals: Option<Rc<GlobalsMap>>,
211}
212
213impl InterpretResult {
214    pub fn empty_success(globals: Option<Rc<GlobalsMap>>) -> Self {
215        Self {
216            output: String::new(),
217            success: true,
218            globals,
219        }
220    }
221
222    pub fn finalize<E: std::io::Write>(self, stderr: &mut E) -> bool {
223        write!(stderr, "{}", self.output).unwrap();
224        self.success
225    }
226
227    pub fn output(&self) -> &str {
228        &self.output
229    }
230
231    pub fn success(&self) -> bool {
232        self.success
233    }
234}
235
236/// Interprets the given code snippet, printing out warnings, errors
237/// and the result itself. The return value indicates whether
238/// evaluation succeeded.
239#[allow(clippy::too_many_arguments)]
240pub fn interpret<E: std::io::Write + Clone + Send>(
241    stderr: &mut E,
242    snix_store_io: Rc<SnixStoreIO>,
243    code: &str,
244    path: Option<PathBuf>,
245    args: &Args,
246    explain: bool,
247    allow_incomplete: AllowIncomplete,
248    env: Option<&FxHashMap<SmolStr, Value>>,
249    globals: Option<Rc<GlobalsMap>>,
250    source_map: Option<SourceCode>,
251) -> Result<InterpretResult, IncompleteInput> {
252    let mut output = String::new();
253    let result = evaluate(
254        stderr,
255        snix_store_io,
256        code,
257        path,
258        args,
259        allow_incomplete,
260        env,
261        globals,
262        source_map,
263    )?;
264
265    if let Some(value) = result.value.as_ref() {
266        if explain {
267            writeln!(&mut output, "=> {}", value.explain()).unwrap();
268        } else if args.raw {
269            writeln!(&mut output, "{value}").unwrap();
270        } else {
271            writeln!(&mut output, "=> {} :: {}", value, value.type_of()).unwrap();
272        }
273    }
274
275    // inform the caller about any errors
276    Ok(InterpretResult {
277        output,
278        success: result.value.is_some(),
279        globals: Some(result.globals),
280    })
281}