rod

0.1.0

rod installs and removes software on UraniumOS. It uses the NixOS package repositories through nix-env, keeps one system profile in /rod/profile and takes a scram frame before every change. It also provides package sets, holds and a pinned index.

rod [-n] +OP [ARGUMENT ...] [-OP [ARGUMENT ...]] ...

An "operation" is a letter with a sign, such as +i (install). The arguments after an operation belong to it until the next operation, so one line can contain several. They run from left to right, and rod stops at the first one that fails.

The rod in the stage is a small shell script that knows +i +r +s +l. It installs the first packages, and the full rod from the uranium-tools package replaces it. The cheat sheet lists every operation.

Operations

OperationEffect
+i PACKAGE|@SET... [-i NAME...]Install. -i leaves out members of the sets.
+r PACKAGE|@SET... [-r NAME...]Remove. -r keeps members of the sets.
+t PACKAGE..., -t PACKAGE...Hold a package at its version, release the hold.
+u [URL|REV]Sync the index and upgrade everything that is not held.
+hList what +u would upgrade.
+s [URL|REV]Fetch the package index, or pin it to a revision.
+pShow the index pin.
+f WORDFind recipes and packages.
+n PACKAGE...Show the name, description and homepage of an index package.
+l [explicit]List installed packages, or only those you asked for.
+y PACKAGE..., -y PACKAGE...What a package needs, what needs it.
+z [PACKAGE...]Closure size of the profile or of packages.
+w PATH...Which package a file belongs to.
+q PACKAGE...List the files of a package.
+x PACKAGE...Why a package is installed.
+kCheck the installed files against the store hashes.
+c [old]Delete unused store paths.
+e PACKAGE... [-- COMMAND]Temporary shell or command with packages on the path.
+d PACKAGE... [-- COMMAND]The same with a compiler and the headers and libraries.
+o FILE, +a FILEWrite the installed set to a file, install from such a file.
-nFirst argument: dry run of +i, +r, +u, +h.

To go back to an earlier state, use scram: scram flash --boot FRAME, or choose a frame in the boot menu.

How packages resolve

A package name is an attribute of the index in /rod/nixpkgs. It starts with a letter or an underscore and has only letters, digits and _ . ' + -. Any other name stops rod with an error.

For each name rod looks for /rod/recipes/NAME.nix. A recipe is installed first, one at a time. All other names go to a single nix-env call. A recipe replaces the index package of the same name. The image has recipes for avtty, doas-uranium, initramfs-uranium, linux-uranium, uranium-tools, zen and zoneinfo. See recipes.

nix-env uses the binary cache https://cache.nixos.org when it has the package, and builds from source when it does not. The sandbox is off, so a source build runs as the user that started rod, normally root. A package without a cache entry compiles on your machine and can take a long time.

The same pin gives the same package versions on every machine. The builds themselves are not identical bit for bit.

The index and the pin

Nothing that needs the index works before the first rod +s. +i, +f, +n, +h, +e, +d and +o stop and tell you to fetch the index first.

+s
Downloads the tarball from the CHANNEL= line of /etc/rod.conf. The image sets it to https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz, and rod uses the same URL when the line is missing.
+s URL
Downloads that tarball with curl and unpacks it by its ending: .xz, .gz or .tgz. file:// URLs work.
+s REV
An argument of 40 lowercase hexadecimal characters is a nixpkgs commit. rod fetches https://github.com/NixOS/nixpkgs/archive/REV.tar.gz.

rod unpacks into /rod/nixpkgs.new and replaces /rod/nixpkgs only when the download and the unpack both worked. A failed download leaves the old index alone. At the end rod tells you the revision of the new index, or that the revision is unknown when the tarball has none.

rod writes the result to /rod/nixpkgs/.pin, and rod +p shows it, or tells you that there is no pin yet:

url https://github.com/NixOS/nixpkgs/archive/08d3a97586f6b6343b3b5e799980773629432610.tar.gz
rev 08d3a97586f6b6343b3b5e799980773629432610
date 2026-11-02

The index moves only on rod +s, rod +u and rod +a with a set that names another revision. Frames do not contain the index, so going back to a frame leaves it where it is.

Install

rod +i NAME... takes a frame labelled pre-install, releases holds on the names, installs them and adds them to /rod/installed. An error before the frame leaves the system untouched.

# rod +s
# rod +f editor
# rod +i git tmux

