text:
use itertools::Itertools;
use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::fs::{read, read_dir, read_link, symlink_metadata};
use std::io::ErrorKind;
use std::os::unix::ffi::OsStrExt;

/// Output of trace-nix.so, sorted and deduplicated.
pub struct Trace {
    items: BTreeMap<Vec<u8>, Vec<u8>>,
}

impl Trace {
    pub fn load(vec: Vec<u8>) -> Trace {
        let items = vec
            .split(|&b| b == 0)
            .filter(|&fname| !fname.is_empty()) // last entry has trailing NUL
            .map(Vec::from)
            .tuples::<(_, _)>()
            .collect::<BTreeMap<Vec<u8>, Vec<u8>>>();
        Trace { items }
    }

    pub fn serialize(&self) -> Vec<u8> {
        let mut result = Vec::<u8>::new();
        for (a, b) in &self.items {
            result.push(0);
            result.extend(a);
            result.push(0);
            result.extend(b);
        }
        result
    }

    /// Return true if trace doesn't match (i.e. some file is changed)
    pub fn check_for_changes(&self) -> bool {
        for (k, v) in &self.items {
            if check_item_updated(k, v) {
                return true;
            }
        }
        false
    }
}

fn check_item_updated(k: &[u8], v: &[u8]) -> bool {
    let tmp: OsString;
    let fname = OsStr::from_bytes(&k[1..]);
    let res = match k.iter().next() {
        Some(b's') => match symlink_metadata(fname) {
            Err(_) => OsStr::new("-"),
            Ok(md) => {
                if md.file_type().is_symlink() {
                    let mut l = OsString::from("l");
                    l.push(read_link(fname).expect("Can't read link"));
                    tmp = l;
                    tmp.as_os_str()
                } else if md.file_type().is_dir() {
                    OsStr::new("d")
                } else {
                    OsStr::new("+")
                }
            }
        },
        Some(b'f') => match read(fname) {
            Ok(data) => {
                tmp = OsString::from(
                    &blake3::hash(&data).to_hex().as_str()[..32],
                );
                tmp.as_os_str()
            }
            Err(ref e) if e.kind() == ErrorKind::NotFound => OsStr::new("-"),
            Err(_) => OsStr::new("e"),
        },
        Some(b'd') => {
            tmp = hash_dir(fname);
            tmp.as_os_str()
        }
        _ => panic!("Unexpected"),
    };

    if res.as_bytes() != v {
        eprintln!(
            "cached-nix-shell: {:?}: expected {:?}, got {:?}",
            fname,
            OsStr::from_bytes(v),
            res
        );
        return true;
    }
    false
}

fn hash_dir(fname: &OsStr) -> OsString {
    let entries = match read_dir(fname) {
        Ok(x) => x,
        Err(_) => return OsString::from("-"),
    };

    let mut hasher = blake3::Hasher::new();
    entries
        .filter_map(|entry| {
            let entry = entry.ok()?;
            let typ = match entry.file_type() {
                Ok(typ) => {
                    if typ.is_symlink() {
                        b'l'
                    } else if typ.is_file() {
                        b'f'
                    } else if typ.is_dir() {
                        b'd'
                    } else {
                        b'u'
                    }
                }
                Err(_) => return None,
            };
            Some([entry.file_name().as_bytes(), &[b'=', typ, 0]].concat())
        })
        .sorted()
        .for_each(|entry| {
            hasher.update(&entry);
        });
    OsString::from(&hasher.finalize().to_hex().as_str()[..32])
}


text:
use crate::args::Args;
use crate::bash::is_literal_bash_string;
use crate::path_clean::PathClean;
use crate::trace::Trace;
use itertools::{chain, Itertools};
use nix::unistd::{access, AccessFlags};
use once_cell::sync::Lazy;
use std::collections::{BTreeMap, HashSet};
use std::env::current_dir;
use std::ffi::{OsStr, OsString};
use std::fs::{read, read_link, File};
use std::io::{Read, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::prelude::OsStringExt;
use std::os::unix::process::CommandExt;
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use std::process::{exit, Command, Stdio};
use std::time::Instant;
use tempfile::NamedTempFile;
use ufcs::Pipe;

mod args;
mod bash;
mod nix_path;
mod path_clean;
mod shebang;
mod trace;

type EnvMap = BTreeMap<OsString, OsString>;

struct EnvOptions {
    env: EnvMap,
    bashopts: OsString,
    shellopts: OsString,
}

static XDG_DIRS: Lazy<xdg::BaseDirectories> = Lazy::new(|| {
    xdg::BaseDirectories::with_prefix("cached-nix-shell")
        .expect("Can't get find base cache directory")
});

/// Serialize environment variables in the same way as `env -0` does.
fn serialize_env(env: &EnvMap) -> Vec<u8> {
    let mut vec = Vec::new();
    for (k, v) in env {
        vec.extend(k.as_bytes());
        vec.push(b'=');
        vec.extend(v.as_bytes());
        vec.push(0);
    }
    vec
}

/// Deserealize environment variables from `env -0` format.
fn deserealize_env(vec: Vec<u8>) -> EnvMap {
    vec.split(|&b| b == 0)
        .filter(|&var| !var.is_empty()) // last entry has trailing NUL
        .map(|var| {
            let pos = var.iter().position(|&x| x == b'=').unwrap();
            (
                OsStr::from_bytes(&var[0..pos]).to_owned(),
                OsStr::from_bytes(&var[pos + 1..]).to_owned(),
            )
        })
        .collect::<BTreeMap<_, _>>()
}

fn serialize_args(args: &[OsString]) -> Vec<u8> {
    let mut vec = Vec::new();
    for arg in args {
        vec.extend(arg.as_bytes());
        vec.push(0);
    }
    vec
}

fn serialize_vecs(vecs: &[&[u8]]) -> Vec<u8> {
    let mut vec = Vec::new();
    for v in vecs {
        vec.extend(format!("{}\0", v.len()).as_str().as_bytes());
        vec.extend(v.iter());
    }
    vec
}

fn unwrap_or_errx<T>(x: Result<T, String>) -> T {
    match x {
        Ok(x) => x,
        Err(x) => {
            eprintln!("cached-nix-shell: {x}");
            exit(1)
        }
    }
}

struct NixShellInput {
    pwd: PathBuf,
    env: EnvMap,
    args: Vec<OsString>,
    weak_args: Vec<OsString>,
}

struct NixShellOutput {
    env: EnvMap,
    trace: trace::Trace,
    drv: String,
}

fn minimal_essential_path() -> OsString {
    let required_binaries = ["tar", "gzip", "git", "nix-shell", "rm"];

    fn which_dir(binary: &&str) -> Option<PathBuf> {
        std::env::var_os("PATH")
            .as_ref()
            .unwrap()
            .pipe(std::env::split_paths)
            .find(|dir| {
                if access(&dir.join(binary), AccessFlags::X_OK).is_err() {
                    return false;
                }

                if binary == &"nix-shell" {
                    // Ignore our fake nix-shell.
                    return !dir
                        .join(binary)
                        .canonicalize()
                        .ok()
                        .and_then(|x| x.file_name().map(|x| x.to_os_string()))
                        .map(|x| x == "cached-nix-shell")
                        .unwrap_or(true);
                }

                true
            })
    }

    let required_paths = required_binaries
        .iter()
        .filter_map(which_dir)
        .collect::<HashSet<PathBuf>>();

    // We can't just join_paths(required_paths) -- we need to preserve order
    std::env::var_os("PATH")
        .as_ref()
        .unwrap()
        .pipe(std::env::split_paths)
        .filter(|path_item| required_paths.contains(path_item))
        .unique()
        .pipe(std::env::join_paths)
        .unwrap()
}

fn absolute_dirname(script_fname: &OsStr) -> PathBuf {
    Path::new(&script_fname)
        .parent()
        .expect("Can't get script dirname")
        .pipe(absolute)
}

fn absolute(path: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        // We do not use PathBuf::canonicalize() here since we do not want
        // symlink resolving.
        current_dir().expect("Can't get PWD").join(path).clean()
    }
}

fn args_to_inp(pwd: PathBuf, x: &Args) -> NixShellInput {
    let mut args = Vec::new();

    args.push(OsString::from("--pure"));

    let env = {
        let mut clean_env = BTreeMap::new();
        // Env vars to pass to `nix-shell --pure`. Changes to these variables
        // would invalidate the cache.
        let whitelist = &[
            "HOME",
            "NIX_PATH",
            // tmp dir
            "TMPDIR",
            "XDG_RUNTIME_DIR",
            // ssl-related
            "CURL_CA_BUNDLE",
            "GIT_SSL_CAINFO",
            "NIX_SSL_CERT_FILE",
            "SSL_CERT_FILE",
            // Necessary if nix build caches are accessed via a proxy
            "http_proxy",
            "https_proxy",
            "ftp_proxy",
            "all_proxy",
            "no_proxy",
        ];
        for var in whitelist {
            if let Some(val) = std::env::var_os(var) {
                clean_env.insert(OsString::from(var), val);
            }
        }
        for var in x.keep.iter() {
            if let Some(val) = std::env::var_os(var) {
                clean_env.insert(var.clone(), val);
                args.push("--keep".into());
                args.push(var.clone());
            }
        }
        clean_env.insert(OsString::from("PATH"), minimal_essential_path());
        clean_env
    };

    args.extend(x.other_kw.clone());
    args.push(OsString::from("--"));
    args.extend(x.rest.clone());

    NixShellInput {
        pwd,
        env,
        args,
        weak_args: x.weak_kw.clone(),
    }
}

fn run_nix_shell(inp: &NixShellInput) -> NixShellOutput {
    let trace_file = NamedTempFile::new().expect("can't create temporary file");

    let env_file = NamedTempFile::new().expect("can't create temporary file");
    let env_cmd = [
        b"{ printf \"BASHOPTS=%s\\0SHELLOPTS=%s\\0\" \"${BASHOPTS-}\" \"${SHELLOPTS-}\" ; env -0; } >",
        bash::quote(env_file.path().as_os_str().as_bytes()).as_slice(),
    ]
    .concat();

    let env = {
        let status = Command::new(concat!(env!("CNS_NIX"), "nix-shell"))
            .arg("--run")
            .arg(OsStr::from_bytes(&env_cmd))
            .args(&inp.weak_args)
            .args(&inp.args)
            .stderr(std::process::Stdio::inherit())
            .current_dir(&inp.pwd)
            .env_clear()
            .envs(&inp.env)
            .env("LD_PRELOAD", env!("CNS_TRACE_NIX_SO"))
            .env("DYLD_INSERT_LIBRARIES", env!("CNS_TRACE_NIX_SO"))
            .env("TRACE_NIX", trace_file.path())
            .stdin(Stdio::null())
            .status()
            .expect("failed to execute nix-shell");
        if !status.success() {
            eprintln!("cached-nix-shell: nix-shell: {status}");
            let code = status
                .code()
                .or_else(|| status.signal().map(|x| x + 127))
                .unwrap_or(255);
            exit(code);
        }
        let mut env = read(env_file.path())
            .expect("can't read an environment file")
            .pipe(deserealize_env);
        // Drop session variables exported by bash
        env.remove(OsStr::new("OLDPWD"));
        env.remove(OsStr::new("PWD"));
        env.remove(OsStr::new("SHLVL"));
        env.remove(OsStr::new("_"));
        env
    };

    let env_out = env
        .get(OsStr::new("out"))
        .expect("expected to have `out` environment variable");

    let mut trace_file =
        trace_file.reopen().expect("can't reopen temporary file");
    let mut trace_data = Vec::new();
    trace_file
        .read_to_end(&mut trace_data)
        .expect("Can't read trace file");
    let trace = Trace::load(trace_data);
    if trace.check_for_changes() {
        eprintln!("cached-nix-shell: some files are already updated, cache won't be reused");
    }
    std::mem::drop(trace_file);

    let drv: String = {
        // nix 2.3
        let mut exec = Command::new(concat!(env!("CNS_NIX"), "nix"))
            .arg("show-derivation")
            .arg(env_out)
            .output()
            .expect("failed to execute nix show-derivation");
        let mut stderr = exec.stderr.clone();
        if !exec.status.success() {
            // nix 2.4
            exec = Command::new(concat!(env!("CNS_NIX"), "nix"))
                .arg("show-derivation")
                .arg("--extra-experimental-features")
                .arg("nix-command")
                .arg(env_out)
                .output()
                .expect("failed to execute nix show-derivation");
            stderr.extend(b"\n");
            stderr.extend(exec.stderr);
        }
        if !exec.status.success() {
            eprintln!(
                "cached-nix-shell: failed to execute nix show-derivation"
            );
            let _ = std::io::stderr().write_all(&stderr);
            exit(1);
        }

        // Path to .drv file is always in ASCII, so no information is lost.
        let output = String::from_utf8_lossy(&exec.stdout);
        let output: serde_json::Value =
            serde_json::from_str(&output).expect("failed to parse json");
        // The first key of the toplevel object contains the path to .drv file.
        let (drv, _) = output.as_object().unwrap().into_iter().next().unwrap();
        drv.clone()
    };

    NixShellOutput { env, trace, drv }
}

