Update the tvix cli's -I option so that it aligns more closely with
nix's behavior: prepending entries to the list of lookup paths provided
by the NIX_PATH environment variable. Before this commit, using the -I
option would instead override and ignore the NIX_PATH variable.
Additionally, update the option's long name and help text to match the
new behavior.
While the tvix cli's interface does not appear to be attempting to mimic
nix exactly, I think this particular case of the -I option's diverging
behavior will inevitably surprise users because it's name, presumably
short for "include" and being similar to gcc's flag, evokes additivity.
The prior implementation hinted at this difference with the help text
and the long name, --nix-search-path, but I still suspect users will be
confused on first usage (at least I was). If we're willing to pay the
maintenance costs of additional code, we can avoid this and provide a
slightly smoother user experience.
Changes were tested by buiding the tvix cli, adding it to the PATH, and
executing simple tests as in the following bash script
mg build //tvix/cli
PATH="$PWD/result/bin:$PATH"
one=$(mktemp) && echo "=> $one :: path" > "$one"
two=$(mktemp) && echo "=> $two :: path" > "$two"
dir1=$(mktemp -d) && file1="$dir1/file1" && echo "=> $file1 :: path" > "$file1"
dir2=$(mktemp -d) && file2="$dir2/file2" && echo "=> $file2 :: path" > "$file2"
# NIX_PATH works with a single non-prefixed lookup path.
NIX_PATH="$dir1" tvix -E "<file1>" | cmp - "$file1"
# NIX_PATH works with multiple non-prefixed lookup paths.
NIX_PATH="$dir1:$dir2" tvix -E "<file2>" | cmp - "$file2"
# NIX_PATH works with a single prefixed lookup path.
NIX_PATH="one=$one" tvix -E "<one>" | cmp - "$one"
# NIX_PATH works with multiple prefixed lookup paths.
NIX_PATH="one=$one:two=$two" tvix -E "<one>" | cmp - "$one"
NIX_PATH="one=$one:two=$two" tvix -E "<two>" | cmp - "$two"
# NIX_PATH first entry takes precedence.
NIX_PATH="one=$one:one=$two" tvix -E "<one>" | cmp - "$one"
# The -I option works with a single non-prefixed lookup path.
tvix -I "$dir1" -E "<file1>" | cmp - "$file1"
# The -I option works with multiple non-prefixed lookup paths.
tvix -I "$dir1" -I "$dir2" -E "<file2>" | cmp - "$file2"
# The -I option works with a single prefixed lookup path.
tvix -I "one=$one" -E "<one>" | cmp - "$one"
# The --extra-nix-path option works with a single prefixed lookup path.
tvix --extra-nix-path "one=$one" -E "<one>" | cmp - "$one"
# The -I options works when passed multiple times with prefixed lookup paths.
tvix -I "one=$one" -I "two=$two" -E "<one>" | cmp - "$one"
tvix -I "one=$one" -I "two=$two" -E "<two>" | cmp - "$two"
# The first -I option takes precedence.
tvix -I "one=$one" -I "one=$two" -E "<one>" | cmp - "$one"
# Both NIX_PATH and the -I option work together and are additive.
NIX_PATH="one=$one" tvix -I "two=$two" -E "<one>" | cmp - "$one"
NIX_PATH="one=$one" tvix -I "two=$two" -E "<two>" | cmp - "$two"
# The -I option takes precedence over NIX_PATH.
NIX_PATH="one=$one" tvix -I "one=$two" -E "<one>" | cmp - "$two"
rm "$one"
rm "$two"
rm "$file1" && rmdir "$dir1"
rm "$file2" && rmdir "$dir2"
The above script assumes it's being run from inside the depot.
Change-Id: I153e6de57939c0eeca1f9e479d807862ab69b2de
Reviewed-on: https://cl.tvl.fyi/c/depot/+/13189
Tested-by: BuildkiteCI
Reviewed-by: flokli <flokli@flokli.de>
273 lines
7.8 KiB
Rust
273 lines
7.8 KiB
Rust
use std::path::PathBuf;
|
|
use std::rc::Rc;
|
|
|
|
use rustc_hash::FxHashMap;
|
|
use smol_str::SmolStr;
|
|
use std::fmt::Write;
|
|
use tracing::{instrument, Span};
|
|
use tracing_indicatif::span_ext::IndicatifSpanExt;
|
|
use tvix_build::buildservice;
|
|
use tvix_eval::{
|
|
builtins::impure_builtins,
|
|
observer::{DisassemblingObserver, TracingObserver},
|
|
ErrorKind, EvalIO, EvalMode, GlobalsMap, SourceCode, Value,
|
|
};
|
|
use tvix_glue::{
|
|
builtins::{add_derivation_builtins, add_fetcher_builtins, add_import_builtins},
|
|
configure_nix_path,
|
|
tvix_io::TvixIO,
|
|
tvix_store_io::TvixStoreIO,
|
|
};
|
|
|
|
pub mod args;
|
|
pub mod assignment;
|
|
pub mod repl;
|
|
|
|
pub use args::Args;
|
|
pub use repl::Repl;
|
|
|
|
pub fn init_io_handle(tokio_runtime: &tokio::runtime::Runtime, args: &Args) -> Rc<TvixStoreIO> {
|
|
let (blob_service, directory_service, path_info_service, nar_calculation_service) =
|
|
tokio_runtime
|
|
.block_on(tvix_store::utils::construct_services(
|
|
args.service_addrs.clone(),
|
|
))
|
|
.expect("unable to setup {blob|directory|pathinfo}service before interpreter setup");
|
|
|
|
let build_service = tokio_runtime
|
|
.block_on({
|
|
let blob_service = blob_service.clone();
|
|
let directory_service = directory_service.clone();
|
|
async move {
|
|
buildservice::from_addr(
|
|
&args.build_service_addr,
|
|
blob_service.clone(),
|
|
directory_service.clone(),
|
|
)
|
|
.await
|
|
}
|
|
})
|
|
.expect("unable to setup buildservice before interpreter setup");
|
|
|
|
Rc::new(TvixStoreIO::new(
|
|
blob_service.clone(),
|
|
directory_service.clone(),
|
|
path_info_service,
|
|
nar_calculation_service.into(),
|
|
build_service.into(),
|
|
tokio_runtime.handle().clone(),
|
|
))
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub enum AllowIncomplete {
|
|
Allow,
|
|
#[default]
|
|
RequireComplete,
|
|
}
|
|
|
|
impl AllowIncomplete {
|
|
fn allow(&self) -> bool {
|
|
matches!(self, Self::Allow)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct IncompleteInput;
|
|
|
|
pub struct EvalResult {
|
|
value: Option<Value>,
|
|
globals: Rc<GlobalsMap>,
|
|
}
|
|
|
|
/// Interprets the given code snippet, printing out warnings and errors and returning the result
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn evaluate(
|
|
tvix_store_io: Rc<TvixStoreIO>,
|
|
code: &str,
|
|
path: Option<PathBuf>,
|
|
args: &Args,
|
|
allow_incomplete: AllowIncomplete,
|
|
env: Option<&FxHashMap<SmolStr, Value>>,
|
|
globals: Option<Rc<GlobalsMap>>,
|
|
source_map: Option<SourceCode>,
|
|
) -> Result<EvalResult, IncompleteInput> {
|
|
let span = Span::current();
|
|
span.pb_start();
|
|
span.pb_set_style(&tvix_tracing::PB_SPINNER_STYLE);
|
|
span.pb_set_message("Setting up evaluator…");
|
|
|
|
let mut eval_builder = tvix_eval::Evaluation::builder(Box::new(TvixIO::new(
|
|
tvix_store_io.clone() as Rc<dyn EvalIO>,
|
|
)) as Box<dyn EvalIO>)
|
|
.enable_import()
|
|
.env(env);
|
|
|
|
if args.strict {
|
|
eval_builder = eval_builder.mode(EvalMode::Strict);
|
|
}
|
|
|
|
match globals {
|
|
Some(globals) => {
|
|
eval_builder = eval_builder.with_globals(globals);
|
|
}
|
|
None => {
|
|
eval_builder = eval_builder.add_builtins(impure_builtins());
|
|
eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&tvix_store_io));
|
|
eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&tvix_store_io));
|
|
eval_builder = add_import_builtins(eval_builder, Rc::clone(&tvix_store_io));
|
|
}
|
|
};
|
|
eval_builder = configure_nix_path(eval_builder, &args.nix_path());
|
|
|
|
if let Some(source_map) = source_map {
|
|
eval_builder = eval_builder.with_source_map(source_map);
|
|
}
|
|
|
|
let source_map = eval_builder.source_map().clone();
|
|
let (result, globals) = {
|
|
let mut compiler_observer =
|
|
DisassemblingObserver::new(source_map.clone(), std::io::stderr());
|
|
if args.dump_bytecode {
|
|
eval_builder.set_compiler_observer(Some(&mut compiler_observer));
|
|
}
|
|
|
|
let mut runtime_observer = TracingObserver::new(std::io::stderr());
|
|
if args.trace_runtime {
|
|
if args.trace_runtime_timing {
|
|
runtime_observer.enable_timing()
|
|
}
|
|
eval_builder.set_runtime_observer(Some(&mut runtime_observer));
|
|
}
|
|
|
|
span.pb_set_message("Evaluating…");
|
|
|
|
let eval = eval_builder.build();
|
|
let globals = eval.globals();
|
|
let result = eval.evaluate(code, path);
|
|
(result, globals)
|
|
};
|
|
|
|
if allow_incomplete.allow()
|
|
&& result.errors.iter().any(|err| {
|
|
matches!(
|
|
&err.kind,
|
|
ErrorKind::ParseErrors(pes)
|
|
if pes.iter().any(|pe| matches!(pe, rnix::parser::ParseError::UnexpectedEOF))
|
|
)
|
|
})
|
|
{
|
|
return Err(IncompleteInput);
|
|
}
|
|
|
|
if args.display_ast {
|
|
if let Some(ref expr) = result.expr {
|
|
eprintln!("AST: {}", tvix_eval::pretty_print_expr(expr));
|
|
}
|
|
}
|
|
|
|
for error in &result.errors {
|
|
error.fancy_format_stderr();
|
|
}
|
|
|
|
if !args.no_warnings {
|
|
for warning in &result.warnings {
|
|
warning.fancy_format_stderr(&source_map);
|
|
}
|
|
}
|
|
|
|
if let Some(dumpdir) = &args.drv_dumpdir {
|
|
// Dump all known derivations files to `dumpdir`.
|
|
std::fs::create_dir_all(dumpdir).expect("failed to create drv dumpdir");
|
|
tvix_store_io
|
|
.known_paths
|
|
.borrow()
|
|
.get_derivations()
|
|
// Skip already dumped derivations.
|
|
.filter(|(drv_path, _)| !dumpdir.join(drv_path.to_string()).exists())
|
|
.for_each(|(drv_path, drv)| {
|
|
std::fs::write(dumpdir.join(drv_path.to_string()), drv.to_aterm_bytes())
|
|
.expect("failed to write drv to dumpdir");
|
|
})
|
|
}
|
|
|
|
Ok(EvalResult {
|
|
globals,
|
|
value: result.value,
|
|
})
|
|
}
|
|
|
|
pub struct InterpretResult {
|
|
output: String,
|
|
success: bool,
|
|
pub(crate) globals: Option<Rc<GlobalsMap>>,
|
|
}
|
|
|
|
impl InterpretResult {
|
|
pub fn empty_success(globals: Option<Rc<GlobalsMap>>) -> Self {
|
|
Self {
|
|
output: String::new(),
|
|
success: true,
|
|
globals,
|
|
}
|
|
}
|
|
|
|
pub fn finalize(self) -> bool {
|
|
print!("{}", self.output);
|
|
self.success
|
|
}
|
|
|
|
pub fn output(&self) -> &str {
|
|
&self.output
|
|
}
|
|
|
|
pub fn success(&self) -> bool {
|
|
self.success
|
|
}
|
|
}
|
|
|
|
/// Interprets the given code snippet, printing out warnings, errors
|
|
/// and the result itself. The return value indicates whether
|
|
/// evaluation succeeded.
|
|
#[instrument(skip_all, fields(indicatif.pb_show=tracing::field::Empty))]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn interpret(
|
|
tvix_store_io: Rc<TvixStoreIO>,
|
|
code: &str,
|
|
path: Option<PathBuf>,
|
|
args: &Args,
|
|
explain: bool,
|
|
allow_incomplete: AllowIncomplete,
|
|
env: Option<&FxHashMap<SmolStr, Value>>,
|
|
globals: Option<Rc<GlobalsMap>>,
|
|
source_map: Option<SourceCode>,
|
|
) -> Result<InterpretResult, IncompleteInput> {
|
|
let mut output = String::new();
|
|
let result = evaluate(
|
|
tvix_store_io,
|
|
code,
|
|
path,
|
|
args,
|
|
allow_incomplete,
|
|
env,
|
|
globals,
|
|
source_map,
|
|
)?;
|
|
|
|
if let Some(value) = result.value.as_ref() {
|
|
if explain {
|
|
writeln!(&mut output, "=> {}", value.explain()).unwrap();
|
|
} else if args.raw {
|
|
writeln!(&mut output, "{}", value.to_contextful_str().unwrap()).unwrap();
|
|
} else {
|
|
writeln!(&mut output, "=> {} :: {}", value, value.type_of()).unwrap();
|
|
}
|
|
}
|
|
|
|
// inform the caller about any errors
|
|
Ok(InterpretResult {
|
|
output,
|
|
success: result.value.is_some(),
|
|
globals: Some(result.globals),
|
|
})
|
|
}
|