Skip to content

Commit

Permalink
Support adding nixsa/bin to PATH
Browse files Browse the repository at this point in the history
  • Loading branch information
noamraph committed Aug 22, 2024
1 parent 50f2902 commit 8bc0d28
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 76 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ wheels/
/nixsa-bin/target
*.tar.xz
/nixsa
check-tarball-*
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Important note: Nixsa currently uses a patched version of Nix, which supports en

One more feature: run `nixsa/bin/nixsa` to start a shell where `/nix` is binded to `nixsa/nix`, and where all the commands in the Nix profile (in `nixsa/state/profile`) are in the PATH.

Tip: You can add the `nixsa/bin` folder to your `$PATH`, to have the installed Nix packages readily available.

## How does it work?

Nixsa uses [Bubblewrap](https://github.com/containers/bubblewrap), a sandboxing tool, to run the commands in an environment where `/nix` is binded to the `nix` subfolder of the `nixsa` folder.
Expand Down
52 changes: 34 additions & 18 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nixsa-bin/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nixsa-bin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nixsa"
version = "0.1.1"
version = "0.1.2-pre"
edition = "2021"

[dependencies]
Expand Down
95 changes: 40 additions & 55 deletions nixsa-bin/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::{bail, Result};
use camino::{absolute_utf8, Utf8Path, Utf8PathBuf};
use anyhow::{bail, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use libc::{signal, SIGINT, SIG_IGN};
use shell_quote::{Bash, QuoteRefExt};
use std::collections::HashSet;
Expand All @@ -11,31 +11,35 @@ use tracing_subscriber::FmtSubscriber;

const DESCRIPTION: &str = "\
Usage:
nixsa [-h] [-s] [-v] [cmd [arg [arg ...]]
nixsa [options] [cmd [arg [arg ...]]
Run a command in the nixsa (Nix Standalone) environment.
Run a command in the Nixsa (Nix Standalone) environment.
Assuming $NIXSA is the nixsa folder, meaning $NIXSA/nixsa.toml exists, will use
bwrap to run the command with $NIXSA/nix binded to /nix.
Assuming NIXSA is the Nixsa folder, meaning NIXSA/nixsa.toml exists, will use
bwrap to run the command with NIXSA/nix binded to /nix.
argv[0] will be resolved until dirname(dirname(path)) contains a file called
`nixsa.toml`. Then, if basename(path) is not 'nixsa', basename(path) will be
used as the command. So, if $NIXSA/bin/nix is a symlink to `nixsa`, running
`$NIXSA/bin/nix --help` is the same as running `$NIXSA/bin/nixsa nix --help`.
The Nixsa folder is found by using /proc/self/exe to find the canonical path
of the nixsa executable, and going upwards until a directory which contains
`nixsa.toml` is found.
If no arguments are given, and basename(path) is 'nixsa', $SHELL will be used
If basename(argv[0]) is not 'nixsa', meaning that we run by a symlink,
basename(argv[0]) will be used as the command, and no argument parsing is done.
So, if NIXSA/bin/nix is a symlink to `nixsa`, running `NIXSA/bin/nix --help`
is the same as running `NIXSA/bin/nixsa nix --help`.
If no arguments are given, and basename(argv[0]) is 'nixsa', $SHELL will be used
as the command.
After running the command, the entries in the $NIXSA/bin directories will be
After running the command, the entries in the NIXSA/bin directories will be
updated with symlinks to `nixsa` according to the entries in
$NIXSA/state/profile/bin. This will only be done if $NIXSA/bin was modified
before $NIXSA/state/profile, so the update will be skipped if the profile
NIXSA/state/profile/bin. This will only be done if NIXSA/bin was modified
before NIXSA/state/profile, so the update will be skipped if the profile
wasn't updated.
options:
-h, --help show this help message and exit
-s, --symlinks update symlinks, regardless of modification time, and exit.
-v, --verbose show the commands which are run
Options:
-h, --help show this help message and exit.
-s, --symlinks update symlinks, regardless of modification time, and exit.
-v, --verbose show the commands which are run.
";

fn verify_bwrap() -> Result<()> {
Expand Down Expand Up @@ -229,60 +233,41 @@ fn nixsa(basepath: &Utf8Path, cmd: &str, args: &[String]) -> Result<ExitCode> {
Ok(ExitCode::from(code))
}

/// Get the nixsa root dir, and the symlink name, if path is in DIR/bin and DIR/nixsa.toml exists.
fn get_nixsa_root_and_name_if_in_bin(path: &Utf8Path) -> Option<(Utf8PathBuf, String)> {
let name = path.file_name();
if let Some(name) = name {
let parent = path.parent();
if let Some(parent) = parent {
if parent.file_name() == Some("bin") {
let parent2 = parent.parent();
if let Some(parent2) = parent2 {
if parent2.join("nixsa.toml").exists() {
return Some((parent2.to_owned(), name.to_owned()));
}
fn find_nixsa_root(path: &Utf8Path) -> Result<Option<Utf8PathBuf>> {
let mut path = path;
loop {
match path.parent() {
None => return Ok(None),
Some(p) => {
path = p;
if path.join("nixsa.toml").try_exists()? {
return Ok(Some(path.into()));
}
}
}
}
None
}

// Resolve symlinks until a path which is in NIXSA/bin is found, return NIXSA and the symlink name
fn find_root_and_name(path: &Utf8Path) -> Result<(Utf8PathBuf, String)> {
let mut path = absolute_utf8(path)?;
if !path.exists() {
bail!("{} doesn't refer to a valid file", path);
}
loop {
if let Some((nixsa_root, name)) = get_nixsa_root_and_name_if_in_bin(&path) {
return Ok((nixsa_root, name));
}
if path.is_symlink() {
path = path.parent().expect("Expecting a parent").join(path.read_link_utf8()?);
} else {
bail!("{} isn't inside a NIXSA/bin directory", path);
}
}
}
enum ParsedArgs {
Help,
Symlinks { basepath: Utf8PathBuf },
Run { basepath: Utf8PathBuf, cmd: String, args: Vec<String>, verbose: bool },
}

fn parse_args(args: Vec<String>) -> Result<ParsedArgs> {
let argv0 = Utf8PathBuf::from(args[0].clone());
let root_and_name = find_root_and_name(&argv0);
match root_and_name {
Err(e) => {
let proc_self_exe: &Utf8Path = "/proc/self/exe".into();
let exe_realpath = proc_self_exe.read_link_utf8()?;
let root = find_nixsa_root(&exe_realpath)?;
let name = <&Utf8Path>::from(args[0].as_str()).file_name().context("Expecting argv[0] to have a final element")?;
match root {
None => {
if args.len() > 1 && (args[1] == "-h" || args[1] == "--help") {
Ok(ParsedArgs::Help)
} else {
Err(e)
bail!("Couldn't find a directory containing {} which contains a `nixsa.toml` file.", proc_self_exe);
}
}
Ok((basepath, name)) => {
Some(basepath) => {
let nixpath = basepath.join("nix");
if !nixpath.is_dir() {
bail!("{:?} doesn't exist or is not a directory", nixpath);
Expand All @@ -292,7 +277,7 @@ fn parse_args(args: Vec<String>) -> Result<ParsedArgs> {
bail!("{:?} is not a symlink", profile_path);
}
if name != "nixsa" {
Ok(ParsedArgs::Run { basepath, cmd: name, args: args[1..].into(), verbose: false })
Ok(ParsedArgs::Run { basepath, cmd: name.into(), args: args[1..].into(), verbose: false })
} else {
if args.len() > 1 && (args[1] == "-h" || args[1] == "--help") {
return Ok(ParsedArgs::Help);
Expand Down
9 changes: 8 additions & 1 deletion nixsa-build/check_tarball.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import sys
from pathlib import Path
from subprocess import check_call, check_output
from subprocess import PIPE, check_call, check_output, run
from tempfile import mkdtemp


Expand All @@ -19,13 +19,20 @@ def sh_output(args: str) -> str:


def check_tarball(tarball: Path) -> None:
# Untar into tmpdir
tmpdir = Path(mkdtemp(prefix='check-tarball-', dir='.'))
sh(f'tar -C {tmpdir} -xf {tarball}')
(out,) = list(tmpdir.iterdir())
# Install `hello` and make sure it works when called directly
assert not (out / 'bin/hello').exists()
sh(f"{out}/bin/nix profile install 'nixpkgs#hello'")
hello_out = sh_output(f'{out}/bin/hello')
assert hello_out.strip() == 'Hello, world!'
# Make sure `hello` can be called when adding nixsa/bin to PATH
status = run('hello', shell=True, check=False, stderr=PIPE)
assert status.returncode != 0, 'Expecting the `hello` command not to be installed'
hello_out2 = sh_output(f'export PATH={out}/bin:$PATH && hello')
assert hello_out2.strip() == 'Hello, world!'
sh(f'{out}/bin/nix profile remove hello')
assert not (out / 'bin/hello').exists()

Expand Down

0 comments on commit 8bc0d28

Please sign in to comment.