fn run_script(
    fname: OsString,
    nix_shell_args: Vec<OsString>,
    script_args: Vec<OsString>,
) {
    let nix_shell_args = Args::parse(nix_shell_args, true).pipe(unwrap_or_errx);
    let inp = args_to_inp(absolute_dirname(&fname), &nix_shell_args);
    let env = cached_shell_env(nix_shell_args.pure, &inp);

    let exec = if is_literal_bash_string(nix_shell_args.interpreter.as_bytes())
    {
        // eprintln!("Interpreter is a literal string, executing directly");
        Command::new(nix_shell_args.interpreter)
            .arg(fname)
            .args(script_args)
            .env_clear()
            .envs(&env.env)
            .exec()
    } else {
        // eprintln!("Interpreter is bash command, executing 'bash -c'");
        let mut exec_string = OsString::new();
        exec_string.push("exec ");
        exec_string.push(nix_shell_args.interpreter);
        exec_string.push(r#" "$@""#);
        Command::new("bash")
            .arg("-c")
            .arg(exec_string)
            .arg("cached-nix-shell-bash") // corresponds to "$0" inside '-i'
            .arg(fname)
            .args(script_args)
            .env_clear()
            .envs(&env.env)
            .exec()
    };

    eprintln!("cached-nix-shell: couldn't run: {exec:?}");
    exit(1);
}

fn run_from_args(args: Vec<OsString>) {
    let mut args = Args::parse(args, false).pipe(unwrap_or_errx);

    // Normalize PWD.
    // References:
    //   https://github.com/NixOS/nix/blob/2.3.10/src/libexpr/common-eval-args.cc#L46-L57
    //   https://github.com/NixOS/nix/blob/2.3.10/src/nix-build/nix-build.cc#L279-L291
    let nix_shell_pwd = if nix_path::contains_relative_paths(&args) {
        // in:  nix-shell -I . ""
        // out: cd $PWD; nix-shell -I . ""
        current_dir().expect("Can't get PWD")
    } else if args.packages_or_expr {
        // in:  nix-shel -p ...
        // out: cd /var/empty; nix-shell -p ...
        PathBuf::from(env!("CNS_VAR_EMPTY"))
    } else if let [arg] = &mut args.rest[..] {
        if arg == "" {
            // in:  nix-shell ""
            // out: cd $PWD; nix-shell ""
            // nix-shell "" will use ./default.nix
            current_dir().expect("Can't get PWD")
        } else if arg.as_bytes().starts_with(b"<")
            && arg.as_bytes().ends_with(b">")
            || nix_path::is_uri(arg.as_bytes())
        {
            // in:  nix-shell '<foo>'
            // out: cd /var/empty; nix-shell '<foo>'
            // in:  nix-shell http://...
            // out: cd /var/empty; nix-shell http://...
            PathBuf::from(env!("CNS_VAR_EMPTY"))
        } else if arg.as_bytes().ends_with(b"/") || Path::new(arg).is_dir() {
            // in:  nix-shell /path/to/dir
            // out: cd /path/to/dir; nix-shell .
            let pwd = absolute(Path::new(arg));
            *arg = OsString::from(".");
            pwd
        } else {
            // in:  nix-shell /path/to/file
            // out: cd /path/to; nix-shell ./file
            let pwd = absolute_dirname(arg);
            *arg = PathBuf::from(&arg)
                .components()
                .next_back()
                .unwrap()
                .pipe(|x| PathBuf::from(".").join(x))
                .into_os_string();
            pwd
        }
    } else {
        // in:  nix-shell
        // out: cd $PWD; nix-shell
        // nix-shell will use ./shell.nix or ./default.nix
        // in:  nix-shell foo.nix bar.nix ...
        current_dir().expect("Can't get PWD")
    };

    let inp = args_to_inp(nix_shell_pwd, &args);
    let env = cached_shell_env(args.pure, &inp);

    let (cmd, cmd_args) = match args.run {
        args::RunMode::InteractiveShell => {
            let mut args = vec!["--rcfile".into(), env!("CNS_RCFILE").into()];
            args.append(build_bash_options(&env).as_mut());
            ("bash".into(), args)
        }
        args::RunMode::Shell(cmd) => {
            let mut args = build_bash_options(&env);
            args.extend_from_slice(&["-c".into(), cmd]);
            ("bash".into(), args)
        }
        args::RunMode::Exec(cmd, cmd_args) => (cmd, cmd_args),
    };

    let exec = Command::new(cmd)
        .args(cmd_args)
        .env_clear()
        .envs(&env.env)
        .exec();
    eprintln!("cached-nix-shell: couldn't run: {exec:?}");
    exit(1);
}

fn cached_shell_env(pure: bool, inp: &NixShellInput) -> EnvOptions {
    let inputs = serialize_vecs(&[
        &serialize_env(&inp.env),
        &serialize_args(&inp.args),
        inp.pwd.as_os_str().as_bytes(),
    ]);

    let inputs_hash = blake3::hash(&inputs).to_hex().as_str().to_string();

    let mut env = if let Some(env) = check_cache(&inputs_hash) {
        env
    } else {
        eprintln!("cached-nix-shell: updating cache");
        let start = Instant::now();
        let outp = run_nix_shell(inp);
        eprintln!("cached-nix-shell: done in {:?}", start.elapsed());

        // TODO: use flock
        cache_write(&inputs_hash, "inputs", &inputs);
        cache_write(&inputs_hash, "env", &serialize_env(&outp.env));
        cache_write(&inputs_hash, "trace", &outp.trace.serialize());
        cache_symlink(&inputs_hash, "drv", &outp.drv);

        outp.env
    };

    let shellopts = env.remove(OsStr::new("SHELLOPTS")).unwrap_or_default();
    let bashopts = env.remove(OsStr::new("BASHOPTS")).unwrap_or_default();
    env.insert(OsString::from("IN_CACHED_NIX_SHELL"), OsString::from("1"));

    EnvOptions {
        env: merge_env(if pure { env } else { merge_impure_env(env) }),
        shellopts,
        bashopts,
    }
}

// Merge ambient (impure) environment into cached env.
fn merge_impure_env(mut env: EnvMap) -> EnvMap {
    let mut delim = EnvMap::new();
    delim.insert(OsString::from("PATH"), OsString::from(":"));
    delim.insert(OsString::from("HOST_PATH"), OsString::from(":"));
    delim.insert(OsString::from("XDG_DATA_DIRS"), OsString::from(":"));

    // Set to "/no-cert-file.crt" by setup.sh for pure envs.
    env.remove(OsStr::new("SSL_CERT_FILE"));
    env.remove(OsStr::new("NIX_SSL_CERT_FILE"));

    env.insert(OsString::from("IN_NIX_SHELL"), OsString::from("impure"));

    for (var, val) in std::env::vars_os() {
        env.entry(var.clone())
            .and_modify(|old_val| {
                if let Some(d) = delim.get(&var) {
                    *old_val = OsString::from(OsStr::from_bytes(
                        &[
                            old_val.as_os_str().as_bytes(),
                            d.as_os_str().as_bytes(),
                            val.as_os_str().as_bytes(),
                        ]
                        .concat(),
                    ));
                }
            })
            .or_insert_with(|| val);
    }

    env
}

fn merge_env(mut env: EnvMap) -> EnvMap {
    // These variables are always passed by the original nix-shell, regardless
    // of the --pure flag.
    let keep = &[
        "USER",
        "LOGNAME",
        "DISPLAY",
        "WAYLAND_DISPLAY",
        "WAYLAND_SOCKET",
        "TERM",
        "NIX_SHELL_PRESERVE_PROMPT",
        "TZ",
        "PAGER",
        "SHLVL",
    ];
    for var in keep {
        if let Some(vel) = std::env::var_os(var) {
            env.insert(OsString::from(var), vel);
        }
    }
    env
}

fn build_bash_options(env: &EnvOptions) -> Vec<OsString> {
    // XXX: only check for options that are set by current stdenv and nix-shell.
    const BASH_OPTIONS: [&[u8]; 3] =
        [b"execfail", b"inherit_errexit", b"nullglob"];
    const SHELL_OPTIONS: [&[u8]; 1] = [b"pipefail"];
    chain!(
        env.bashopts
            .as_bytes()
            .split(|b| *b == b':')
            .filter(|opt| BASH_OPTIONS.contains(opt))
            .map(|opt| vec!["-O".into(), OsString::from_vec(opt.to_vec())]),
        env.shellopts
            .as_bytes()
            .split(|b| *b == b':')
            .filter(|opt| SHELL_OPTIONS.contains(opt))
            .map(|opt| vec!["-o".into(), OsString::from_vec(opt.to_vec())]),
    )
    .flatten()
    .collect()
}

fn check_cache(hash: &str) -> Option<BTreeMap<OsString, OsString>> {
    let env_fname = XDG_DIRS.find_cache_file(format!("{hash}.env"))?;
    let drv_fname = XDG_DIRS.find_cache_file(format!("{hash}.drv"))?;
    let trace_fname = XDG_DIRS.find_cache_file(format!("{hash}.trace"))?;

    let env = read(env_fname).unwrap().pipe(deserealize_env);

    let drv_store_fname = read_link(drv_fname).ok()?;
    std::fs::metadata(drv_store_fname).ok()?;

    let trace = read(trace_fname).unwrap().pipe(Trace::load);
    if trace.check_for_changes() {
        return None;
    }

    Some(env)
}

fn cache_write(hash: &str, ext: &str, text: &[u8]) {
    let f = || -> Result<(), std::io::Error> {
        let fname = XDG_DIRS.place_cache_file(format!("{hash}.{ext}"))?;
        let mut file = File::create(fname)?;
        file.write_all(text)?;
        Ok(())
    };
    match f() {
        Ok(_) => (),
        Err(e) => eprintln!("Warning: can't store cache: {e}"),
    }
}

fn cache_symlink(hash: &str, ext: &str, target: &str) {
    let f = || -> Result<(), std::io::Error> {
        let fname = XDG_DIRS.place_cache_file(format!("{hash}.{ext}"))?;
        let _ = std::fs::remove_file(&fname);
        std::os::unix::fs::symlink(target, &fname)?;
        Ok(())
    };
    match f() {
        Ok(_) => (),
        Err(e) => eprintln!("Warning: can't symlink to cache: {e}"),
    }
}

fn wrap(cmd: Vec<OsString>) {
    if cmd.is_empty() {
        eprintln!("cached-nix-shell: command not specified");
        eprintln!("usage: cached-nix-shell --wrap COMMAND ARGS...");
        exit(1);
    }

    if access(
        Path::new(&format!("{}/nix-shell", env!("CNS_WRAP_PATH"))),
        AccessFlags::X_OK,
    )
    .is_err()
    {
        eprintln!(
            "cached-nix-shell: couldn't wrap, {}/nix-shell is not executable",
            env!("CNS_WRAP_PATH")
        );
        exit(1);
    }

    let new_path = [
        env!("CNS_WRAP_PATH").as_bytes(),
        b":",
        std::env::var_os("PATH").unwrap().as_bytes(),
    ]
    .concat();

    let exec = Command::new(&cmd[0])
        .args(&cmd[1..])
        .env("PATH", OsStr::from_bytes(&new_path))
        .exec();
    eprintln!("cached-nix-shell: couldn't run: {exec}");
    exit(1);
}

fn main() {
    let argv: Vec<OsString> = std::env::args_os().collect();

    if argv.len() >= 2 && argv[1] == "--wrap" {
        wrap(std::env::args_os().skip(2).collect());
    }

    if argv.len() >= 2 {
        let fname = &argv[1];
        if let Some(nix_shell_args) = shebang::parse_script(fname) {
            run_script(
                fname.clone(),
                nix_shell_args,
                std::env::args_os().skip(2).collect(),
            );
        }
    }
    run_from_args(std::env::args_os().skip(1).collect());
}

repo_name: xzfc/cached-nix-shell

repo_language: Rust

file_name: Makefile

mime_type: text/plain


text:
pub fn is_literal_bash_string(command: &[u8]) -> bool {
    let mut previous = None;
    for &c in command {
        if b"\t\n !\"$&'()*,;<>?[\\]^`{|}".contains(&c) {
            return false;
        }
        if previous.is_none() && b"#-~".contains(&c) {
            // Special case: `-` isn't a part of bash syntax, but it is treated
            // as an argument of `exec`.
            return false;
        }
        if (previous == Some(b':') || previous == Some(b'=')) && c == b'~' {
            return false;
        }
        previous = Some(c);
    }
    true
}

pub fn quote(arg: &[u8]) -> Vec<u8> {
    let mut result = vec![b'\''];
    for &i in arg {
        if i == b'\'' {
            result.extend_from_slice(br#"'\''"#)
        } else {
            result.push(i)
        }
    }
    result.push(b'\'');
    result
}


text:
#define _GNU_SOURCE

#include "blake3.h"
#include <dirent.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

static FILE *log_f = NULL;
static const char *pwd = NULL;

#define FATAL() \
	do { \
		fprintf(stderr, "nix-trace.c:%d: %s: %s\n", \
			__LINE__, __func__, strerror(errno)); \
		exit(2); \
	} while(0)

#define LEN 16

// Locks

#ifdef __APPLE__

#include <dispatch/dispatch.h>
static dispatch_semaphore_t print_mutex;
static dispatch_semaphore_t buf_mutex;
#define INIT_MUTEX(MUTEX) MUTEX = dispatch_semaphore_create(1)
#define LOCK(MUTEX) dispatch_semaphore_wait(MUTEX, DISPATCH_TIME_FOREVER)
#define UNLOCK(MUTEX) dispatch_semaphore_signal(MUTEX)

#else

#include <pthread.h>
static pthread_mutex_t print_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t buf_mutex = PTHREAD_MUTEX_INITIALIZER;
#define INIT_MUTEX(MUTEX)
#define LOCK(MUTEX) pthread_mutex_lock(&MUTEX)
#define UNLOCK(MUTEX) pthread_mutex_unlock(&MUTEX)

#endif

// Predeclarations

static void convert_digest(char [static LEN*2+1], const uint8_t [static LEN]);
static int enable(const char *);
static void hash_dir(char [static LEN*2+1], DIR *);
static void hash_file(char [static LEN*2+1], int);
static void print_log(char, const char *, const char *);
static void print_stat(int result, const char *path, struct stat *sb);
static int strcmp_qsort(const void *, const void *);

////////////////////////////////////////////////////////////////////////////////

static void __attribute__((constructor)) init() {
	// Remove ourselves from LD_PRELOAD and DYLD_INSERT_LIBRARIES.
	// We do not want to log child processes.
	// TODO: use `ld.so --preload` instead
	unsetenv("LD_PRELOAD");
	unsetenv("DYLD_INSERT_LIBRARIES");

	const char *fname = getenv("TRACE_NIX");
	if (fname != NULL) {
		log_f = fopen(fname, "w");
		if (log_f == NULL) {
			fprintf(stderr, "trace-nix: can't open file %s: %s\n", fname,
				strerror(errno));
			errno = 0;
		}
#ifdef __APPLE__
		pwd = getcwd(NULL, 0);
#else
		pwd = get_current_dir_name();
#endif
		if (pwd == NULL)
			FATAL();
	}
	unsetenv("TRACE_NIX");

	INIT_MUTEX(print_mutex);
	INIT_MUTEX(buf_mutex);
}

#ifdef __APPLE__

#define WRAPPER(RET, FUN, ARGS) \
	static RET _cns_wrapper_##FUN ARGS; \
	__attribute__((used)) static void *_cns_interpose_##FUN[2] \
	__attribute__((section("__DATA,__interpose"))) = { &_cns_wrapper_##FUN, &FUN }; \
	static RET _cns_wrapper_##FUN ARGS
#define REAL(FUN) FUN

#else

#define WRAPPER(RET, FUN, ARGS) \
	static RET (*_cns_real_##FUN)ARGS = NULL; \
	RET FUN ARGS
#define REAL(FUN) \
	(_cns_real_##FUN == NULL ? (_cns_real_##FUN = dlsym(RTLD_NEXT, #FUN)) : _cns_real_##FUN)

#endif

WRAPPER(int, lstat, (const char *path, struct stat *sb)) {
	int result = REAL(lstat)(path, sb);
	print_stat(result, path, sb);
	return result;
}

#ifdef __linux__
WRAPPER(int, __lxstat, (int ver, const char *path, struct stat *sb)) {
	int result = REAL(__lxstat)(ver, path, sb);
	print_stat(result, path, sb);
	return result;
}
#endif

WRAPPER(int, open, (const char *path, int flags, ...)) {
	va_list args;
	va_start(args, flags);
	int mode = va_arg(args, int);
	va_end(args);

	int fd = REAL(open)(path, flags, mode);

	if (flags == (O_RDONLY|O_CLOEXEC) && enable(path)) {
		if (fd == -1) {
			print_log('f', path, "-");
		} else {
			char digest[LEN*2+1];
			hash_file(digest, fd);
			print_log('f', path, digest);
		}
	}

	return fd;
}

WRAPPER(DIR *, opendir, (const char *path)) {
	DIR *dirp = REAL(opendir)(path);
	if (enable(path)) {
		if (dirp == NULL) {
			print_log('d', path, "-");
		} else {
			char digest[LEN*2+1];
			hash_dir(digest, dirp);
			print_log('d', path, digest);
		}
	}
	return dirp;
}

////////////////////////////////////////////////////////////////////////////////

static int enable(const char *path) {
	if (log_f == NULL || (*path != '/' && strcmp(path, "shell.nix")))
		return 0;

	static const char *ignored_paths[] = {
		"/etc/ssl/certs/ca-certificates.crt",
		"/nix/var/nix/daemon-socket/socket",
		"/nix",
		"/nix/store",
		NULL,
	};
	static const char *ignored_prefices[] = {
		"/nix/store/", // assuming store paths are immutable
		"/nix/var/nix/temproots/",
		"/proc/",
		NULL,
	};
	for (const char **p = ignored_paths; *p; p++)
		if (!strcmp(path, *p))
			return 0;
	for (const char **p = ignored_prefices; *p; p++)
		if (!memcmp(path, *p, strlen(*p)))
			return 0;

	return 1;
}

static void print_stat(int result, const char *path, struct stat *sb) {
	static char *buf = NULL;
	static off_t buf_len = 0;

	if (enable(path)) {
		if (result != 0) {
			print_log('s', path, "-");
		} else if (S_ISLNK(sb->st_mode)) {
			LOCK(buf_mutex);
			if (buf_len < sb->st_size + 2) {
				buf_len = sb->st_size + 2;
				buf = realloc(buf, buf_len);
				if (buf == NULL)
					FATAL();
			}
			ssize_t link_len = readlink(path, buf+1, sb->st_size);
			if (link_len < 0 || link_len != sb->st_size)
				FATAL();
			buf[0] = 'l';
			buf[sb->st_size+1] = 0;
			print_log('s', path, buf);
						UNLOCK(buf_mutex);
		} else if (S_ISDIR(sb->st_mode)) {
			print_log('s', path, "d");
		} else {
			print_log('s', path, "+");
		}
	}
}

static void print_log(char op, const char *path, const char *result) {
	LOCK(print_mutex);
	fprintf(
		log_f,
		"%c" "%s%s" "%s%c" "%s%c",
		op,
		path[0] == '/' ? "" : pwd, path[0] == '/' ? "" : "/",
		path, (char)0,
		result, (char)0
	);
	fflush(log_f);
	UNLOCK(print_mutex);
}

static void hash_file(char digest_s[static LEN*2+1], int fd) {
	struct stat stat_;
	int rc = fstat(fd, &stat_);
	if (rc != 0)
		FATAL();
	char *mmaped = NULL;
	if (stat_.st_size != 0) {
		mmaped = mmap(NULL, stat_.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
		if (mmaped == MAP_FAILED) {
			strcpy(digest_s, "e");
			return;
		}
	}

	blake3_hasher hasher;
	blake3_hasher_init(&hasher);
	blake3_hasher_update(&hasher, mmaped, stat_.st_size);
	uint8_t digest_b[LEN];
	blake3_hasher_finalize(&hasher, digest_b, LEN);
	convert_digest(digest_s, digest_b);

	if (stat_.st_size != 0) {
		rc = munmap(mmaped, stat_.st_size);
		if (rc != 0)
			FATAL();
	}
}

static int strcmp_qsort(const void *a, const void *b) {
	return strcmp(*(const char* const*)a, *(const char * const*)b);
}

static void hash_dir(char digest_s[static LEN*2+1], DIR *dirp) {
	// A dynamically growing array of strings
	size_t entries_total = 32, n = 0;
	char **entries = calloc(entries_total, sizeof(char*));
	if (entries == NULL)
		FATAL();

	struct dirent *ent;
	while ((ent = readdir(dirp))) {
		if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, ".."))
			continue;

		if (n+1 >= entries_total) {
			entries_total *= 2;
			entries = realloc(entries, entries_total * sizeof(char*));
			if (entries == NULL)
				FATAL();
		}

		char ent_type =
			ent->d_type == DT_DIR ? 'd' :
			ent->d_type == DT_LNK ? 'l' :
			ent->d_type == DT_REG ? 'f' :
			'u';
		int l = asprintf(&entries[n++], "%s=%c", ent->d_name, ent_type);
		if (l == -1)
			FATAL();
	}

	qsort(entries, n, sizeof(char*), strcmp_qsort);

	// Calculate hash
	uint8_t digest_b[LEN];
	blake3_hasher hasher;
	blake3_hasher_init(&hasher);
	for (size_t i = 0; i < n; i++)
		blake3_hasher_update(&hasher, entries[i], strlen(entries[i])+1);
	blake3_hasher_finalize(&hasher, digest_b, LEN);
	convert_digest(digest_s, digest_b);

	// Memory cleanup
	for (size_t i = 0; i < n; i++)
		free(entries[i]);
	free(entries);

	// Revert dirp into initial state
	rewinddir(dirp);
}

static void convert_digest(char digest_s[static LEN*2+1], const uint8_t digest_b[static LEN]) {
	for (int i = 0; i < LEN; i++)
		sprintf(digest_s + i*2, "%02x", (unsigned)digest_b[i]);
}


text:
.PHONY: test clean

DESTDIR := ./build

BLAKE3_SRCS := blake3.c blake3_dispatch.c blake3_portable.c
BLAKE3_FLAGS := -I $(BLAKE3_CSRC)
ifneq ($(filter aarch64-%, $(system)),)
    BLAKE3_SRCS += blake3_neon.c
else
    BLAKE3_FLAGS += -DBLAKE3_USE_NEON=0
endif
ifneq ($(filter x86_64-%, $(system)),)
    BLAKE3_SRCS += blake3_sse2_x86-64_unix.S \
                   blake3_sse41_x86-64_unix.S \
                   blake3_avx2_x86-64_unix.S \
                   blake3_avx512_x86-64_unix.S
else
    BLAKE3_FLAGS += -DBLAKE3_NO_SSE2 -DBLAKE3_NO_SSE41 \
                    -DBLAKE3_NO_AVX2 -DBLAKE3_NO_AVX512
endif

$(DESTDIR)/trace-nix.so: trace-nix.c Makefile
	@mkdir -p $(DESTDIR)
	$(CC) -fPIC -shared -o $@ $< \
		$(BLAKE3_FLAGS) $(addprefix $(BLAKE3_CSRC)/, $(BLAKE3_SRCS))

test: build/trace-nix.so
	./test.sh

clean:
	rm -rf build test-tmp


text:
#!/bin/sh

run() {
	rm -f test-tmp/log
	DYLD_INSERT_LIBRARIES=$PWD/build/trace-nix.so LD_PRELOAD=$PWD/build/trace-nix.so TRACE_NIX=test-tmp/log \
		nix-shell --run : -p -- "$@" 2>/dev/null
}

run_without_p() {
	rm -f test-tmp/log
	DYLD_INSERT_LIBRARIES=$PWD/build/trace-nix.so LD_PRELOAD=$PWD/build/trace-nix.so TRACE_NIX=test-tmp/log \
		nix-shell --run : -- "$@" 2>/dev/null
}

result=0

dir_b3sum() {
	find "$1" -mindepth 1 -maxdepth 1 -printf '%P=%y\0' |
		sed -z 's/[^dlf]$/u/' |
		LC_ALL=C sort -z |
		b3sum |
		head -c 32
}

check() {
	local name="$1" key="$2" val="$3"

	if ! grep -qzFx -- "$key" test-tmp/log; then
		printf "\33[31mFail: %s: can't find key\33[m\n" "$name"
		return
		result=1
	fi

	local actual_val="$(grep -zFx -A1 -- "$key" test-tmp/log | tail -zn1 | tr -d '\0')"
	if [ "$val" != "$actual_val" ]; then
		printf "\33[31mFail: %s: expected '%s', got '%s'\33[m\n" \
			"$name" "$val" "$actual_val"
		return
		result=1
	fi

	printf "\33[32mOK: %s\33[m\n" "$name"
}

rm -rf test-tmp
mkdir test-tmp
echo '"foo"' > test-tmp/test.nix
: > test-tmp/empty
ln -s empty test-tmp/link

x=""
for i in {1..64};do
	x=x$x
	mkdir -p test-tmp/many-dirs/$x
done

run 'with import <unstable> {}; bash'
check import-channel \
	"s/nix/var/nix/profiles/per-user/root/channels/unstable" \
	"l$(readlink /nix/var/nix/profiles/per-user/root/channels/unstable)"

run 'with import <nonexistentChannel> {}; bash'
check import-channel-ne \
	"s/nix/var/nix/profiles/per-user/root/channels/nonexistentChannel" '-'

run 'import ./test-tmp/test.nix'
check import-relative-nix \
	"s$PWD/test-tmp/test.nix" "+"

run 'import ./test-tmp'
check import-relative-nix-dir \
	"s$PWD/test-tmp" "d"

run 'import ./nonexistent.nix'
check import-relative-nix-ne \
	"s$PWD/nonexistent.nix" "-"

run 'builtins.readFile ./test-tmp/test.nix'
check builtins.readFile \
	"f$PWD/test-tmp/test.nix" \
	"$(b3sum ./test-tmp/test.nix | head -c 32)"

run 'builtins.readFile "/nonexistent/readFile"'
check builtins.readFile-ne \
	"f/nonexistent/readFile" "-"

run 'builtins.readFile ./test-tmp'
check builtins.readFile-dir \
	"f$PWD/test-tmp" "e"

run 'builtins.readFile ./test-tmp/empty'
check builtins.readFile-empty \
	"f$PWD/test-tmp/empty" \
	"$(b3sum ./test-tmp/empty | head -c 32)"

run 'builtins.readDir ./test-tmp'
check builtins.readDir \
	"d$PWD/test-tmp" "$(dir_b3sum ./test-tmp)"

run 'builtins.readDir "/nonexistent/readDir"'
check builtins.readDir-ne \
	"d/nonexistent/readDir" "-"

run 'builtins.readDir ./test-tmp/many-dirs'
check builtins.readDir-many-dirs \
	"d$PWD/test-tmp/many-dirs" "$(dir_b3sum ./test-tmp/many-dirs)"

run_without_p
check implicit:shell.nix \
	"s$PWD/shell.nix" "-"
check implicit:default.nix \
	"s$PWD/default.nix" "-"

exit $result


text:
# trace-nix

Using `LD_PRELOAD` trick to trace nix access to dirs and files.

## Usage

Run `nix-shell` (or `nix repl` or any other nix tool) with the `TRACE_NIX` environment variable.

Example:
``` bash
LD_PRELOAD=/path/to/trace-nix.so TRACE_NIX=./log nix-shell -p stdenv --run :

# The NUL-separated list of entries will be stored in `./log`
```

## Log format

Since the file names could contain arbitrary byte sequences (broken utf8, `\n`, etc), the NUL-separated format is chosen.

```
lstat() == -1:               `s` FILENAME `\0` `-` `\0`
lstat() ==  0 && S_ISLNK():  `s` FILENAME `\0` `l` readlink(FILENAME) `\0`
lstat() ==  0 && S_ISDIR():  `s` FILENAME `\0` `d` `\0`
lstat() ==  0 && !S_ISDIR() && !S_ISLNK(): `s` FILENAME `\0` `+` `\0`

open() == -1:                `f` FILENAME `\0` `-` `\0`
open() != -1:                `f` FILENAME `\0` b3sum(file contents) `\0`
open() != -1 && read error:  `f` FILENAME `\0` `e` `\0`

opendir() == NULL:           `d` FILENAME `\0` `-` `\0`
opendir() != NULL:           `d` FILENAME `\0` b3sum(directory listing) `\0`
```

Directory listing:
```
find -mindepth 1 -maxdepth 1 -printf '%P=%y\0' | sed -z 's/[^dlf]$/u/' | LC_ALL=C sort -z
```


text:
set -e

export XDG_CACHE_HOME=$PWD/tmp/cache
rm -rf ./tmp
mkdir -p ./tmp

printf "\n\33[1m* Test file %s\33[m\n" "$0"

trap 'at_exit $?' EXIT
begin_t=$(date +%s)

at_exit() {
	local rc=$(($? || result))
	local end_t
	end_t=$(date +%s)
	set -- tmp/cache/cached-nix-shell/*.env
	[ -f "$1" ] || shift
	printf "\33[1m* rc:%s seconds:%s entries:%s\33[m\n" \
		"$rc" "$((end_t - begin_t))" "$#"
	exit "$rc"
}

result=0

put() {
	if [ "$#" = 1 ]; then
		cat > "$1"
	elif [ "$#" = 2 ] && [ "$1" = "+x" ]; then
		cat > "$2"
		chmod +x "$2"
	fi
}

run() {
	rm -f tmp/time tmp/out tmp/err
	local testtmp=$PWD/tmp
	printf "\33[33m  * Running %s\33[m\n" "$*"
	(
		if [ "$1" = "--chdir" ]; then
			cd "$2"
			shift 2
		fi
		command time -p -o $testtmp/time -- "$@"
	) 2>&1 > tmp/out | tee tmp/err
}

inline=0
run_inline() {
	put +x ./tmp/inline$inline
	run ./tmp/inline$inline "$@"
	inline=$((inline+1))
}

not() {
	! "$@"
}

check() {
	local text
	text=$1
	shift
	if "$@";then
		printf "\33[32m  + %s\33[m\n" "$text"
	else
		printf "\33[31m  - %s\33[m\n" "$text"
		result=1
	fi
}

check_contains() { check "contains $1" grep -q "$1" tmp/out; }
check_stderr_contains() { check "contains $1" grep -q "$1" tmp/err; }
check_slow() {
	check "slow ($(sed 's/.* //;q' tmp/time))" \
		grep -q "^cached-nix-shell: updating cache$" tmp/err
}
check_fast() {
	check "fast ($(sed 's/.* //;q' tmp/time))" \
		not grep -q "^cached-nix-shell: updating cache$" tmp/err
}

skip() { printf "\33[33m? skip %s\33[m\n" "$*"; }


text:
#!/bin/sh
. ./lib.sh

run_inline << 'EOF'
#! /usr/bin/env cached-nix-shell
--[[
#! nix-shell -i lua -p "luajit.withPackages (p: [ p.basexx ] )"
#! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-22.11.tar.gz
--]]

print(require("basexx").to_base64("hello"))
EOF
check_contains "aGVsbG8="


text:
#!/bin/sh
. ./lib.sh

put tmp/prefix.nix << 'EOF'
with import <nixpkgs> { };
let
  add-prefix = stdenv.mkDerivation {
    name = "cached-nix-shell-test-add-prefix";
    unpackPhase = ":";
    installPhase = ":";
    setupHook = writeText "setup-hook" ''
      export FOO=prefix:$FOO
    '';
  };
in mkShell { buildInputs = [ add-prefix ]; }
EOF

export FOO=foo-value
unset BAR
unset TERM

run cached-nix-shell -p --run 'echo ${FOO-doesnt-have-foo}'
check_contains "^foo-value$"

run cached-nix-shell -p --pure --run 'echo ${FOO-doesnt-have-foo}'
check_contains "^doesnt-have-foo$"
check_fast

run cached-nix-shell -p --pure --keep FOO --run 'echo ${FOO-doesnt-have-foo}'
check_contains "^foo-value$"
# TODO: this should not invalidate the cache
skip check_fast

run cached-nix-shell ./tmp/prefix.nix --keep FOO \
	--run 'echo ${FOO-doesnt-have-foo}'
check_contains "^prefix:foo-value$"

run cached-nix-shell ./tmp/prefix.nix --pure --keep FOO \
	--run 'echo ${FOO-doesnt-have-foo}'
check_contains "^prefix:foo-value$"
check_fast

export BAR=bar-value
run cached-nix-shell ./tmp/prefix.nix --pure --keep FOO --keep BAR \
	--run 'echo ${FOO-doesnt-have-foo}; echo ${BAR-doesnt-have-bar}'
check_contains "^prefix:foo-value$"
check_contains "^bar-value$"
# TODO: this should not invalidate the cache
skip check_fast

run env TERM=term-value cached-nix-shell -p --pure \
	--run 'echo ${TERM-doesnt-have-term}'
check_contains "^term-value$"
check_fast


text:
#!/bin/sh
. ./lib.sh
# Check that shell options are preserved.
# https://github.com/xzfc/cached-nix-shell/issues/28

run nix-shell --pure -p --run '{ shopt -p; shopt -po; } > tmp/nix-shell'
run cached-nix-shell --pure -p --run '{ shopt -p; shopt -po; } > tmp/cached-nix-shell'
check "Options are the same" diff tmp/nix-shell tmp/cached-nix-shell
rm tmp/nix-shell tmp/cached-nix-shell

run $(nix-shell --pure -p which expect --run 'which expect') << 'EOF'
set timeout 5
log_user 0

spawn nix-shell --pure -p
send "\{ shopt -p; shopt -po; \} > tmp/nix-shell; exit\r"
expect eof

spawn cached-nix-shell --pure -p
send "\{ shopt -p; shopt -po; \} > tmp/cached-nix-shell; exit\r"
expect eof
EOF
check "Options are the same" diff tmp/nix-shell tmp/cached-nix-shell


text:
#!/bin/sh
. ./lib.sh
# Test --wrap

run cached-nix-shell --wrap env nix-shell -p lua --run 'lua -v'
check_contains "Lua.org"
check_slow

run cached-nix-shell --wrap env nix-shell -p lua --run 'lua -v'
check_contains "Lua.org"
check_fast


text:
#!/bin/sh
. ./lib.sh

put +x ./tmp/lua.sh << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -i sh -p lua

lua -v
EOF

put +x ./tmp/luajit.sh << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -i sh -p luajit

lua -v
EOF

put +x ./tmp/lua.lua << 'EOF'
#! /usr/bin/env cached-nix-shell
--[[
#! nix-shell -i lua -p lua
]]

print(6 * 7)
EOF

run ./tmp/lua.sh
check_contains "Lua.org"
check_slow

run ./tmp/lua.sh
check_contains "Lua.org"
check_fast

run ./tmp/luajit.sh
check_contains "https\?://luajit.org/"
check_slow

run ./tmp/luajit.sh
check_contains "https\?://luajit.org/"
check_fast

run ./tmp/lua.lua
check_contains "42"
check_fast

run cached-nix-shell -E 'with import <nixpkgs> { }; mkShell { buildInputs = [ lua ]; }' --run 'lua -v'
check_contains "Lua.org"
check_slow

run cached-nix-shell -E 'with import <nixpkgs> { }; mkShell { buildInputs = [ lua ]; }' --run 'lua -v'
check_contains "Lua.org"
check_fast

run cached-nix-shell -E 'with import <nixpkgs> { }; mkShell { buildInputs = [ luajit ]; }' --run 'lua -v'
check_contains "https\?://luajit.org/"
check_slow

run cached-nix-shell -E 'with import <nixpkgs> { }; mkShell { buildInputs = [ luajit ]; }' --run 'lua -v'
check_contains "https\?://luajit.org/"
check_fast

# Check various paths to use the same .nix file.
put +x ./tmp/lua.nix << 'EOF'
with import <nixpkgs> { }; mkShell { buildInputs = [ lua ]; }
EOF

run_inline << 'EOF'
#! /usr/bin/env cached-nix-shell
#! nix-shell -i sh ./lua.nix
lua -v
EOF
check_contains "Lua.org"

run cached-nix-shell ./tmp/lua.nix --run 'lua -v'
check_contains "Lua.org"
check_fast

run --chdir tmp cached-nix-shell ./lua.nix --run 'lua -v'
check_contains "Lua.org"
check_fast

run --chdir tmp cached-nix-shell lua.nix --run 'lua -v'
check_contains "Lua.org"
check_fast

run --chdir / cached-nix-shell "$PWD/tmp/lua.nix" --run 'lua -v'
check_contains "Lua.org"
check_fast

# Script with a trailing space in the nix-shell options
run_inline << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -i sh -p lua
lua -v
EOF
check_contains "Lua.org"


text:
#!/bin/sh
. ./lib.sh
# Check the normalization of PWD.  The cache should evaluate in the directory
# containing default.nix no matter the current PWD or the way the path to
# default.nix is specified.  This behavior doesn't match the behavior of
# nix-shell, but this test case checks that at least it is consistent.

mkdir tmp/foo tmp/bar
put +x ./tmp/foo/default.nix << 'EOF'
with import <nixpkgs> { }; mkShell {
	shellHook = ''printf '%s\n' "$PWD" > "$GOT"'';
}
EOF

check_normalized_pwd() {
	rm -f "$GOT" tmp/cache/cached-nix-shell/*
	run --chdir "$1" cached-nix-shell --keep GOT "$2" --run :
	check "got $(cat tmp/got)" cmp -s tmp/expected tmp/got
}

export GOT=$PWD/tmp/got
printf "%s\n" "$PWD/tmp/foo" > "$PWD/tmp/expected"
printf "Expected: %s\n" "$PWD/tmp/foo"

check_normalized_pwd . tmp/foo
check_normalized_pwd . tmp/foo/
check_normalized_pwd . tmp/foo/default.nix

check_normalized_pwd . ./tmp/foo
check_normalized_pwd . ./tmp/foo/
check_normalized_pwd . ./tmp/foo/default.nix

check_normalized_pwd . "$PWD"/tmp/foo
check_normalized_pwd . "$PWD"/tmp/foo/
check_normalized_pwd . "$PWD"/tmp/foo/default.nix

check_normalized_pwd tmp/foo ""
check_normalized_pwd tmp/foo .
check_normalized_pwd tmp/foo ./default.nix
check_normalized_pwd tmp/foo default.nix

check_normalized_pwd tmp/bar ../foo
check_normalized_pwd tmp/bar ../foo/
check_normalized_pwd tmp/bar ../foo/default.nix


text:
#!/bin/sh
. ./lib.sh

unset F0 F1
F0="$(printf '%b' '\360')"
F1="$(printf '%b' '\361')"

# Non-UTF-8 PWD
# Skip on filesystems that do not support non-UTF-8 paths, e.g. APFS.
if mkdir ./tmp/a"$F0"b; then
put ./tmp/a"$F0"b/shell.nix << 'EOF'
with import <nixpkgs> { }; mkShell { buildInputs = [ lua ]; }
EOF

put +x ./tmp/a"$F0"b/run.sh << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -i sh --pure ./shell.nix
lua -v
EOF

run cached-nix-shell ./tmp/a"$F0"b/run.sh
check_contains "Lua.org"
fi

# Non-UTF-8 environment variable passed to shebang script
export VAR=a"$F0"b
run_inline << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -pi sh
env -0 | LANG=C grep -az "^VAR=" | cat -v
EOF
check_contains '^VAR=aM-pb\^@$'
unset VAR

# Non-UTF-8 shebang script
run_inline << EOF # unquoted
#!/usr/bin/env cached-nix-shell
#! nix-shell -pi sh
echo '$F0$F1' | cat -v
EOF
check_contains '^M-pM-q$'

# Non-UTF-8 shebang script argument
run_inline foo"$F0"bar << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -pi sh
printf "!%s!\n" "$@" | cat -v
EOF
check_contains "^!fooM-pbar!$"

# Non-UTF-8 environment variable passed to/exported by setup.sh
export VAR_IN=a"$F0"b
put ./tmp/shellhook.nix << 'EOF'
with import <nixpkgs> { };
mkShell {
  shellHook = ''
    export VAR_OUT
    VAR_OUT=out:$(env -0 | LANG=C grep -z "^VAR_IN=" | cat -v)
  '';
}
EOF
run cached-nix-shell ./tmp/shellhook.nix --pure --keep VAR_IN \
	--run 'printf "%s\n" "$VAR_OUT"'
check_contains '^out:VAR_IN=aM-pb\^@$'
unset VAR_IN

# Non-UTF-8 evaluation result
put ./tmp/evaluation.nix << EOF # unquoted
with import <nixpkgs> { };
mkShell {
  "VAR1" = "A${F0}B";
  "VAR${F1}2" = "CD";
}
EOF

# Skip because nix show-derivation fails
run cached-nix-shell ./tmp/evaluation.nix --pure \
	--run 'env -0 | LANG=C grep -z "^VAR1=" | cat -v'
skip check_contains '^VAR1=AM-pB\^@$'

run cached-nix-shell ./tmp/evaluation.nix --pure \
	--run 'env -0 | LANG=C grep -z "^VAR.2=" | cat -v'
skip check_contains '^VARM-q2=CD\^@$'


text:
#!/bin/sh
. ./lib.sh

mkdir -p tmp/bin

put +x tmp/bin/some_bin << 'EOF'
#!/bin/sh
echo running-some-bin
EOF

export PATH=$PWD/tmp/bin:$PATH

run_inline << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -i sh -p hello

if command -v some_bin >/dev/null 2>&1; then
	some_bin
else
	echo cant-find-some-bin
fi
hello
EOF
check_contains "running-some-bin"
check_contains "Hello, world!"

run_inline << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -i sh -p hello --pure

if command -v some_bin >/dev/null 2>&1; then
	some_bin
else
	echo cant-find-some-bin
fi
hello
EOF
check_contains "cant-find-some-bin"
check_contains "Hello, world!"


text:
#!/bin/sh

set -e

# Check prerequisites
hash cached-nix-shell cat chmod cmp cp \
	date diff env grep ln mkdir rev rm tail tee time touch

trap 'exit 130' INT

case "$0" in
  */*) cd -- "${0%/*}";;
esac

echo "Testing $(command -v cached-nix-shell)"

result=0

if [ $# = 0 ]; then
	set -- ./t[0-9]*.sh
fi

for t in "$@"; do
	sh -- "$t" || result=1
done

if [ "$result" = 0 ]; then
	printf "\n\33[32mAll tests passed\33[m\n"
else
	printf "\n\33[31mSome tests failed\33[m\n"
fi
rm -rf tmp
exit $result


text:
#!/bin/sh
. ./lib.sh
# Test interactive shell

unset PS1 BASHRC_IS_SOURCED NIX_SHELL_PRESERVE_PROMPT

expect=$(nix-shell --pure -p which expect --run 'which expect')
put +x tmp/run << EOF
# Make sure that prompt containing long PWD will fit
set stty_init "rows 10 cols 2000"
# TODO: fix test not to download nixpkgs when running on CI
set timeout 180 ;

log_file -a tmp/out
log_user 0
spawn sh -c [lindex \$argv 0]
send [string map {"\n" "\r\n"} [read stdin]]
send "exit\r"
expect eof
log_file
exec sh -c "tr -d \"\\r\" < tmp/out > tmp/err"
EOF

mkdir -p tmp/home
export HOME=$PWD/tmp/home
put tmp/home/.bashrc << 'EOF'
PS1=NEW_PROMPT
BASHRC_IS_SOURCED=1
EOF

run $expect tmp/run 'sleep 0.1 && cached-nix-shell --pure -p' << 'EOF'
echo uryyb jbeyq | tr a-z n-za-m
EOF
check_contains '\[cached-nix-shell:'
check_contains 'hello world'
check_slow

run $expect tmp/run 'cached-nix-shell --pure -p' << 'EOF'
echo uryyb jbeyq | tr a-z n-za-m
EOF
check_contains 'hello world'
skip check_fast  # TODO: fix /tmp/nix-$$-* issue

run cached-nix-shell --pure -p --exec true
check_fast

# Check pre-defined env vars and inclusion of .bashrc
run $expect tmp/run 'cached-nix-shell --pure -p' << 'EOF'
echo IN_NIX_SHELL=$IN_NIX_SHELL
echo IN_CACHED_NIX_SHELL=$IN_CACHED_NIX_SHELL
echo BASHRC_IS_SOURCED=${BASHRC_IS_SOURCED:-unset}
echo PS1=$PS1
EOF
check_contains 'IN_NIX_SHELL=pure'
check_contains 'IN_CACHED_NIX_SHELL=1'
check_contains 'BASHRC_IS_SOURCED=unset'
check_contains 'PS1=.*\[cached-nix-shell:\\w].*'
check_fast

run $expect tmp/run 'cached-nix-shell -p' << 'EOF'
echo IN_NIX_SHELL=$IN_NIX_SHELL
echo IN_CACHED_NIX_SHELL=$IN_CACHED_NIX_SHELL
echo BASHRC_IS_SOURCED=${BASHRC_IS_SOURCED:-unset}
echo PS1=$PS1
EOF
check_contains 'IN_NIX_SHELL=impure'
check_contains 'IN_CACHED_NIX_SHELL=1'
check_contains 'BASHRC_IS_SOURCED=1'
check_contains 'PS1=.*\[cached-nix-shell:\\w].*'
check_fast

NIX_SHELL_PRESERVE_PROMPT=1 run $expect tmp/run 'cached-nix-shell -p' << 'EOF'
echo PS1=$PS1
EOF
check_contains 'PS1=NEW_PROMPT'
check_fast


text:
#!/bin/sh
. ./lib.sh
# Ensure that the output of shellHook does not screw the environment variables.

put ./tmp/shell.nix << 'EOF'
with import <nixpkgs> { }; mkShell { shellHook = "echo hello$((6*7))world"; }
EOF

run cached-nix-shell ./tmp/shell.nix --run 'env > tmp/env'

check_contains hello42world

check "tmp/env does not contain hello42world" \
	not grep -q hello42world tmp/env


text:
#!/bin/sh
. ./lib.sh
# Test -A / --attr handling

put ./tmp/foobar.nix << 'EOF'
with import <nixpkgs> { }; {
  foo = mkShell {
    buildInputs = [
      (stdenv.mkDerivation {
        name = "cached-nix-shell-test-foo";
        unpackPhase = ":";
        installPhase = ''
          mkdir -p $out/bin
          echo 'echo i-am-foo' > $out/bin/foo
          chmod +x $out/bin/foo
        '';
      })
    ];
  };
  bar = mkShell {
    buildInputs = [
      (stdenv.mkDerivation {
        name = "cached-nix-shell-test-bar";
        unpackPhase = ":";
        installPhase = ''
          mkdir -p $out/bin
          echo 'echo i-am-bar' > $out/bin/bar
          chmod +x $out/bin/bar
        '';
      })
    ];
  };
}
EOF

run_inline << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -i sh -A foo ./foobar.nix

if command -v foo >/dev/null 2>&1; then
	foo
else
	echo cant-find-foo
fi

if command -v bar >/dev/null 2>&1; then
	bar
else
	echo cant-find-bar
fi
EOF
check_contains "i-am-foo"
check_contains "cant-find-bar"


text:
#!/bin/sh
. ./lib.sh

put ./tmp/foo << 'EOF'
with import <nixpkgs> { };
mkShell { VAR="val"; }
EOF

put ./tmp/empty << 'EOF'
{}
EOF

# Get default NIX_PATH in case it's not set. Do not export it to not interfere
# with tests that do not use it.
[ "${NIX_PATH:-}" ] || NIX_PATH="$(
	nix-instantiate --eval -E '
		builtins.concatStringsSep ":"
		(map (a: (if a.prefix == "" then "" else a.prefix + "=") + a.path)
		builtins.nixPath)
	' | tr -d '"')"

################################################################################
# <path_variable>
################################################################################

# <path_variable>, absolute NIX_PATH

run            env "NIX_PATH=$PWD/tmp:$NIX_PATH"         cached-nix-shell '<foo>' --run 'echo $VAR'
check_contains "^val$"
check_slow

run --chdir .. env "NIX_PATH=$PWD/tmp:$NIX_PATH"         cached-nix-shell '<foo>' --run 'echo $VAR'
check_contains "^val$"
check_fast

run            env "NIX_PATH=bar=$PWD/tmp/foo:$NIX_PATH" cached-nix-shell '<bar>' --run 'echo $VAR'
check_contains "^val$"
check_slow

run --chdir .. env "NIX_PATH=bar=$PWD/tmp/foo:$NIX_PATH" cached-nix-shell '<bar>' --run 'echo $VAR'
check_contains "^val$"
check_fast

# <path_variable>, absolute -I

run            cached-nix-shell -I "$PWD/tmp"         '<foo>' --run 'echo $VAR'
check_contains "^val$"
check_slow

run --chdir .. cached-nix-shell -I "$PWD/tmp"         '<foo>' --run 'echo $VAR'
check_contains "^val$"
check_fast

run            cached-nix-shell -I "bar=$PWD/tmp/foo" '<bar>' --run 'echo $VAR'
check_contains "^val$"
check_slow

run --chdir .. cached-nix-shell -I "bar=$PWD/tmp/foo" '<bar>' --run 'echo $VAR'
check_contains "^val$"
check_fast

# <path_variable>, relative NIX_PATH

run env "NIX_PATH=.:$NIX_PATH"         cached-nix-shell '<tmp/foo>' --run 'echo $VAR'
check_contains "^val$"

run env "NIX_PATH=bar=./tmp:$NIX_PATH" cached-nix-shell '<bar/foo>' --run 'echo $VAR'
check_contains "^val$"

# <path_variable>, relative -I

run cached-nix-shell -I .             '<tmp/foo>' --run 'echo $VAR'
check_contains "^val$"

run cached-nix-shell -I bar=./tmp/foo '<bar>'     --run 'echo $VAR'
check_contains "^val$"

################################################################################
# Other cases
################################################################################

# URI
run cached-nix-shell https://github.com/NixOS/nixpkgs/archive/nixos-22.11.tar.gz -A lua --run 'env'
check_contains '^name=lua-5\..*'

# Multiple .nix files (why it is even supported by nix-shell)

run cached-nix-shell ./tmp/foo ./tmp/empty --run 'echo $VAR'
check_contains "^val$"

run cached-nix-shell ./tmp/empty ./tmp/foo --run 'echo $VAR'
check_contains "^val$"

# Path to .drv file

lua_drv=$(nix-instantiate --no-gc-warning -E '(import <nixpkgs> {}).lua')

run cached-nix-shell "$lua_drv" --run 'env'
check_contains '^name=lua-5\..*'

# TODO: `nix-shell /nix/store/*.drv!out` is not implemented yet.
# run cached-nix-shell "$lua_drv!out" --run 'env'
# check_contains '^name=lua-5\..*'


text:
#!/bin/sh
. ./lib.sh
# Check that cache in invalidated when indirecly used .nix file is updated.

put ./tmp/small.nix << 'EOF'
with import <nixpkgs> { };
mkShell { VAR = import ./foo.nix; }
EOF

echo '"val1"' > ./tmp/foo.nix
run cached-nix-shell ./tmp/small.nix --run 'echo $VAR'
check_contains '^val1$'
check_slow

run cached-nix-shell ./tmp/small.nix --run 'echo $VAR'
check_contains '^val1$'
check_fast

echo '"val2"' > ./tmp/foo.nix
run cached-nix-shell ./tmp/small.nix --run 'echo $VAR'
check_contains '^val2$'
check_slow


text:
#!/bin/sh
. ./lib.sh

# Check that changing --run/--exec arguments do not invalidate the cache.
run cached-nix-shell -p lua --run 'lua -v'
check_contains "Lua.org"
check_slow

run cached-nix-shell -p lua --run 'lua -v | rev'
check_contains "gro.auL"
check_fast

run cached-nix-shell -p luajit --run 'lua -v'
check_contains "https\?://luajit.org/"
check_slow

run cached-nix-shell -p luajit --exec lua -v
check_contains "https\?://luajit.org/"
check_fast

# Check argument expanding "-pj16" -> "-p -j 16"
run cached-nix-shell -pj16 luajit --exec lua -v
check_contains "https\?://luajit.org/"
check_fast

# Check -v/--verbose.
# This test produces a lot of output, so, pipe it through `tail`
run cached-nix-shell -vp --run : | tail -n3
check_slow
check_stderr_contains "^evaluating file '/"

run cached-nix-shell -vp --run :
check_fast

# Check shebang argument passing.
run_inline a b c << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -i bash -p

printf "count=%s" "$#"
printf " '%s'" "$@"
printf "\n"
EOF
check_contains "^count=3 'a' 'b' 'c'$"

# Check nontrivial interpreter.
run_inline a 'b c' << 'EOF'
#!/usr/bin/env cached-nix-shell
#! nix-shell -i "printf ', %q' {1..3}" -p
EOF
check_contains "^, 1, 2, 3, \./tmp/inline[0-9]\+, a, 'b c'$"


text:
#!/bin/sh
. ./lib.sh

put ./tmp/readdir.nix << 'EOF'
with import <nixpkgs> { };
mkShell { x = builtins.toJSON (builtins.readDir ./dir); }
EOF

mkdir -p tmp/dir/a
ln -s c tmp/dir/b
touch tmp/dir/c
run cached-nix-shell ./tmp/readdir.nix --run 'echo $x'
check_contains '^{"a":"directory","b":"symlink","c":"regular"}$'
check_slow

run cached-nix-shell ./tmp/readdir.nix --run 'echo $x'
check_contains '^{"a":"directory","b":"symlink","c":"regular"}$'
check_fast

rm tmp/dir/b
touch tmp/dir/b
run cached-nix-shell ./tmp/readdir.nix --run 'echo $x'
check_contains '^{"a":"directory","b":"regular","c":"regular"}$'
check_slow


text:
#!/bin/sh
. ./lib.sh

put +x ./tmp/lua.nix << 'EOF'
with import <nixpkgs> { }; mkShell { buildInputs = [ lua ]; }
EOF

put +x ./tmp/luajit.nix << 'EOF'
with import <nixpkgs> { }; mkShell { buildInputs = [ luajit ]; }
EOF

mkdir -p ./tmp/foo
cp ./tmp/lua.nix ./tmp/foo/default.nix
# ./tmp/foo is a directory containing ./tmp/foo/default.nix

run cached-nix-shell ./tmp/foo --run 'lua -v'
check_contains "Lua.org"
check_slow

rm -rf ./tmp/foo
cp ./tmp/luajit.nix ./tmp/foo
# now ./tmp/foo is a plain .nix file

run cached-nix-shell ./tmp/foo --run 'lua -v'
check_contains "https\?://luajit.org/"
check_slow

rm -rf ./tmp/foo
mkdir -p ./tmp/foo
cp ./tmp/lua.nix ./tmp/foo/default.nix
# now ./tmp/foo is a directory containing ./tmp/foo/default.nix (again)

run cached-nix-shell ./tmp/foo --run 'lua -v'
check_contains "Lua.org"
check_fast


text:
#!/bin/sh
. ./lib.sh
# Check that cached-nix-shell works even if the $PATH contains an old derivation
# of nix-shell. https://github.com/xzfc/cached-nix-shell/issues/24

PATH=$(nix-build '<old>' -A nix --no-out-link -I \
	old=https://github.com/NixOS/nixpkgs/archive/nixos-21.05.tar.gz
	)/bin:$PATH run cached-nix-shell -p --exec nix-shell --version
check_contains '^nix-shell (Nix) 2\.3\.16$'


text:
# This file has been generated by Niv.

let

  #
  # The fetchers. fetch_<type> fetches specs of type <type>.
  #

  fetch_file = pkgs: name: spec:
    let
      name' = sanitizeName name + "-src";
    in
    if spec.builtin or true then
      builtins_fetchurl { inherit (spec) url sha256; name = name'; }
    else
      pkgs.fetchurl { inherit (spec) url sha256; name = name'; };

  fetch_tarball = pkgs: name: spec:
    let
      name' = sanitizeName name + "-src";
    in
    if spec.builtin or true then
      builtins_fetchTarball { name = name'; inherit (spec) url sha256; }
    else
      pkgs.fetchzip { name = name'; inherit (spec) url sha256; };

  fetch_git = name: spec:
    let
      ref =
        spec.ref or (
          if spec ? branch then "refs/heads/${spec.branch}" else
          if spec ? tag then "refs/tags/${spec.tag}" else
          abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"
        );
      submodules = spec.submodules or false;
      submoduleArg =
        let
          nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0;
          emptyArgWithWarning =
            if submodules
            then
              builtins.trace
                (
                  "The niv input \"${name}\" uses submodules "
                  + "but your nix's (${builtins.nixVersion}) builtins.fetchGit "
                  + "does not support them"
                )
                { }
            else { };
        in
        if nixSupportsSubmodules
        then { inherit submodules; }
        else emptyArgWithWarning;
    in
    builtins.fetchGit
      ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg);

  fetch_local = spec: spec.path;

  fetch_builtin-tarball = name: throw
    ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
        $ niv modify ${name} -a type=tarball -a builtin=true'';

  fetch_builtin-url = name: throw
    ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
        $ niv modify ${name} -a type=file -a builtin=true'';

  #
  # Various helpers
  #

  # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695
  sanitizeName = name:
    (
      concatMapStrings (s: if builtins.isList s then "-" else s)
        (
          builtins.split "[^[:alnum:]+._?=-]+"
            ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name)
        )
    );

  # The set of packages used when specs are fetched using non-builtins.
  mkPkgs = sources: system:
    let
      sourcesNixpkgs =
        import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; };
      hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
      hasThisAsNixpkgsPath = <nixpkgs> == ./.;
    in
    if builtins.hasAttr "nixpkgs" sources
    then sourcesNixpkgs
    else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
      import <nixpkgs> { }
    else
      abort
        ''
          Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
          add a package called "nixpkgs" to your sources.json.
        '';

  # The actual fetching function.
  fetch = pkgs: name: spec:

    if ! builtins.hasAttr "type" spec then
      abort "ERROR: niv spec ${name} does not have a 'type' attribute"
    else if spec.type == "file" then fetch_file pkgs name spec
    else if spec.type == "tarball" then fetch_tarball pkgs name spec
    else if spec.type == "git" then fetch_git name spec
    else if spec.type == "local" then fetch_local spec
    else if spec.type == "builtin-tarball" then fetch_builtin-tarball name
    else if spec.type == "builtin-url" then fetch_builtin-url name
    else
      abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";

  # If the environment variable NIV_OVERRIDE_${name} is set, then use
  # the path directly as opposed to the fetched source.
  replace = name: drv:
    let
      saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name;
      ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}";
    in
    if ersatz == "" then drv else
      # this turns the string into an actual Nix path (for both absolute and
      # relative paths)
    if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}";

  # Ports of functions for older nix versions

  # a Nix version of mapAttrs if the built-in doesn't exist
  mapAttrs = builtins.mapAttrs or (
    f: set: with builtins;
    listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
  );

  # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
  range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1);

  # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257
  stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));

  # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
  stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
  concatMapStrings = f: list: concatStrings (map f list);
  concatStrings = builtins.concatStringsSep "";

  # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331
  optionalAttrs = cond: as: if cond then as else { };

  # fetchTarball version that is compatible between all the versions of Nix
  builtins_fetchTarball = { url, name ? null, sha256 }@attrs:
    let
      inherit (builtins) lessThan nixVersion fetchTarball;
    in
    if lessThan nixVersion "1.12" then
      fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; }))
    else
      fetchTarball attrs;

  # fetchurl version that is compatible between all the versions of Nix
  builtins_fetchurl = { url, name ? null, sha256 }@attrs:
    let
      inherit (builtins) lessThan nixVersion fetchurl;
    in
    if lessThan nixVersion "1.12" then
      fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; }))
    else
      fetchurl attrs;

  # Create the final "sources" from the config
  mkSources = config:
    mapAttrs
      (
        name: spec:
          if builtins.hasAttr "outPath" spec
          then
            abort
              "The values in sources.json should not have an 'outPath' attribute"
          else
            spec // { outPath = replace name (fetch config.pkgs name spec); }
      )
      config.sources;

  # The "config" used by the fetchers
  mkConfig =
    { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null
    , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile)
    , system ? builtins.currentSystem
    , pkgs ? mkPkgs sources system
    }: rec {
      # The sources, i.e. the attribute set of spec name to spec
      inherit sources;

      # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
      inherit pkgs;
    };

