scram
0.1.0scram is the snapshot tool of UraniumOS. A frame is a read-only btrfs snapshot of the root and of /home. scram keeps the frames, builds updates in a copy of the system, writes the boot menu and schedules changes that the initramfs applies before the root is mounted. It is one binary from the uranium-tools package and runs as root. rod takes a frame before every install, remove and update, and the autoframe service takes one every hour.
scram COMMAND [ARGUMENT ...]
boot describes the boot side, storage the disk layout and backups copies to other machines. A frame is stored on the same disk as the system, so it is not a backup.
What a frame is
A frame is a directory in .scram on the top level of the btrfs filesystem. It holds a read-only snapshot of @ (the root) and one of @home, plus a few small files. A snapshot shares its blocks with the live subvolume, so a frame only costs the blocks that change after it was taken. Flashing to a frame replaces the live subvolumes with writable copies of the snapshots, so packages, /etc, services and home directories go back to that moment.
The top level of the filesystem (subvolume 5) is mounted on /run/scram/top by the initramfs. Without root, scram cannot read the frames, and geiger warns that the frames cannot be reached.
.scram/ID/@, .scram/ID/@home | The read-only snapshots of a frame. |
.scram/ID/note | The text given to frame -m. |
.scram/ID/boot/ | The kernel and initramfs of the profile when the frame was taken (vmlinuz, initrd) and id, the first 12 characters of the md5 sum of both. There is no boot/ if the profile had no kernel or if the initramfs took the frame. |
.scram/ID/keep | An empty file. A frame with it is skipped by prune. |
.scram/keep.list | The left-out directories, one per line as SUBVOLUME PATH, for example @ var/cache. apply adds to it. |
.scram/try/@, .scram/try/state | The copy made by try, and the id of the pre-update frame taken before it. |
.scram/boot-action | One line, commit, flash ID or flash ID @, for the initramfs to carry out at the next boot. |
.scram/trial, .scram/trial-boot | Written by the initramfs when it commits an update. See Trial boots. |
.scram/live/ | The writable copies that a frame boot runs from. |
.scram/.new-@, .scram/scratch.PID/ | Temporary copies during a flash and during scratch or bisect. |
Frame ids
An id is the local date and time, a dash and a label, like 20261102-165551-install. A second frame in the same second with the same label gets .2, .3 and so on. A frame without a label has only the time.
Wherever a command takes FRAME, you can give an id or a part of one. scram uses the newest frame whose label or id equals the argument. If there is none, it uses the newest frame whose id contains the argument. With no argument it uses the newest frame. A short name can match another label, so update finds pre-update. Give the full id when you are unsure.
Which frames exist
install | The installer takes it with the note "fresh install" right after copying the system. The manual install does the same by hand. |
pre-install, pre-remove, pre-update | rod +i, rod +r and rod +u, before they change anything. Not with rod -n and not inside scram try. |
pre-container, pre-container-remove | silo, before it creates or removes a container. |
auto | scram auto. The autoframe service waits an hour after it starts, runs scram auto 48 and repeats. |
pre-restore, pre-flash, pre-update | restore, an offline flash and try. |
pre-commit, pre-flash, pre-revert | The initramfs, before it applies a scheduled commit, a scheduled flash or a revert. |
| your label | scram frame LABEL. |
scram only deletes frames labelled auto. Every other frame stays until you remove it by hand, see prune.
Commands
Take and look at frames
frame [-m NOTE] [LABEL]- Takes a frame and prints its id. It runs the
pre-framehooks, takes snapshots of@and@home, copies the kernel and initramfs of the profile into the frame, rewrites the boot menu if/boot/limine.confexists, and runs thepost-framehooks.-mneeds a note after it. If only the kernel copy fails, the frame is still made, without a kernel copy. Errors in the menu rewrite are not shown, so runscram bootto see them. roll- Lists the ids, oldest first. A kept frame has
*after the id, and a pending update adds a last line. log- Lists the ids with the number of files that changed since the frame before, and the notes. It compares every pair of neighbouring frames with rsync, which takes a while with many frames.
status- Prints the top level, the number of frames, what a frame covers, the prune rule and, when they apply, an update on trial, a pending update and the action waiting for the next boot. It ends with the left-out directories from
keep.list. The prune line always says 48, whatever number you giveprune. diff [FRAME [FRAME]]- With one frame (default the newest), it lists what differs from the live subvolumes. With two frames, it lists what changed from the first to the second. Each line comes from
rsync -i, with the subvolume in front:>f+++++++++is a new file,>f.st......is a file whose size or time changed, and*deletingis a file that is gone. The comparison uses size and time, not content, and it skips/var/log,/var/cache,/var/tmp,/tmp,/run,/proc,/sys,/devand the left-out directories. cat PATH [FRAME]- Writes the file as it was in the frame to standard output. A path under
/home/comes from@home, and everything else comes from@. If the file does not exist in the frame, the command fails. blame PATH- Goes through all frames, oldest first, and prints the id and a version whenever the version changes: the first 8 characters of the md5 sum of a file,
dirfor a directory orabsent. The first frame is always printed. keep [FRAME]- Marks the frame as kept. Running it again removes the mark. With no argument it uses the newest frame.
# scram frame -m "fresh install" install
20261102-165551-install
# scram frame -m "before kernel update" kernel
20261102-165552-kernel
# scram roll
20261102-165551-install
20261102-165552-kernel
# scram log
20261102-165551-install
fresh install
20261102-165552-kernel 2 files changed
before kernel update
# scram diff install
@: >f..t...... usr/version
@home: >f.st...... alice/.config/theme
# scram status
top: /run/scram/top
frames: 2 (0 auto, 0 kept)
covers: @ and @home, each with its own kernel and initramfs for the boot menu
prune: auto frames beyond the newest 48 are deleted, all other frames stay
excluded:
@home alice/Downloads
# scram frame same
20261102-165629-same
# scram frame same
20261102-165629-same.2
# scram keep install
20261102-165551-install: kept
# scram blame /etc/hostname
20261102-165551-install absent
20261102-165552-hostname 25687c92
20261102-165553-rename e00492ec
Go back
restore PATH [FRAME]- Replaces one file or directory with its copy from the frame. It takes a
pre-restoreframe first, then removes PATH and copies the saved version back. A directory comes back as it was, so files that were created in it later are gone. scram tells you which frame it used and how to undo the restore. It refuses if PATH is a left-out directory, and it fails if the frame does not contain PATH. flash [--boot] [--root] [FRAME]- Replaces the live subvolumes with copies of the frame. Put the options before FRAME. Without
--bootit runs now, but while the system runs from@it refuses and asks you to run it offline. It takes apre-flashframe, then replaces@and after it@home: a writable snapshot of the frame is made, left-out subvolumes are moved into it, the old subvolume is deleted and the copy takes its place. With--rootonly@is replaced and the home directories stay as they are. With--bootscram writes the action toboot-action, says that the flash is scheduled for the next boot, and the initramfs does the work. Use--booton a running system. cancel- Deletes
boot-action. It prints nothing, and it does not touch a pendingtrycopy or an update on trial. scratch [-f FRAME] [COMMAND]- Makes a writable snapshot of
@, or of the frame with-f, mounts/dev,/procand/sysin it and runs COMMAND in a chroot. Without COMMAND it opensbash -lwithSCRAM_SCRATCH=1set, which changes the prompt. The copy is deleted afterwards. The copy holds@only, not/home. Left-out directories are bind-mounted from the live system, so writes to them are real. bisect GOOD BAD COMMAND- Takes the frames from GOOD to BAD and runs COMMAND in a copy of the frame in the middle, as
scratchdoes, with its output discarded. Status 0 means good. The search halves the range until two neighbouring frames are left.
# scram restore /home/alice/.config/theme install
restored /home/alice/.config/theme from 20261102-165551-install (undo: scram restore /home/alice/.config/theme 20261102-165552-pre-restore)
# scram flash --boot install
flash to 20261102-165551-install scheduled for the next boot
# scram scratch -f hostname 'cat /etc/hostname'
uranium
# scram bisect step-a step-e 'grep -qx 1 /etc/mode'
20261102-165625-step-c: good
20261102-165626-step-d: bad
first bad frame: 20261102-165626-step-d
# scram flash install
flashed to 20261102-165551-install (undo: scram flash 20261102-165552-pre-flash)
On a running system, the last command refuses and asks you to run it offline.
Booting a frame from the boot menu leaves @ and @home unmounted, because the system runs from copies in .scram/live. An offline scram flash FRAME can run from there, see troubleshooting.
Update in a copy
An update is built in a copy of @ while the system keeps running, and the copy replaces the root at the next boot. rod +u does this when scram works: it takes a pre-update frame, syncs the package index and runs scram try 'ROD_INNER=1 rod +u'. See rod.
try [--check COMMAND] COMMAND- Takes a
pre-updateframe, snapshots@into.scram/try/@, mounts/dev,/proc,/sysand the left-out directories in the copy, and runs COMMAND in a chroot. If it works and a check was given, the check runs the same way. If either fails, the copy is deleted and scram says that the update failed and the system is untouched. If both work, scram says that the update is pending and how to commit it. commit [--boot] [--force]- Makes the tried copy the root. It first compares the live
@with thepre-updateframe, asdiffdoes. If anything differs, it lists the differences and stops, unless you give--force, which goes on and loses those changes, because the copy replaces@whole. Home directories are not touched. Without--boot, the swap happens now, and only while@is not mounted. With--boot, scram writescommittoboot-actionand tells you that the commit is scheduled for the next boot and that it reverts unless you confirm it or the system stays healthy. The comparison happens when you run the command, not at the reboot, so changes made in between are dropped without a warning. If there is no copy, scram says that nothing is pending. discard- Deletes the copy in
.scram/try. It prints nothing. confirm- Deletes
trialandtrial-boot. It prints nothing and always succeeds. verify- Does nothing when no update is on trial. Otherwise it waits
SCRAM_TRIAL_WAITseconds (300), then runs geiger up to three times, waitingSCRAM_TRIAL_RETRYseconds (30) after each failure. A pass confirms the update. After three failures, scram reboots to revert the update, unlessSCRAM_NOREBOOTis set. Thetrialservice runs it at every boot.
# scram try 'echo broken > /usr/version; exit 1'
scram: update failed (1), system untouched
# scram try 'echo 3.0 > /usr/version'
update ok, pending: commit with scram commit --boot
# scram roll | tail -2
20261102-165552-pre-update
(update pending, frame 20261102-165552-pre-update)
# scram try true
scram: update pending: commit or discard
# scram commit --boot
commit scheduled for the next boot, it reverts unless you run scram confirm or the system stays healthy
Trial boots
A commit scheduled with --boot is applied by the initramfs before the root is mounted. It takes a pre-commit frame, swaps the tried copy in and writes trial (the id of pre-commit) and trial-boot. The system then boots into the new root on trial.
The update stays on trial until scram confirm removes the two files, by hand or through scram verify after geiger passes. If the next normal boot finds both files it takes a pre-revert frame and flashes @ back to pre-commit, and home directories keep their state. Booting a frame from the menu is not a normal boot and leaves the files alone. A system that hangs or panics in the new root is reverted by the next power cycle that boots normally. The pre-revert frame holds the system from before the revert, so the revert can be undone with scram flash.
geiger decides if the new root is healthy, and verify waits five minutes before it asks. geiger fails on a service that is not running, a machine with a network device and no IPv4 address, or a root filesystem more than 95% full. An update booted without a network link is therefore reverted. Messages and file formats are on boot.
A commit without --boot, done offline, has no trial and no revert. The way back is the pre-update frame.
Copy frames
send [-p PARENT] [FRAME]- Writes the frame to standard output as a tar archive. It holds
id,@.sendand@home.send, which are the output ofbtrfs sendfor each snapshot. With-p, each stream holds only what changed since the same snapshot in PARENT. The note, thekeepmark, the kernel copy and the left-out directories are not in it. Both streams are written to$TMPDIR(default/tmp, a tmpfs on UraniumOS) before they are packed, so the whole frame has to fit there. receive- Reads an archive from standard input the same way, creates the frame with the id from the archive, and runs
btrfs receivefor each stream. An incremental stream needs its parent frame on the receiving machine.
# scram send install > full.tar
# scram send -p install kernel > step.tar
# scram send install | scram receive
scram: frame 20261102-165551-install exists
On the machine that receives, scram receive < full.tar prints the id of the new frame.
See backups for using this to copy a frame to another machine.
Set up
apply- Reads the left-out directories from the configuration and makes each one a subvolume of its own. An existing directory is renamed to
PATH.tf-old, a subvolume is created, the contents are copied back, and the old directory is removed. A missing directory is created, and a path that is already a subvolume is only recorded. Every new subvolume is announced and added to.scram/keep.list. scram never turns a subvolume back into a directory. If a step fails, scram stops with an error, and if the copy fails the data is still inPATH.tf-old. Run it while nothing uses the directory, because programs that have files open in it keep the old copy. boot [N]- Copies the kernel and initramfs of the profile to the boot partition, copies limine to
EFI/BOOT/BOOTX64.EFI, and writeslimine.confwith the current system, the newest N frames (default all) and a rescue shell. It prints the path oflimine.conf, which is described in boot. A full partition is reported by the failing file operation, but not duringframeor a rod operation. doctor- Checks the layout and prints a line per check:
ok,warnorFAIL. prune [N]- Keeps the newest N frames labelled
auto(default 48) that have nokeepmark and deletes the older ones, with both snapshots,boot/andnote. Kept frames do not count toward N. An argument that is not a number means 48. scram never deletes frames with other labels, so thepre-installandpre-updateframes of rod pile up. auto [N]- Takes a frame labelled
auto, then runsprune N. It prints nothing. Theautoframeservice runsscram auto 48.
To remove another frame, do what prune does as root with the real id.
# cd /run/scram/top/.scram/ID
# btrfs subvolume delete @ @home
# rm -rf boot note keep
# cd .. && rmdir ID
# scram doctor
ok subvolume @
ok subvolume @home
ok excluded @home/alice/Downloads is a subvolume
ok 8 frames, newest 20261102-165631-auto
warn no boot menu, run: scram boot
ok 95% free
# echo $?
0
The checks:
| FAIL | A subvolume (@ or @home) is missing, or a left-out directory is not a subvolume. In the second case, run scram apply. |
| warn | No frames exist yet; an update is waiting and has to be committed or discarded; an action is scheduled for the next boot; there is no boot menu, so run scram boot; the kernel in the profile differs from the one on the boot partition, so run scram boot; or less than 10% of the filesystem is free. |
| ok | The subvolumes, the left-out directories, the number of frames and the newest one, whether a boot menu is present, and the free space. |
Leaving directories out
A directory with big data you can get again, or data a rollback must not touch, is made a seperate subvolume. A snapshot does not contain a nested subvolume, so frames do not hold the directory and a flash leaves it alone. A copy made by try does not contain it either.
scram reads /etc/scram, then the files in /etc/scram.d/ in name order, then ~/.config/scram of the user who runs the command. Each line is one path, and text after # is ignored. Only the first word of a line counts, so a path with a space is cut short. A line that starts with - leaves the path out, a line that starts with + puts it back, and the last line for a path wins. A leading ~ means $HOME. Other lines are ignored.
A + line cancels a - line only before apply has run. It does not remove a subvolume that exists and does not edit keep.list. The system ships /etc/scram.d/00-uranium.conf:
# index and caches
-/rod/nixpkgs
-/var/cache
-/var/tmp
Example that leaves out a web root:
# echo '-/srv/www' > /etc/scram.d/10-www.conf
# scram apply
excluded /srv/www
# scram status | grep -A4 ^excluded
excluded:
@ rod/nixpkgs
@ var/cache
@ var/tmp
@ srv/www
A path under /home/ belongs to @home and is stored without the /home/ part, so -~/Downloads for alice becomes @home alice/Downloads. All other paths belong to @.
A flash and a commit move each left-out subvolume into the new copy. They stop with an error if another nested subvolume is still in the old one. List such subvolumes with - and run apply. If a frame that was taken before apply still holds files at the path, the move fails.
Hooks
Executable files in /etc/scram.hooks/pre-frame.d/ run before a frame is taken, in name order, with the label as the only argument. Files in post-frame.d/ run afterwards with the new id. Hooks run for every frame scram takes, including those of rod and the autoframe service, and a hook that hangs stops them. They do not run for frames the initramfs takes. A hook can stop a database or flush files so the snapshot is consistent. Without one, a snapshot holds what a power cut would leave.
Environment
SCRAM_TOP | Where the top level is mounted. Default /run/scram/top. |
SCRAM_FLASH | Use this directory as .scram and do not mount the top level. The boot menu is not rewritten after a frame. |
SCRAM_LIVE | Where the live system is mounted. Default /. |
SCRAM_SUBS | The subvolumes a frame covers. Default @ @home. |
SCRAM_CONF, SCRAM_USER_CONF | The configuration file and the user file. The defaults are /etc/scram and $HOME/.config/scram. The directory is SCRAM_CONF followed by .d. |
SCRAM_HOOKS | The hook directory. Default /etc/scram.hooks. |
SCRAM_BOOT | The boot partition mount point. Default /boot. |
SCRAM_PROFILE | The profile that holds the kernel, initramfs and limine. Default /rod/profile. |
SCRAM_LABEL | The root filesystem label used in root=LABEL=. Default uranium. |
SCRAM_CMDLINE | File with extra kernel arguments. Default /etc/cmdline. |
SCRAM_KERNELS | How many kernels besides the current one the boot partition keeps. Default 3. |
SCRAM_TRIAL_WAIT, SCRAM_TRIAL_RETRY, SCRAM_NOREBOOT | Used by verify: seconds before the first check (300), seconds between checks (30) and a switch that stops the reboot. |
SCRAM_TOP and SCRAM_LIVE make scram work on a mounted copy of a disk.
Limits
- A frame is on the same disk as the system. A failed disk, a damaged filesystem or a root user who deletes
.scramtakes the frames with it. See backups. - A flash does not restore left-out directories, the boot partition, other filesystems or memory (
/tmpand/runare tmpfs). It does restore/etcincluding accounts and passwords,/var/log, the package store in/nix/store,/rodand all of/home. Files created after the frame are gone, and thepre-flashframe holds them. scram bootwrites the boot partition, and it runs after every frame and every rod operation. After a flash, a revert or a commit the partition may hold a kernel that does not belong to the restored root until the nextscram boot. The initramfs never writes to it.- Frames taken by the initramfs have no kernel copy and are not in the boot menu until the next
scram boot. - Only
autoframes are pruned. Every frame is a boot menu entry, and every frame or rod operation rewrites the menu with all of them. scratchandtrycopy@, and/homeis not part of the copy.- The initramfs has the names
@and@homefixed, so changingSCRAM_SUBSdoes not change what a flash or commit at boot does. - Only x86-64 UEFI machines are supported.