If a recipe fails nothing is added to /rod/installed, even when earlier names of the same command installed. rod +l shows them and rod +l explicit does not. Run the command again to record them.

Sets

An argument that starts with @ is a set: a file /etc/rod/sets/NAME with one package name per line. Empty lines and lines that start with # are skipped. A set without a file stops rod with an error.

The image ships four sets:

@xorg
xorg-server xinit xf86-input-libinput xkeyboard-config xkbcomp xauth xrandr setxkbmap xterm dbus eudev polkit fontconfig dejavu_fonts mesa xdg-user-dirs shared-mime-info desktop-file-utils adwaita-icon-theme xrdb xmodmap xset xsetroot
@xfce
xfce4-session xfwm4 xfce4-panel xfdesktop xfce4-settings xfconf garcon libxfce4ui xfce4-exo xfce4-appfinder xfce4-notifyd xfce4-terminal xfce4-power-manager thunar mousepad polkit_gnome xfce4-icon-theme
@dev
git gcc gnumake pkg-config gdb strace
@tools
htop ripgrep fd tmux vim wget curl less

Add your own by writing a file in /etc/rod/sets. /rod/installed lists the members, not the set.

Leaving members out with -i

Directly after +i, -i starts a list of names to leave out. The names can be sets too. Each name has to be one of the packages given or a member of a set given:

# rod +i @tools -i vim wget

installs htop, ripgrep, fd, tmux, curl and less, and records those six in /rod/installed:

# rod +l explicit | tail -6
htop
ripgrep
fd
tmux
curl
less

Leaving out a name that nothing named is an error, and so is leaving out everything:

# rod +i @tools -i bash
rod: -i bash: not named by +i, and a run-time dependency of a package cannot be left out
# rod +i curl -i curl
rod: +i: nothing is left after -i

A package that @tools installs may need another package to run. That one comes along and cannot be dropped. -i only chooses among the packages you or a set named.

-i works only directly after +i, and only once. The same goes for -r.

Remove

rod +r NAME... removes packages from the profile. Use the names rod +l prints without the version. They can differ from the attribute you installed with, so bashInteractive is removed with rod +r bash-interactive.

Directly after +r, -r starts a list of names to keep. Each name has to be installed:

# rod +r @tools -r tmux

removes every installed member of @tools except tmux. The removed packages stay in the store until rod +c deletes them.

To install a package again, for example to rebuild it from the current index, remove it and install it in one line:

# rod +r htop +i htop

This takes two frames, pre-remove and pre-install. If the install fails the removal stays.

Holds

rod +t NAME... adds the names to /rod/holds and rod -t NAME... removes them. A hold has one effect: +u and +h skip the package. +i of a held name releases the hold. +o writes the holds to a file and +a sets them again.

Frames before changes

When scram is installed, these operations run scram frame LABEL before they change anything:

OperationLabel
+i, +apre-install
+r (when something is removed)pre-remove
+upre-update

The frame id is the time plus the label, such as 20261102-101500-pre-install. scram roll lists frames. If scram is missing or the frame fails, rod goes on without one and says nothing, so run scram roll after the first change on a new system. +t, +s, +e and +o change no package and take no frame.

After a successful +i, +r, +u or +a, rod runs scram boot to refresh the boot menu.

scram flash --boot FRAME returns the system, the configuration and the home directories to a frame. A frame is a snapshot on the same disk and is not a backup. See scram and backups.

Updating

rod +h shows what an upgrade would change for every installed package that is not held. It prints nothing when everything is current.

rod +u does this:

  1. Takes a pre-update frame.
  2. Runs the same download as rod +s with the URL or revision given, so the index moves first. If that fails rod stops with status 1.
  3. If scram status works, rod says that it is updating in a copy of the system, and runs the upgrade inside a copy of the root with scram try. The running system is not touched. scram takes its own pre-update frame first, so an update leaves two. When the update works, scram says that it is pending and how to commit it. When it fails, scram throws the copy away and says that the system is untouched. If an earlier update is still pending, scram asks you to commit or discard it first.
  4. If scram status fails, rod upgrades the profile in place.

Apply a pending update with scram commit --boot and reboot. It runs on trial and reverts if geiger fails, see scram. The index is already new by then, because the sync ran before the copy was made.

Dry runs