in
mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); }


text:
no-default-target: ; @false

VERSION := $(shell sed < Cargo.toml -n 's/^version *= *"\(.*\)".*/\1/p')

post-build:
	make -C nix-trace
	ronn --organization="cached-nix-shell ${VERSION}" -r cached-nix-shell.1.md

post-install:
	mkdir -p ${out}/lib
	cp nix-trace/build/trace-nix.so ${out}/lib

	mkdir -p ${out}/share/cached-nix-shell
	cp rcfile.sh ${out}/share/cached-nix-shell/

	mkdir -p ${out}/share/man/man1
	cp cached-nix-shell.1 ${out}/share/man/man1/

	mkdir -p ${out}/libexec/cached-nix-shell
	ln -s ${out}/bin/cached-nix-shell ${out}/libexec/cached-nix-shell/nix-shell

	mkdir -p ${out}/var/empty

.PHONY: no-default-target post-build post-install


text:
---
title: 'NAME OF THIS STUDY'
author: 'Firstname Lastname'
date: '01 January 2099'
---

repo_name: Reproducible-Science-Curriculum/rr-init

repo_language: R


text:
/* see http://dataprotocols.org/data-packages/ for full specification of this format */
{
    "title": "Data package for study NAME OF STUDY",
    "name": "name-of-study",
    "license": "CC0-1.0",
    "contributors": [
        {
            "name": "Firstname Lastname",
            "email": "youremail@example.com"
        }
    ],
    "resources": [
        {
            "path": "data/raw/rawDataFile1.csv",
            "name": "Name of data file 1",
            "format": "csv"
        }
        {
            "path": "data/clean/cleanDataFile1.csv",
            "name": "Clean version of Name of data file 1",
            "format": "csv",
            "sources": [
                {
                    "path": "data/raw/rawDataFile1.csv"
                }
            ],
        }
    ],
    "datapackage_version": "1.0-beta.10"
}


