snix/tvix/cli/src/args.rs
Ben Webb 2daa483249 fix(tvix/cli): make -I prepend to lookup paths provided by NIX_PATH
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>
2025-03-03 02:14:08 +00:00

132 lines
4.7 KiB
Rust

use std::path::PathBuf;
use clap::Parser;
use tvix_store::utils::ServiceUrlsMemory;
/// Provides a CLI interface to trigger evaluation using tvix-eval.
///
/// Uses configured tvix-[ca]store and tvix-build components,
/// and by default a set of builtins similar to these present in Nix.
///
/// None of the stores available add to the local `/nix/store` location.
///
/// The CLI interface is not stable and subject to change.
#[derive(Parser, Clone)]
pub struct Args {
/// Path to a script to evaluate
pub script: Option<PathBuf>,
#[clap(long, short = 'E')]
pub expr: Option<String>,
/// Dump the raw AST to stdout before interpreting
#[clap(long, env = "TVIX_DISPLAY_AST")]
pub display_ast: bool,
/// Dump the bytecode to stdout before evaluating
#[clap(long, env = "TVIX_DUMP_BYTECODE")]
pub dump_bytecode: bool,
/// Trace the runtime of the VM
#[clap(long, env = "TVIX_TRACE_RUNTIME")]
pub trace_runtime: bool,
/// Capture the time (relative to the start time of evaluation) of all events traced with
/// `--trace-runtime`
#[clap(long, env = "TVIX_TRACE_RUNTIME_TIMING", requires("trace_runtime"))]
pub trace_runtime_timing: bool,
/// Only compile, but do not execute code. This will make Tvix act
/// sort of like a linter.
#[clap(long)]
pub compile_only: bool,
/// Don't print warnings.
#[clap(long)]
pub no_warnings: bool,
/// Additional entries to the Nix expression search path, a colon-separated list of directories
/// used to resolve `<...>`-style lookup paths.
///
/// This option may be given multiple times. Paths added through -I take precedence over
/// NIX_PATH.
#[clap(long = "extra-nix-path", short = 'I', env = "NIX_PATH", action = clap::ArgAction::Append)]
pub extra_nix_paths: Option<Vec<String>>,
/// Print "raw" (unquoted) output.
#[clap(long)]
pub raw: bool,
/// Strictly evaluate values, traversing them and forcing e.g.
/// elements of lists and attribute sets before printing the
/// return value.
#[clap(long)]
pub strict: bool,
#[clap(flatten)]
pub service_addrs: ServiceUrlsMemory,
#[arg(long, env, default_value = "dummy://")]
pub build_service_addr: String,
/// An optional path in which Derivations encountered during evaluation
/// are dumped into, after evaluation. If it doesn't exist, the directory is created.
///
/// Files dumped there are named like they would show up in `/nix/store`,
/// if produced by Nix. Existing files are not overwritten.
///
/// This is only for debugging and diffing purposes for post-eval inspection;
/// Tvix does not read from these.
#[clap(long)]
pub drv_dumpdir: Option<PathBuf>,
}
impl Args {
pub fn nix_path(&self) -> Option<String> {
resolve_nix_path(std::env::var("NIX_PATH"), &self.extra_nix_paths)
}
}
fn resolve_nix_path(
nix_path: Result<String, std::env::VarError>,
extra_nix_paths: &Option<Vec<String>>,
) -> Option<String> {
let nix_path_option = nix_path.ok().filter(|string| !string.is_empty());
let extra_nix_paths_option = extra_nix_paths.to_owned().map(|vec| vec.join(":"));
match (nix_path_option, extra_nix_paths_option) {
(Some(nix_path), Some(mut extra_nix_paths)) => {
extra_nix_paths.push(':');
Some(extra_nix_paths + &nix_path)
}
(nix_path_option, extra_nix_paths_option) => nix_path_option.or(extra_nix_paths_option),
}
}
#[cfg(test)]
mod tests {
use super::resolve_nix_path;
#[test]
fn test_resolve_nix_path() {
let nix_path = Ok("/nixpath1:nixpath2=/nixpath2".to_owned());
let extra_nix_paths = Some(vec!["/extra1".to_owned(), "extra2=/extra2".to_owned()]);
let expected = Some("/extra1:extra2=/extra2:/nixpath1:nixpath2=/nixpath2".to_owned());
let actual = resolve_nix_path(nix_path, &extra_nix_paths);
assert!(actual == expected);
let nix_path = Err(std::env::VarError::NotPresent);
let extra_nix_paths = Some(vec!["/extra1".to_owned(), "extra2=/extra2".to_owned()]);
let expected = Some("/extra1:extra2=/extra2".to_owned());
let actual = resolve_nix_path(nix_path, &extra_nix_paths);
assert!(actual == expected);
let nix_path = Ok("/nixpath1:nixpath2=/nixpath2".to_owned());
let extra_nix_paths = None;
let expected = Some("/nixpath1:nixpath2=/nixpath2".to_owned());
let actual = resolve_nix_path(nix_path, &extra_nix_paths);
assert!(actual == expected);
let nix_path = Err(std::env::VarError::NotPresent);
let extra_nix_paths = None;
let expected = None;
let actual = resolve_nix_path(nix_path, &extra_nix_paths);
assert!(actual == expected);
}
}