-n or --dry-run as the first argument makes +i, +r and +u show what they would do and change nothing. No frame is taken, no hold is released, and /rod/installed does not change. rod shows the lines of nix-env --dry-run that say what would be installed, removed or upgraded, or says that there is nothing to change.

Output when htop has no cache entry:

# rod -n +i htop
installing 'htop-3.3'
this derivation will be built:

When the packages come from the cache, the download size is shown too. The checks before the install still run, so a dry run of +r for a package that is not installed fails. Other operations after -n stop with an error. -n +u does what +h does.

Looking around

+l
Lists the packages of the profile, one name-version per line.
+l explicit
Shows /rod/installed, the names you typed or a set gave, in install order. If the file does not exist yet, rod tells you to install something first.
+f WORD
Takes one word. Prints recipe: NAME for each recipe whose name contains it, then the packages of the index whose name or description matches. Needs the index.
+n NAME...
Shows the description, homepage and name of an index package, and says if it is installed in the profile. A name that does not exist stops rod with an error. Recipes are not looked up.
+y NAME...
What the installed package needs, as a tree.
-y NAME...
What needs it, without the package itself and the profile.
+z [NAME...]
The size of the profile with everything it needs, or of each named package.
+w PATH...
Shows the package that owns each path, with its name, version and store path. A file outside the store is reported as not in the store. A path that does not exist is skipped.
+q NAME...
Lists the files of an installed package, sorted. Symbolic links are listed as files.
+x NAME...
For a package in the profile, it says if you asked for it or if it came in as a dependency. For anything else in its closure, it shows the chain of packages that pulled it in. A name that is not in the closure stops rod with an error.
+k
Checks every store path of the profile against the hashes in the Nix database. It reports whether the profile is intact and lists every damaged path. The database is on the same disk, so this finds damage and does not tell you who caused it.
+p
Prints the pin file described above.

+y -y +z +q stop with an error for a name that is not in the profile.

Environments

rod +e NAME... opens a shell with the packages on the path and a marker in the prompt. With -- COMMAND it runs the command instead. Nothing is installed or recorded. +d does the same and adds the compiler toolchain with the headers and libraries of the packages. The first use downloads the toolchain. Both need the index and at least one name, and both take names from the index, not from seperate recipe files.

# rod +e hello -- hello
# rod +d zlib

The second line opens a shell in which a compiler finds the zlib headers and library.

Share a package set

rod +o FILE writes the installed set to a text file, and rod +a FILE installs it on another machine.

# rod set
pin 08d3a97586f6b6343b3b5e799980773629432610
pkg git
pkg curl
recipe avtty
hold git

The file holds the index revision, then every name of /rod/installed as recipe NAME or pkg NAME, then the holds. Recipe files are not written, so a recipe has to exist on the other machine too.

+a syncs the index to the revision in the file, installs all pkg and recipe lines with +i, and then sets the holds. It removes nothing. A missing file stops rod with an error. The result has the same package versions as the first machine, though not the same builds.

Cleaning up

rod +c deletes store paths that no generation uses. rod +c old deletes old generations first. Frames can still keep old files on disk, see storage.

Files and variables

/rod/profile
The nix-env profile. rod adds its bin directory to PATH.
/rod/nixpkgs
The index, with the pin in .pin. Frames leave it out.
/rod/recipes
Recipes, one NAME.nix per package.
/rod/installed
Names you asked for, one per line. rod adds a line after a successful install and removes it after a successful remove. If the attribute and the profile name differ (bashInteractive, bash-interactive) a stale line stays.
/rod/holds
Held names, one per line.
/etc/rod/sets
Package sets.
/etc/rod.conf
Only the line CHANNEL=URL is read.
/etc/nix/nix.conf
Nix settings: substituters, sandbox = false, experimental-features = nix-command.
ROD_ROOT
Replaces /rod. The store stays in /nix.
ROD_SETS
Replaces /etc/rod/sets.
ROD_INNER
Set by rod +u for the command it runs in a copy. While it is set rod takes no frames and does not sync. Do not set it by hand.
SCRAM_BOOT
The boot partition mount point, default /boot.
NIX_SSL_CERT_FILE
If unset, rod points it at the certificate bundle of the profile and copies it to SSL_CERT_FILE and CURL_CA_BUNDLE.

Errors

The message says what is wrong. Errors always come before any change, so a failed command leaves the system as it was, except when the install itself fails after the frame was taken.

Limits