text:
Reproducible Research Project Initialization
=======

Research project initialization and organization following reproducible research guidelines.

Overview
--------

    project
    |- doc/            # documentation for the study
    |  +- paper/       # manuscript(s), whether generated or not
    |
    |- data            # raw and primary data, are not changed once created
    |  |- raw/         # raw data, will not be altered
    |  +- clean/       # cleaned data, will not be altered once created
    |
    |- code/           # any programmatic code
    |- results         # all output from workflows and analyses
    |  |- figures/     # graphs, likely designated for manuscript figures
    |  +- pictures/    # diagrams, images, and other non-graph graphics
    |
    |- scratch/        # temporary files that can be safely deleted or lost
    |- README          # the top level description of content
    |- study.Rmd       # executable Rmarkdown for this study, if applicable
    |- Makefile        # executable Makefile for this study, if applicable
    |- study.Rproj     # RStudio project for this study, if applicable
    |- datapackage.json # metadata for the (input and output) data files

How to use
----------

* Create a new directory for your project.
* Download the [latest release] of this repository, and unzip it in the directory you just created.
* Open this document in an editor. Change the first line to reflect the title of your research study, and delete the rest of the content in this file. You can, but are not obligated to keep the Acknowledgements section.
* Delete the LICENSE.md file, unless it will also apply to your project. (However, it probbaly will not to all of it.)
* Go forth and experiment, keeping files in their appropriate places.

