on c7f802a help sign in
README.md+26−35
since splitsnapshots
unifiedsplit
@@ -5,10 +5,8 @@ A Nix flake that builds an **OCI image** (via
5Apple's [`container`](https://github.com/apple/container) CLI on macOS.
5Apple's [`container`](https://github.com/apple/container) CLI on macOS.
6
6
7It's a plain, single-process image — no NixOS, no systemd — defined in
7It's a plain, single-process image — no NixOS, no systemd — defined in
8−[`flake.nix`](./flake.nix). `c start` drops you into a
8+[`flake.nix`](./flake.nix). It runs `fish` by default (with a configured
9−[`herdr`](https://herdr.dev) session whose panes use your host login shell
10−(detected and bundled by `c init`). You choose what else it ships via a
11−per-project `container.nix`.
9+prompt); you choose what else it ships via a per-project `container.nix`.
12
10
13## Prerequisites
11## Prerequisites
14
12
@@ -27,31 +25,24 @@ per-project `container.nix`.
27
25
28## Packages
26## Packages
29
27
30−`init` builds the image from a **`container.nix`** in the current directory — an
28+`init` builds the image from a **`container.nix`** in the current directory — a
31−attrset with a `packages` function (`{ pkgs, nur }` returning the package list;
29+`{ pkgs, nur }` function returning the package list (`nur` is
32−`nur` is [NUR](https://github.com/nix-community/NUR), for packages outside
30+[NUR](https://github.com/nix-community/NUR), for packages outside nixpkgs). It
33−nixpkgs) and an optional `mounts` list. It is **required**; there is no default.
31+is **required**; there is no default set.
34
32
35```nix
33```nix
36# ./container.nix
34# ./container.nix
37−{
38− packages = { pkgs, nur }: with pkgs; [
35+{ pkgs, nur }: with pkgs; [
36+ coreutils
39− git
37+ git
40− go
38+ go
41− nur.repos.charmbracelet.crush
39+ nur.repos.charmbracelet.crush
42− ];
43− # Extra "host:container" bind mounts, on top of $PWD → /workspace. A leading
44− # ~/ in the host path expands to $HOME. Optional.
45− mounts = [
46− "~/data:/data"
47− ];
40+]
48−}
49```
41```
50
42
51−Your **host login shell** (detected by `c init`, bash fallback), **coreutils**,
43+**fish** (the default command, with a configured prompt) and **Nix** itself
52−[**herdr**](https://herdr.dev) (the session `c start` launches), **Nix** itself
53−(with CA certs and `nix-command`/`flakes` enabled, single-user), and the ncurses
44+(with CA certs and `nix-command`/`flakes` enabled, single-user) are always
54−**terminfo** database are always included — no need to list them.
45+included — no need to list them.
55
46
56Point at a different file with `-c`/`--config`:
47Point at a different file with `-c`/`--config`:
57
48
@@ -68,17 +59,18 @@ that holds a `container.nix`; the container is named after that directory.
68# build the image, load it, and create the container (named after $PWD):
59# build the image, load it, and create the container (named after $PWD):
69nix run github:ilyakooo0/nix-container -- init
60nix run github:ilyakooo0/nix-container -- init
70
61
71−# start it and attach — drops you into a herdr session:
62+# start it and attach — drops you into fish:
72nix run github:ilyakooo0/nix-container -- start
63nix run github:ilyakooo0/nix-container -- start
73```
64```
74
65
75- `init` builds the OCI archive in a temp dir, loads it into `container`,
66- `init` builds the OCI archive in a temp dir, loads it into `container`,
76 deletes the temp file, then (re)creates the container. It mounts the current
67 deletes the temp file, then (re)creates the container. It mounts the current
77− directory at `/workspace` (the container's working directory).
68+ directory at `/workspace` (the container's working directory) and your
69+ `~/.config` at `/root/.config` (fish runs with `--no-config`, so this won't
70+ override its bundled prompt; other tools still read it).
78- `--ssh` forwards your host SSH agent socket into the container (so `git` over
71- `--ssh` forwards your host SSH agent socket into the container (so `git` over
79 SSH works with your keys).
72 SSH works with your keys).
80−- Add more mounts persistently via `mounts` in `container.nix` (above), or
81− ad-hoc by passing them through to `create` (mounts can only be set at creation):
73+- Add more mounts by passing them through (mounts can only be set at creation):
82
74
83 ```sh
75 ```sh
84 nix run github:ilyakooo0/nix-container -- init -v $HOME/data:/data
76 nix run github:ilyakooo0/nix-container -- init -v $HOME/data:/data
@@ -99,19 +91,18 @@ nix run github:ilyakooo0/nix-container -- init
99## Customize further
91## Customize further
100
92
101Fork or clone to change what isn't per-project — `config.cmd`/`env` or the image
93Fork or clone to change what isn't per-project — `config.cmd`/`env` or the image
102−name — in [`flake.nix`](./flake.nix). The flake also exposes build helpers:
94+name — in [`flake.nix`](./flake.nix). The flake also exposes build helpers, both
95+taking a `{ pkgs, nur }: [ ... ]` function:
103
96
97+- `lib.<system>.mkImage` → an OCI image.
104−- `lib.<system>.copyWith` → a skopeo copy app for a `{ pkgs, nur }: [ ... ]`
98+- `lib.<system>.copyWith` → a skopeo copy app, for building/pushing the image
105− function, for building/pushing the image anywhere skopeo can write (e.g. a
99+ anywhere skopeo can write (e.g. a registry):
106− registry):
107
100
108 ```sh
101 ```sh
109 drv=$(nix build --no-link --print-out-paths --impure --expr \
102 drv=$(nix build --no-link --print-out-paths --impure --expr \
110− '(builtins.getFlake "github:ilyakooo0/nix-container").lib.${builtins.currentSystem}.copyWith (import ./container.nix).packages')
103+ '(builtins.getFlake "github:ilyakooo0/nix-container").lib.${builtins.currentSystem}.copyWith (import ./container.nix)')
111 $drv/bin/copy-to docker://ghcr.io/me/img:latest
104 $drv/bin/copy-to docker://ghcr.io/me/img:latest
112 ```
105 ```
113−- `lib.<system>.mkImage <name> <shell>` → the underlying OCI image (`copyWith`
114− defaults these to `nix-container` / `bash`).
115
106
116The flake uses `flake-utils` over `{aarch64,x86_64}-{darwin,linux}`; the host
107The flake uses `flake-utils` over `{aarch64,x86_64}-{darwin,linux}`; the host
117system maps to the matching Linux target automatically (an Intel Mac builds an
108system maps to the matching Linux target automatically (an Intel Mac builds an
c+13−43
@@ -1,7 +1,7 @@
1#!/usr/bin/env fish
1#!/usr/bin/env fish
2# Manage a container named after the current directory.
2# Manage a container named after the current directory.
3# c init [-c FILE] [extra create args] build + load the image, then (re)create it
3# c init [-c FILE] [extra create args] build + load the image, then (re)create it
4−# (config from FILE, default ./container.nix)
4+# (packages from FILE, default ./container.nix)
5# c start start it and attach
5# c start start it and attach
6
6
7set -l name (path basename $PWD)
7set -l name (path basename $PWD)
@@ -22,23 +22,17 @@ switch $argv[1]
22 end
22 end
23
23
24 if not test -e $config
24 if not test -e $config
25− echo "c: config file not found: $config" >&2
25+ echo "c: package file not found: $config" >&2
26− echo " create one ('{ packages = { pkgs, nur }: [ ... ]; mounts = [ ]; }') or pass -c FILE" >&2
26+ echo " create one ('{ pkgs, nur }: [ ... ]') or pass -c FILE" >&2
27 exit 1
27 exit 1
28 end
28 end
29
29
30− # Host login shell (by name): added to the image automatically and baked
31− # in as the cmd + default $SHELL (what herdr panes etc. spawn), so it
32− # needn't be listed in container.nix. Falls back to bash.
33− set -l shell bash
34− test -n "$SHELL"; and set shell (path basename $SHELL)
35−
36− # Build an image with the config's `packages` (plus the shell) into a
30+ # Build an image with the packages from $config into a temp dir, load
37− # temp dir, load it, then clean up.
31+ # it, then clean up.
38 set -l tmp (mktemp -d)
32 set -l tmp (mktemp -d)
39 set -l archive $tmp/image.tar.gz
33 set -l archive $tmp/image.tar.gz
40− set -lx NIX_CONTAINER_CONFIG (path resolve $config)
34+ set -lx NIX_CONTAINER_PKGS (path resolve $config)
41− set -l expr "(builtins.getFlake \"$flake\").lib.\${builtins.currentSystem}.copyWithShell \"nix-container-$name\" \"$shell\" (import (/. + builtins.getEnv \"NIX_CONTAINER_CONFIG\")).packages"
35+ set -l expr "(builtins.getFlake \"$flake\").lib.\${builtins.currentSystem}.copyWith (import (/. + builtins.getEnv \"NIX_CONTAINER_PKGS\"))"
42 set -l copyer (nix build --impure --no-link --print-out-paths --expr "$expr")
36 set -l copyer (nix build --impure --no-link --print-out-paths --expr "$expr")
43 set -l rc $status
37 set -l rc $status
44 if test $rc -eq 0
38 if test $rc -eq 0
@@ -53,40 +47,16 @@ switch $argv[1]
53 rm -rf $tmp
47 rm -rf $tmp
54 test $rc -eq 0; or exit $rc
48 test $rc -eq 0; or exit $rc
55
49
56− # Mount the project at /workspace (the default cwd), plus any extra
50+ # Mount the project at /workspace (the default cwd) and host ~/.config.
57− # mounts from the config (`mounts = [ "host:container" … ]`). A leading
51+ # (fish runs with --no-config, so the mount can't shadow the prompt.)
58− # ~/ in the host path expands to $HOME (nix leaves it literal).
59 set -l mounts -v "$PWD:/workspace"
52 set -l mounts -v "$PWD:/workspace"
60− for m in (nix eval --impure --raw --expr "builtins.concatStringsSep \"\n\" ((import (/. + builtins.getEnv \"NIX_CONTAINER_CONFIG\")).mounts or [])")
53+ test -d $HOME/.config; and set -a mounts -v "$HOME/.config:/root/.config"
61− set -a mounts -v (string replace -r '^~/' "$HOME/" -- $m)
62− end
63−
64− # TERM to bake into the container env (`-e TERM`): the runtime hands the
65− # `start -ai` session a bare "xterm", so set the host's real terminal,
66− # mapped to an entry the bundled ncurses ships — Ghostty reports
67− # "xterm-ghostty", which ncurses ships as "ghostty" — falling back to
68− # xterm-256color for an empty or bare-"xterm" value.
69− set -l term xterm-256color
70− if test "$TERM" = xterm-ghostty
71− set term ghostty
72− else if test -n "$TERM"; and test "$TERM" != xterm
73− set term $TERM
74− end
75
54
76− # Replace any existing container of the same name, then create it running
55+ # Replace any existing container of the same name. The image's cmd is
77− # herdr in a session named after the project (`<dir>-container`). This
56+ # /bin/fish, so no command is passed.
78− # overrides the image's default plain-herdr cmd; `c start` attaches to it.
79 container rm --force $name 2>/dev/null
57 container rm --force $name 2>/dev/null
80− # The image only lays down /bin and /etc, so /tmp and /run don't exist;
81− # without them anything that writes temp files (herdr, build tools, …)
82− # fails with ENOENT. Mount writable tmpfs at both.
83− container create --name $name --ssh -it -e "TERM=$term" \
58+ container create --name $name --ssh -it --cpus 8 --memory 8g --cwd /workspace $mounts $argv $image
84− --tmpfs /tmp --tmpfs /run --memory 8g --cwd /workspace $mounts $argv \
85− $image /bin/herdr session attach "$name-container"
86 case start
59 case start
87− # Start the container and attach to its cmd (herdr). TERM was baked into
88− # the container env at create time (`-e TERM`), which survives this path.
89− # Exiting herdr (PID1) stops the container; `c start` boots it again.
90 container start -ai $name
60 container start -ai $name
91 case '*'
61 case '*'
92 echo "usage: c init [-c FILE] [create args] | start" >&2
62 echo "usage: c init [-c FILE] [create args] | start" >&2
flake.nix+65−50
@@ -72,22 +72,53 @@
72 # Host package set, for the wrapper app below.
72 # Host package set, for the wrapper app below.
73 pkgsHost = nixpkgs.legacyPackages.${system};
73 pkgsHost = nixpkgs.legacyPackages.${system};
74
74
75− # Default image name (for `copyWith`); `c init` passes a per-directory
75+ # Name/tag baked into the image's OCI config.
76− # name (`nix-container-<dir>`) to `copyWithShell` instead.
77 imageName = "nix-container";
76 imageName = "nix-container";
78 imageTag = "latest";
77 imageTag = "latest";
79
78
79+ # Fish prompt config. Installed at /etc/fish/prompt.fish and loaded via
80+ # `fish --no-config --init-command` (see `cmd` below), NOT from
81+ # $HOME/.config/fish — so the `~/.config` mount can't shadow it. Built
82+ # on the host arch (just a text file), so no Linux builder is needed.
83+ fishConfig = pkgsHost.writeText "prompt.fish" ''
84+ # No welcome banner.
85+ set -g fish_greeting
86+
87+ # Define the prompt as a function so fish renders it before every
88+ # command; at top level it would run once at startup and be ignored.
89+ function fish_prompt
90+ set -l nix_shell_info
91+ if set -q IN_NIX_SHELL; or set -q IN_NIX_RUN
92+ set nix_shell_info ' ❄️'
93+ end
94+
95+ set -l cwd (prompt_pwd)
96+
97+ set -l bookmark_info
98+ if jj root 2>/dev/null >/dev/null
99+ set -l jj_bookmark (jj log -r 'heads(::@ & bookmarks())' -T 'local_bookmarks ++ "\n"' --no-graph 2>/dev/null | tail -1)
100+ if test -n "$jj_bookmark"
101+ set bookmark_info ' ' (set_color brmagenta) $jj_bookmark (set_color normal)
102+ end
103+ end
104+
105+ echo -n -s (set_color $fish_color_cwd) $cwd (set_color normal) $bookmark_info ' 🏗️' $nix_shell_info '> '
106+ end
107+ '';
108+
109+ # Lay the prompt out at the path the cmd sources.
110+ fishRoot = pkgsHost.runCommand "fish-prompt" { } ''
111+ mkdir -p "$out/etc/fish"
112+ cp ${fishConfig} "$out/etc/fish/prompt.fish"
113+ '';
114+
80− # Build the OCI image (named `name`) from a `{ pkgs, nur }: [ ... ]`
115+ # Build the OCI image from a `{ pkgs, nur }: [ ... ]` package function.
81− # package function, for the named shell. The image's `cmd` is herdr
116+ # The image always runs /bin/fish; fish, its prompt config, and Nix are
82− # (the session `c start` attaches to); the shell is the default `$SHELL`
83− # (what herdr panes spawn) and is added automatically, so `container.nix`
117+ # bundled automatically, so `container.nix` lists only your extras.
84− # lists only your extras. Nix (+ CA certs) and the ncurses terminfo
85− # database are always shipped too. Nothing here is fish-specific: the
86− # TERM fixup happens host-side in `c` (passed via `-e TERM` at create).
87 mkImage =
118 mkImage =
88− name: shell: packages:
119+ packages:
89 n2c.buildImage {
120 n2c.buildImage {
90− inherit name;
121+ name = imageName;
91 tag = imageTag;
122 tag = imageTag;
92 inherit arch;
123 inherit arch;
93
124
@@ -95,6 +126,10 @@
95 # buildEnv only *symlinks* paths (never runs them), so build it on
126 # buildEnv only *symlinks* paths (never runs them), so build it on
96 # the host arch — the tree is arch-neutral and links the
127 # the host arch — the tree is arch-neutral and links the
97 # (substituted) Linux store paths, sparing the Linux builder.
128 # (substituted) Linux store paths, sparing the Linux builder.
129+ #
130+ # Always shipped: fish (the cmd) + its prompt, and Nix (+ CA certs)
131+ # so every container can use Nix; `initializeNixDatabase` registers
132+ # the baked store.
98 copyToRoot = pkgsHost.buildEnv {
133 copyToRoot = pkgsHost.buildEnv {
99 name = "root";
134 name = "root";
100 paths =
135 paths =
@@ -102,27 +137,19 @@
102 pkgs = pkgsLinux;
137 pkgs = pkgsLinux;
103 nur = nurPkgs;
138 nur = nurPkgs;
104 }
139 }
105− # The shell (cmd + $SHELL), when it exists in nixpkgs.
106− ++ pkgsLinux.lib.optional (pkgsLinux ? ${shell}) pkgsLinux.${shell}
107 ++ [
140 ++ [
141+ pkgsLinux.fish
142+ fishRoot
108 pkgsLinux.nix
143 pkgsLinux.nix
109 pkgsLinux.cacert
144 pkgsLinux.cacert
110− # Basic userland (ls, cat, cp, sleep, …) so the container is
111− # usable out of the box and tools that shell out don't fail.
112− pkgsLinux.coreutils-full
113 # Networking glue glibc needs: /etc/nsswitch.conf (+passwd,
145 # Networking glue glibc needs: /etc/nsswitch.conf (+passwd,
114 # group) so DNS/host lookups resolve, and /etc/protocols,
146 # group) so DNS/host lookups resolve, and /etc/protocols,
115 # /etc/services. Without these, hostname resolution fails.
147 # /etc/services. Without these, hostname resolution fails.
116 pkgsLinux.fakeNss
148 pkgsLinux.fakeNss
117 pkgsLinux.iana-etc
149 pkgsLinux.iana-etc
118− # The terminfo database (+ tput/clear/reset) so TUIs find the
119− # entry for whatever TERM `c` passes in and render correctly.
120− pkgsLinux.ncurses
121− # The session `c start` always launches.
122− pkgsLinux.herdr
123 ];
150 ];
124 # Link all of /etc so the above files (nsswitch.conf, protocols,
151 # Link all of /etc so the above files (nsswitch.conf, protocols,
125− # services, passwd, group, ssl certs) are present.
152+ # services, passwd, group, ssl certs, fish prompt) are present.
126 pathsToLink = [
153 pathsToLink = [
127 "/bin"
154 "/bin"
128 "/etc"
155 "/etc"
@@ -133,25 +160,20 @@
133
160
134 config = {
161 config = {
135 # `cmd` (not `entrypoint`): the default command, *replaced* by any
162 # `cmd` (not `entrypoint`): the default command, *replaced* by any
136− # command passed to `container run/create`. Plain herdr here;
163+ # command passed to `container run/create`. `--no-config` skips
137− # `c init` overrides it with `herdr session attach <dir>-container` so
164+ # $HOME/.config/fish (so a mounted ~/.config can't override the
138− # the session is named per project. `c start` attaches via
165+ # prompt); `--init-command` loads the bundled prompt instead.
139− # `container start -ai`; exiting herdr (PID1) stops the container.
140− cmd = [ "/bin/herdr" ];
166+ cmd = [
167+ "/bin/fish"
168+ "--no-config"
169+ "--init-command"
170+ "source /etc/fish/prompt.fish"
171+ ];
141 env = [
172 env = [
142 "PATH=/bin"
173 "PATH=/bin"
143 "HOME=/root"
174 "HOME=/root"
144− # Default shell for tools that spawn one (herdr panes, etc.).
145− "SHELL=/bin/${shell}"
146 # Marker so shells/scripts can detect they're in here.
175 # Marker so shells/scripts can detect they're in here.
147 "NIX_CONTAINER=1"
176 "NIX_CONTAINER=1"
148− # Advertise truecolor for 24-bit-aware tools; `c` passes the
149− # resolved TERM via -e (the runtime would otherwise force xterm).
150− "COLORTERM=truecolor"
151− # A UTF-8 locale so tools handle multibyte/wide characters
152− # correctly. glibc ships C.UTF-8 built in, so this needs no
153− # locale-archive.
154− "LANG=C.UTF-8"
155 # CA trust for every TLS client (curl/git/…) and Nix.
177 # CA trust for every TLS client (curl/git/…) and Nix.
156 "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"
178 "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"
157 "NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"
179 "NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"
@@ -164,7 +186,7 @@
164
186
165 # `nix run . -- init|start` runs the bundled `c` manager script. Its
187 # `nix run . -- init|start` runs the bundled `c` manager script. Its
166 # `init` builds the image from the project's `container.nix` (via
188 # `init` builds the image from the project's `container.nix` (via
167− # `lib.copyWithShell`); the script finds the flake from its own (store)
189+ # `lib.copyWith`); the script finds the flake from its own (store)
168 # location, so this works from a checkout or straight from
190 # location, so this works from a checkout or straight from
169 # `nix run github:…/nix-container`.
191 # `nix run github:…/nix-container`.
170 cApp = pkgsHost.writeShellScriptBin "c" ''
192 cApp = pkgsHost.writeShellScriptBin "c" ''
@@ -177,22 +199,15 @@
177 default = flake-utils.lib.mkApp { drv = cApp; };
199 default = flake-utils.lib.mkApp { drv = cApp; };
178 };
200 };
179
201
180− # Build helpers take a `{ pkgs, nur }: [ ... ]` packages function (the
202+ # Build helpers. `c init` uses `copyWith` to honour a per-project
181− # `packages` attr of a project's `container.nix`). `c init` uses
203+ # `container.nix`:
182− # `copyWithShell` (host shell baked in); `copyWith` is the generic form:
183 # (builtins.getFlake "github:…/nix-container")
204 # (builtins.getFlake "github:…/nix-container")
184− # .lib.<system>.copyWith (import ./container.nix).packages
205+ # .lib.<system>.copyWith (import ./container.nix)
185 lib = {
206 lib = {
186 inherit mkImage;
207 inherit mkImage;
187− # Build the image's `copyTo` script (a skopeo-copy wrapper) from a
208+ # skopeo copy app (`skopeo copy nix:<image> "$@"`) for an image built
188− # `{ pkgs, nur }: [ ... ]` function, named `imageName`. Defaults the
209+ # from a `{ pkgs, nur }: [ ... ]` function.
189− # shell (cmd + $SHELL) to bash; use `copyWithShell` to pick another.
190− copyWith = packages: (mkImage imageName "bash" packages).copyTo;
210+ copyWith = packages: (mkImage packages).copyTo;
191− # Like `copyWith`, but for the given image `name` and shell — `c init`
192− # passes `nix-container-<dir>` and the host login shell, which is added
193− # to the image and used as cmd + $SHELL without being listed in
194− # `container.nix`.
195− copyWithShell = name: shell: packages: (mkImage name shell packages).copyTo;
196 };
211 };
197 }
212 }
198 );
213 );