To the extent possible under law, the author(s) of this template have dedicated all copyright and related and neighboring rights to it to the public domain worldwide under the [CC0 Public Domain Dedication]. The template and all other content in the [rr-init repository] is distributed without any warranty.

Key concepts and goals
----------------------

See [Noble 2009] for a full description of and argument for the principle template structure. Some concepts and goals that guided this work:
* (Good) There is a folder for the raw data, which do not get altered, or intermixed with data that is the result of manual or programmatic manipulation. I.e., derived data is kept separate from raw data, and raw data are not duplicated.
* (Good) Code is kept separate from data.
* (Better) Manuscript production output is kept separate from everything else.
* (Good) There is a scratch directory for experimentation. Everything in the scratch directory can be deleted at any time without negative impact.
* (Better) There should be a `README` in evey directory, describing the purpose of the directory and its contents.
* (Best) There is a top-level `Makefile` or [Rmarkdown] file that documents the computational study in executable form. Those files may call out to other `Makefile`'s or `.Rmd` files in subdirectories.
* (Best) There is a formal metadata descriptor at the root of the package that describes all the important input and output data files.
* (Meta) The template structure should have no special software or skill prerequisites to install. Specifically, deploying the template structure should not require installing git, or using the command line.

Acknowledgements
----------------

The initial file and directory structure of this project was developed by a group of participants in the Reproducible Science Curriculum Workshop, held at [NESCent] in December 2014. The structure is based on, and heavily follows the one proposed by [Noble 2009], with a few but small modifications.

[rr-init repository]: https://github.com/Reproducible-Science-Curriculum/rr-init
[latest release]: https://github.com/Reproducible-Science-Curriculum/rr-init/releases/latest
[NESCent]: http://nescent.org
[Rmarkdown]: http://rmarkdown.rstudio.com/
[Noble 2009]: http://dx.doi.org/10.1371/journal.pcbi.1000424
[CC0 Public Domain Dedication]: http://creativecommons.org/publicdomain/zero/1.0/


text:
package hashring

import (
	"reflect"
	"sort"
	"testing"
)

func TestNodeIdx_Sort(t *testing.T) {
	tests := []struct {
		v nodeIdx
		r nodeIdx
	}{
		{
			v: nodeIdx{5, 1, 3, 9, 10, 0, 78},
			r: nodeIdx{0, 1, 3, 5, 9, 10, 78},
		},

		{
			v: nodeIdx{9, 3, 5, 4, 2, 3, 6},
			r: nodeIdx{2, 3, 3, 4, 5, 6, 9},
		},
	}

	for _, c := range tests {
		sort.Sort(c.v)
		if !reflect.DeepEqual(c.v, c.r) {
			t.Fatalf("expected %v but got %v\n", c.r, c.v)
		}
	}
}

func TestHashRing_AddNode(t *testing.T) {
	tests := []struct {
		virtualNodeCount int
		nodes            []string
		requiredCount    int
	}{
		{
			virtualNodeCount: 1,
			nodes:            []string{"abc_node"},
			requiredCount:    1,
		},

		{
			virtualNodeCount: 3,
			nodes:            []string{"abc_node"},
			requiredCount:    3,
		},

		{
			virtualNodeCount: 4,
			nodes:            []string{"abc_1", "abc_2"},
			requiredCount:    8,
		},

		{
			virtualNodeCount: 1,
			nodes:            []string{"abc_1", "abc_2", "abc_3"},
			requiredCount:    3,
		},
	}

	testContains := func(hr *HashRing, nodes []string) bool {
		result := true
		for _, node := range nodes {
			count := 0
			for _, v := range hr.nodes {
				if v == node {
					count++
				}
			}

			result = result && (count == hr.replicaCount)
		}

		return result
	}

	for _, c := range tests {
		hr := New(c.virtualNodeCount, nil)
		for _, node := range c.nodes {
			hr.Add(node)
		}

		if !(testContains(hr, c.nodes) && len(hr.idx) == c.virtualNodeCount*len(c.nodes)) {
			t.Fatalf("node count didn't match: %+v\n", hr)
		}
	}
}

func TestHashRing_Delete(t *testing.T) {
	vcount := 3
	nodes := []string{"abc_1", "abc_node", "abc_2", "abc_3"}

	hr := New(vcount, nil)
	for _, n := range nodes {
		hr.Add(n)
	}

	testContains := func(hr *HashRing, nodes []string) bool {
		result := true
		for _, node := range nodes {
			count := 0
			for _, v := range hr.nodes {
				if v == node {
					count++
				}
			}

			result = result && (count == hr.replicaCount)
		}

		return result
	}

	tests := []struct {
		nodes         []string
		requiredCount int
	}{
		{
			nodes:         []string{"abc_node"},
			requiredCount: 9,
		},

		{
			nodes:         []string{"abc_3"},
			requiredCount: 6,
		},

		{
			nodes:         []string{"abc_1", "abc_2"},
			requiredCount: 0,
		},
	}

	for _, c := range tests {
		for _, n := range c.nodes {
			hr.Delete(n)
		}

		if testContains(hr, c.nodes) || len(hr.idx) != c.requiredCount {
			t.Fatalf("delete failed: %+v\n", hr)
		}
	}
}

func TestHashRing_Get(t *testing.T) {
	tests := []struct {
		replicas int
		nodes    []string
		keys     []string
	}{
		{
			replicas: 2,
			nodes:    []string{"10.10.10.1", "10.10.10.2", "10.10.10.3"},
			keys:     []string{"abc", "cde", "some random key", "found it"},
		},

		{
			replicas: 4,
			nodes:    []string{"10.10.10.1", "10.10.10.2"},
			keys:     []string{"abc", "cde", "some random key", "found it"},
		},

		{
			replicas: 1,
			nodes:    []string{"10.10.10.1", "10.10.10.2", "10.10.10.3", "10.10.10.4"},
			keys:     []string{"abc", "cde", "some random key", "found it"},
		},
	}

	testContains := func(list []string, item string) bool {
		for _, l := range list {
			if l == item {
				return true
			}
		}

		return false
	}

	for _, c := range tests {
		hr := New(c.replicas, nil)
		for _, n := range c.nodes {
			err := hr.Add(n)
			if err != nil {
				t.Fatalf("unexpected error: %v\n", err)
			}
		}

		for _, k := range c.keys {
			n, err := hr.Locate(k)
			if err != nil {
				t.Fatalf("unexpected error: %v\n", err)
			}

			if !testContains(c.nodes, n) {
				t.Fatalf("unexpected node found: %s\n", n)
			}
		}
	}

}

repo_name: vedhavyas/hashring

repo_language: Go

mime_type: text/x-c


text:
package hashring

import (
	"fmt"
	"hash"
	"hash/fnv"
	"sort"
	"sync"
)

// nodeIdx type implementing Sort Interface
type nodeIdx []uint32

// Len returns the size of nodeIdx
func (idx nodeIdx) Len() int {
	return len(idx)
}

// Swap swaps the ith with jth
func (idx nodeIdx) Swap(i, j int) {
	idx[i], idx[j] = idx[j], idx[i]
}

// Less returns true if ith <= jth else false
func (idx nodeIdx) Less(i, j int) bool {
	return idx[i] <= idx[j]
}

// HashRing to hold the nodes and indexes
type HashRing struct {
	nodes        map[uint32]string // map to idx -> node
	idx          nodeIdx           // sorted indexes
	replicaCount int               // replicas to be inserted
	hash         hash.Hash32
	mu           sync.RWMutex // to protect above fields
}

// New returns a Hash ring with provided virtual node count and hash
// If hash is nil, fvn32a is used instead
func New(replicaCount int, hash hash.Hash32) *HashRing {
	if hash == nil {
		hash = fnv.New32a()
	}

	return &HashRing{
		nodes:        make(map[uint32]string),
		replicaCount: replicaCount,
		hash:         hash,
	}
}

// getHash returns uint32 hash
func getHash(hash hash.Hash32, key []byte) (uint32, error) {
	hash.Reset()
	_, err := hash.Write(key)
	if err != nil {
		return 0, err
	}

	return hash.Sum32(), nil
}

// Add adds a node to Hash ring
func (hr *HashRing) Add(node string) error {
	hr.mu.Lock()
	defer hr.mu.Unlock()

	for i := 0; i < hr.replicaCount; i++ {
		key := fmt.Sprintf("%s:%d", node, i)
		hkey, err := getHash(hr.hash, []byte(key))
		if err != nil {
			return fmt.Errorf("failed to add node: %v", err)
		}

		hr.idx = append(hr.idx, hkey)
		hr.nodes[hkey] = node
	}

	sort.Sort(hr.idx)
	return nil
}

// getKeys returns the keys of map m
func getKeys(m map[uint32]string) (idx nodeIdx) {
	for k := range m {
		idx = append(idx, k)
	}

	return idx
}

// Delete deletes the nodes from hash ring
func (hr *HashRing) Delete(node string) error {
	hr.mu.Lock()
	defer hr.mu.Unlock()

	for i := 0; i < hr.replicaCount; i++ {
		key := fmt.Sprintf("%s:%d", node, i)
		hkey, err := getHash(hr.hash, []byte(key))
		if err != nil {
			return fmt.Errorf("failed to delete node: %v", err)
		}

		delete(hr.nodes, hkey)
	}

	hr.idx = getKeys(hr.nodes)
	sort.Sort(hr.idx)
	return nil
}

// Locate returns the node for a given key
func (hr *HashRing) Locate(key string) (node string, err error) {
	hr.mu.RLock()
	defer hr.mu.RUnlock()

	if len(hr.idx) < 1 {
		return node, fmt.Errorf("no available nodes")
	}

	hkey, err := getHash(hr.hash, []byte(key))
	if err != nil {
		return node, fmt.Errorf("failed to fetch node: %v", err)
	}

	pos := sort.Search(len(hr.idx), func(i int) bool { return hr.idx[i] >= hkey })
	if pos == len(hr.idx) {
		return hr.nodes[hr.idx[0]], nil
	}
	return hr.nodes[hr.idx[pos]], nil
}


text:
# Consistent Hashing
Consistent hashing is a special kind of hashing such that when a hash table is resized, only K/n keys need to be remapped on average, where K is the number of keys, and n is the number of slots.
This academic paper from 1997 introduced the term "consistent hashing" as a way of distributing requests among a changing population of Web servers. Each slot is then represented by a node in a distributed system. The addition (joins) and removal (leaves/failures) of nodes only requires K/n items to be re-shuffled when the number of slots/nodes change

## Usage

`import "github.com/vedhavyas/hashring"`

#### type HashRing

```go
type HashRing struct {
}
```

HashRing to hold the nodes and indexes

#### func  New

```go
func New(replicaCount int, hash hash.Hash32) *HashRing
```
New returns a Hash ring with provided virtual node count and hash If hash is
nil, fvn32a is used instead

#### func (*HashRing) Add

```go
func (hr *HashRing) Add(node string) error
```
Add adds a node to Hash ring

#### func (*HashRing) Delete

```go
func (hr *HashRing) Delete(node string) error
```
Delete deletes the nodes from hash ring

#### func (*HashRing) Locate

```go
func (hr *HashRing) Get(key string) (node string, err error)
```
Locate returns the node for a given key


text:
/*
Package to test the distribution of keys across nodes
*/

package main

import (
	"bufio"
	"flag"
	"fmt"
	"log"
	"os"

	"github.com/spaolacci/murmur3"
	"github.com/vedhavyas/hashring"
)

var (
	replicaCount = flag.Int("rc", 4, "replication count")
	nodeCount    = flag.Int("nc", 8, "node count")
	keyCount     = flag.Int("kc", 6000000, "key count")
)

func main() {
	flag.Parse()
	hr := hashring.New(*replicaCount, murmur3.New32())
	nodeMap := make(map[string]int)
	for i := 0; i < *nodeCount; i++ {
		err := hr.Add(fmt.Sprintf("node-%d", i))
		if err != nil {
			log.Fatal(err)
		}
	}

	fd, _ := os.Open("/usr/share/dict/words")
	defer fd.Close()

	scanner := bufio.NewScanner(fd)
	for i := 0; i < *keyCount; i++ {
		ok := scanner.Scan()
		if !ok {
			break
		}

		n, err := hr.Locate(scanner.Text())
		if err != nil {
			log.Fatal(err)
		}

		nodeMap[n]++
	}

	if scanner.Err() != nil {
		log.Fatal(scanner.Err())
	}

	for k, v := range nodeMap {
		fmt.Printf("%s: %d\n", k, v)
	}
}


text:
# v0.5.1

- Remove `engines` field from `package.json`
- Security updates for dependencies

# v0.5.0

- Add SR and SF bullets
- Update SIR bullet color and text size
- Recompress PNGs

# v0.4.1

- Fix relative sizes of H and T bullets

# v0.4.0

- **NOTE:** Package has been renamed to `mta-subway-bullets`.
- Add 6 and 7 diamond bullets
- Compress all PNGs

# v0.3.3

- Reassigned all dependencies to `devDependencies` so the package install footprint is smaller
- Add build script to `npm start` command
- Explicitly set `files` array in `package.json`

# v0.3.2

- Update `gulp@4.0.0` to resolve security vulnerabilities
- Can now be installed with `npm install`

# v0.3.1

- Update dependencies

# v0.3.0

- Add H bullet (post-Hurricane Sandy Rockaway Park shuttle)
- Add T bullet (proposed Second Avenue line). Background color from Illustrator file provided by Edward Cheng and MTAHQ.

# v0.2.0

- Add W bullet.

repo_name: louh/mta-subway-bullets

repo_language: JavaScript


text:
mta-subway-bullets
==================

SVG icons based on current New York City MTA subway lines.

## Some notes

* Uses the [official MTA colors](http://web.mta.info/developers/resources/line_colors.htm).
* __Notes:__
  * There's no official color for the SIR (Staten Island Railway) line; this now uses the latest color on the new MTA website.
  * There's no official color for the planned T (Second Avenue) line; this color was adapted from internal MTA sources.
* A good resource for SVG icons exist in the public domain on [Wikipedia](http://commons.wikimedia.org/wiki/New_York_City_Subway_bullets), which includes historical lines. I referred to these for guidance, but created my own from scratch. In comparison, I increased the X-height of each letter in relationship to the circle in my version. This allows each bullet to "read" better when used as part of inline text, such as in MTA status alerts.
* Because of the shape of the diamond bullets, they seem "smaller" when used side by side with the circle bullets. I recommend increasing the size of the diamond bullets by about 15-20% to make the numbers more legible.

### Examples of usage in the wild

* The [MTA Weekender Unofficial HD Remaster](https://weekender.netlify.com/), my update of the [MTA's Weekender](http://web.mta.info/weekender/servicestatus.html). ([GitHub](https://github.com/louh/weekender))
* [Signal sticker pack](https://signal.art/addstickers/#pack_id=14994fdf754d54904483a50523e4cfe2&pack_key=a607d7b07c3ca8626ea5f6dd51eefb8e7e9f05829d966aa496ea737ec04b525a) by [Parker Higgens](https://twitter.com/xor/status/1223413178253697025).

## Technocery

You can install this in a Node project with a package manager, e.g.

```sh
npm install mta-subway-bullets
```

or

```sh
yarn add mta-subway-bullets
```

### To convert SVG to PNG

Get `librsvg` for your system, e.g. (Mac OSX instructions follow)

```sh
brew install librsvg
```

Then:

```sh
for svg in ./svg/*.svg; do rsvg-convert -o "${svg%.svg}".png "$svg"; done
```

For convenience, a set of optimized PNGs (90px ✕ 90px) is included in the `./build/png` directory.

## License

This repository is not the official work of the MTA and I have no affiliation with the MTA. I claim no ownership or rights over this work.

[CC0](http://creativecommons.org/publicdomain/zero/1.0/).


text:
'use strict'

var gulp = require('gulp')
var svgSprite = require('gulp-svg-sprite')
var config = {
  mode: {
    shape: {
      padding: 10
    },
    symbol: {
      dest: '.',
      sprite: 'icons',
      prefix: 'nyc-subway-%s'
    }
  }
}

gulp.task('default', function () {
  return gulp.src('svg/*.svg')
    .pipe(svgSprite(config))
    .pipe(gulp.dest('build'))
})


text:
# These are supported funding model platforms

github: [louh]
patreon: biffud
open_collective: # Replace with a single Open Collective username
ko_fi: louhuang
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


text:
source "https://rubygems.org"

gemspec

gem 'rdf',                git: "https://github.com/ruby-rdf/rdf",                 branch: "develop"
gem 'rdf-aggregate-repo', git: "https://github.com/ruby-rdf/rdf-aggregate-repo",  branch: "develop"
gem 'sparql',             git: "https://github.com/ruby-rdf/sparql",              branch: "develop"
#gem "nokogiri",           '~> 1.8'

group :development, :test do
  gem 'ebnf',           git: "https://github.com/dryruby/ebnf",                 branch: "develop"
  gem 'rdf-isomorphic', git: "https://github.com/ruby-rdf/rdf-isomorphic",      branch: "develop"
  gem 'rdf-spec',       git: "https://github.com/ruby-rdf/rdf-spec",            branch: "develop"
  gem 'rdf-turtle',     git: "https://github.com/ruby-rdf/rdf-turtle",          branch: "develop"
  gem "rdf-xsd",        git: "https://github.com/ruby-rdf/rdf-xsd",             branch: "develop"
  gem 'sxp',            git: "https://github.com/dryruby/sxp.rb",               branch: "develop"
  gem "redcarpet",      platform: :ruby
  gem 'simplecov',      platforms: :mri
  gem 'coveralls',      '~> 0.8', platforms: :mri
end

group :debug do
  gem 'shotgun'
  gem "byebug", platforms: :mri
end

repo_name: ruby-rdf/sparql-client

repo_language: Ruby


text:
#!/usr/bin/env ruby
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), 'lib')))
require 'rubygems'
begin
  require 'rakefile' # @see https://github.com/artob/rakefile
rescue LoadError => e
end

namespace :gem do
  desc "Build the sparql-client-#{File.read('VERSION').chomp}.gem file"
  task :build do
    sh "gem build sparql-client.gemspec && mv sparql-client-#{File.read('VERSION').chomp}.gem pkg/"
  end

  desc "Release the sparql-client-#{File.read('VERSION').chomp}.gem file"
  task :release do
    sh "gem push pkg/sparql-client-#{File.read('VERSION').chomp}.gem"
  end
end


text:
# How to contribute

Community contributions are essential for keeping Ruby RDF great. We want to keep it as easy as possible to contribute changes that get things working in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things.

## Development

This repository uses [Git Flow](https://github.com/nvie/gitflow) to manage development and release activity. All submissions _must_ be on a feature branch based on the _develop_ branch to ease staging and integration.

* create or respond to an issue on the [Github Repository](https://github.com/ruby-rdf/sparql-client/issues)
* Fork and clone the repo:
  `git clone git@github.com:your-username/sparql-client.git`
* Install bundle:
  `bundle install`
* Create tests in RSpec and make sure you achieve at least 90% code coverage for the feature your adding or behavior being modified.
* Push to your fork and [submit a pull request][pr].

## Do's and Dont's
* Do your best to adhere to the existing coding conventions and idioms.
* Don't use hard tabs, and don't leave trailing whitespace on any line.
  Before committing, run `git diff --check` to make sure of this.
* Do document every method you add using [YARD][] annotations. Read the
  [tutorial][YARD-GS] or just look at the existing code for examples.
* Don't touch the `.gemspec` or `VERSION` files. If you need to change them,
  do so on your private branch only.
* Do feel free to add yourself to the `CREDITS` file and the
  corresponding list in the the `README`. Alphabetical order applies.
* Don't touch the `AUTHORS` file. If your contributions are significant
  enough, be assured we will eventually add you in there.
* Do note that in order for us to merge any non-trivial changes (as a rule
  of thumb, additions larger than about 15 lines of code), we need an
  explicit [public domain dedication][PDD] on record from you,
  which you will be asked to agree to on the first commit to a repo within the organization.
  Note that the agreement applies to all repos in the [Ruby RDF](https://github.com/ruby-rdf/) organization.

[YARD]:    https://yardoc.org/
[YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md
[PDD]:              https://unlicense.org/#unlicensing-contributions
[pr]:      https://github.com/ruby-rdf/rdf/compare/


text:
* Christoph Badura <bad@bsd.de>
* Thomas Feron <tho.feron@gmail.com>
* James Hetherington <jamespjh@googlemail.com>
* Gabriel Horner <gabriel.horner@gmail.com>
* Nicholas Humfrey <njh@aelius.com>
* Fumihiro Kato <fumi@fumi.me>
* David Nielsen <david@graveyard.dk>
* Thamaraiselvan Poomalai <p.thamarai@gmail.com>
* Michael Sokol <mikaa123@gmail.com>
* Yves Raimond <yves.raimond@bbc.co.uk>
* Danny Tran <dannybtran@gmail.com>
* Nick Gottlieb <ngottlieb@gmail.com>


text:
* Arto Bendiken <arto@bendiken.net>
* Ben Lavender <blavender@gmail.com>
* Gregg Kellogg <gregg@greggkellogg.net>


text:
source "https://rubygems.org"

gemspec

gem 'rdf',                git: "https://github.com/ruby-rdf/rdf",                 branch: "develop"
gem 'rdf-aggregate-repo', git: "https://github.com/ruby-rdf/rdf-aggregate-repo",  branch: "develop"
gem 'sparql',             git: "https://github.com/ruby-rdf/sparql",              branch: "develop"
gem "nokogiri",           '~> 1.13', '>= 1.13.4'

group :development, :test do
  gem 'ebnf',           git: "https://github.com/dryruby/ebnf",                 branch: "develop"
  gem 'rdf-isomorphic', git: "https://github.com/ruby-rdf/rdf-isomorphic",      branch: "develop"
  gem 'rdf-spec',       git: "https://github.com/ruby-rdf/rdf-spec",            branch: "develop"
  gem 'rdf-turtle',     git: "https://github.com/ruby-rdf/rdf-turtle",          branch: "develop"
  gem "rdf-xsd",        git: "https://github.com/ruby-rdf/rdf-xsd",             branch: "develop"
  gem 'sxp',            git: "https://github.com/dryruby/sxp.rb",               branch: "develop"
  gem "redcarpet",      platform: :ruby
  gem 'simplecov',      '~> 0.21',  platforms: :mri
  gem 'simplecov-lcov', '~> 0.8',  platforms: :mri
end

group :debug do
  gem 'shotgun'
  gem "byebug", platforms: :mri
end


text:
#!/usr/bin/env ruby -rubygems
# -*- encoding: utf-8 -*-

Gem::Specification.new do |gem|
  gem.version            = File.read('VERSION').chomp
  gem.date               = File.mtime('VERSION').strftime('%Y-%m-%d')

  gem.name               = 'sparql-client'
  gem.homepage           = 'https://github.com/ruby-rdf/sparql-client'
  gem.license            = 'Unlicense'
  gem.summary            = 'SPARQL client for RDF.rb.'
  gem.description        = %(Executes SPARQL queries and updates against a remote SPARQL 1.0 or 1.1 endpoint,
                            or against a local repository. Generates SPARQL queries using a simple DSL.
                            Includes SPARQL::Client::Repository, which allows any endpoint supporting
                            SPARQL Update to be used as an RDF.rb repository.)
  gem.metadata           = {
    "documentation_uri" => "https://ruby-rdf.github.io/sparql-client",
    "bug_tracker_uri"   => "https://github.com/ruby-rdf/sparql-client/issues",
    "homepage_uri"      => "https://github.com/ruby-rdf/sparql-client",
    "mailing_list_uri"  => "https://lists.w3.org/Archives/Public/public-rdf-ruby/",
    "source_code_uri"   => "https://github.com/ruby-rdf/sparql-client",
  }

  gem.authors            = ['Arto Bendiken', 'Ben Lavender', 'Gregg Kellogg']
  gem.email              = 'public-rdf-ruby@w3.org'

  gem.platform           = Gem::Platform::RUBY
  gem.files              = %w(AUTHORS CREDITS README.md UNLICENSE VERSION) + Dir.glob('lib/**/*.rb')
  gem.bindir             = %q(bin)
  gem.require_paths      = %w(lib)

  gem.required_ruby_version      = '>= 2.6'
  gem.requirements               = []
  gem.add_runtime_dependency     'rdf',       '~> 3.2', '>= 3.2.11'
  gem.add_runtime_dependency     'net-http-persistent', '~> 4.0', '>= 4.0.2'
  gem.add_development_dependency 'rdf-spec',  '~> 3.2'
  gem.add_development_dependency 'sparql',    '~> 3.2'
  gem.add_development_dependency 'rspec',     '~> 3.12'
  gem.add_development_dependency 'rspec-its', '~> 1.3'
  gem.add_development_dependency 'webmock',   '~> 3.14'
  gem.add_development_dependency 'yard' ,     '~> 0.9'

  gem.post_install_message       = nil
end


text:
platform:
  Rubygems:
    rdf-isomorphic:
      tests:
        unmaintained: skip