Skip to content

The catalog and plans

How entries are written, and how Helix Toolbox turns the ones you choose into a plan, checks it and runs it.

catalog/tabs.toml names the areas: Applications, Configurations, Helix Doctor, Maintenance, Security and Profiles. Each is a directory with a tab.toml; inside it, entries are <id>.toml files and groups are directories with a group.toml. An entry is one of six kinds:

name = "Ardour"
description = "A digital audio workstation."
flags = "I" # what it does; g in the interface explains the codes
[package] # tried in order: the family's package, the AUR, Flathub, a [release]
binary = "ardour" # optional: its command, for installs made another way
debian = "ardour" # also arch, fedora, suse, alpine, void, solus
fedora = "ardour"
aur = "ardour" # Arch, through paru or yay, when arch is not given
flatpak = "org.ardour.Ardour" # for families with no package
removable = true # offer removal: r in the interface, helix-toolbox remove

Each name must be a package in the family’s repositories: the Distributions workflow checks every one in each distribution’s container. A release that lacks one, such as Ubuntu 22.04 for many newer tools, lists it in tests/package-gaps.txt; there the entry tries its next source.

Whether an entry is installed is asked of the package manager (pacman -Q, dpkg-query, rpm -q and the like) for its own packages, then of Flatpak. Only when none of them is installed does binary count, so a tool installed through Nix, Home Manager or a download is not installed twice. An entry that replaces another package names it in conflicts (WINE Staging has conflicts = ["wine"]): when that package is installed, the plan says it will be replaced instead of taking the entry as installed.

A script or command entry says what it installs in a [detect] table with the same fields (no removable or conflicts); its script still runs when chosen, and the plan notes that the software is already there:

[script]
id = "gaming/steam-launcher"
[detect]
binary = "steam"
arch = "steam"
debian = "steam-installer"
fedora = "steam"
flatpak = "com.valvesoftware.Steam"
[script] # scripts/installers/system-report.sh, run from its directory
id = "system-report"
interactive = true # the default; false for scripts that never prompt
[script] # or a script beside the entry's TOML file
path = "setup.sh"
[command] # an inline shell command
line = "echo hello"
[nix] # one of the two
profile_install = "nixpkgs#ripgrep"
home_manager_switch = "~/dotfiles/home-manager#commander"
[native] # implemented in Rust by the engine
action = "system-report"
[profile] # other entries, as one reviewable plan
entries = ["git", "ripgrep", "fzf", "neovim"]

Groups are listed before entries at every level. Entries and groups can carry order to sort before their name among their own kind, multi_select = false to keep them out of the queue, and preconditions that hide them on machines they do not apply to:

[[preconditions]]
matches = true
check = "command_exists" # or "file_exists", { containing_file = "..." }, { environment = "..." }
values = ["sddm"]

A profile names entries anywhere in the catalog, hidden here or not. On a machine where a member is hidden (by its preconditions, its processor, or a tool it needs, such as Nix), the profile is shown without it, and a profile with no member left is hidden too. Planning several profiles that share a member, or a profile with one of its members, installs that member once.

Scripts source the shared helpers with . "$HELIX_TOOLBOX_LIB/common-script.sh". The installers brought across use its name before 1.7, HELIXFORGE_LIB, which every run sets too. The catalog and scripts are embedded into the binary at build time and unpacked to a private temporary directory when it starts, so scripts run as real files. The copy is removed on exit; one left behind by a Helix Toolbox that was killed is removed the next time Helix Toolbox starts.

A script entry can say what it changes in an [effects] table, which the change preview shows before it runs:

[effects]
packages = ["plymouth (with plymouth-themes on Debian and Ubuntu)"]
services = ["enables sddm.service from the next reboot, if you agree"]
takes_effect = "the boot splash shows from the next reboot"
unknown = ["the regenerated boot menu, written from the installed kernels"]
[[effects.files]]
path = "/etc/default/grub"
change = "adds splash to the kernel command line; backed up first"
render = ["grub-cmdline", "/etc/default/grub", "add", "splash"]
when = "the machine boots with GRUB"

render names one of the installers’ configure.py actions that only print a file’s new contents (grub, grub-cmdline, sddm, mkinitcpio-hooks, limine-cmdline), then the file it reads, which must be path; the preview runs it on the file as it is and shows the two side by side. {grub} stands for /boot/grub (/boot/grub2 on Fedora) and {time} for the date and time the installer puts in a name.

A script entry’s exit status alone does not show that it worked. After its run, what its [detect] table says it installs is looked for, as for a package entry, and its [[verify]] checks say what it leaves, each read from the machine as you, changing nothing:

# The setting selects a theme that is installed.
[[verify]]
kind = "setting"
files = ["/etc/sddm.conf.d/zz-commander-theme.conf", "/etc/sddm.conf"]
section = "Theme"
key = "Current"
equals = "silent"
points_to = "/usr/share/sddm/themes/{value}/metadata.desktop"
[[verify]]
kind = "text"
path = "/proc/cmdline"
word = "splash"
after_reboot = true
[[verify]]
kind = "service"
unit = "fstrim.timer"
enabled = true
active = true
[[verify]]
kind = "path"
path = "/efi/dotfiles-splash.png"
is = "file"
when_exists = "/efi/limine.conf"
Kind Takes Holds when
path path, is (file or directory), absent it is there (of that kind), or gone with absent
text path, one word or line, absent the file has the word (between spaces) or the whole line, or lacks it
setting files, key, section, then equals, contains and points_to, or unset alone the key’s value, from the last of the files that sets it (as programs read drop-ins, then the main file), matches; points_to is a path made from it with {value} that must exist
service unit, enabled, active, user systemd says so, of the system’s unit or your own

Any check takes when_exists (it applies only where that path is) and after_reboot (it holds only once the machine has started again: kept beside the history, checked by Helix Doctor and helix-toolbox verify after the reboot). Paths are absolute, under ~/ or under {grub}/. What cannot be read here, a file only root can read or a container without systemd, is reported as not checked, never as passed; a check that fails makes the run not verified. The VM tests run helix-toolbox verify after each of their cases, so the checks of the entries they cover are proven on real systems.

catalog/setup.toml holds the setup wizard’s workstations, each with entries and [[workstation.when]] additions for a graphics vendor (gpu), a distribution family (family, not_family) or a desktop, and the entries the Desktop screen offers for each desktop ([[desktop]], any for all).

catalog/validation.toml says how far entries are tested beyond the checks every entry gets: [critical] names the entries whose failure can stop a machine booting, logging in or showing a display; each [[stand-ins]] names entries, the installer tests in tests/installers/ that cover them and what they cover; [[hardware]] and [[vm]] record runs reported from real and virtual machines (entry, system, version, date, note). A new installer test or a tester’s report goes there; the Reliability screen reads it.

Running entries first makes a plan: the steps in order, what they need, whether any asks for your password, and the risk from the flags. The interface shows it in the confirmation; helix-toolbox plan and helix-toolbox run --dry-run print it. Every run is recorded with its outcome, including runs that failed or were interrupted.

Before anything runs, Helix Toolbox checks the plan on this machine. A blocker stops the run (the menu says why instead of asking, the command line exits without changing anything); a warning is shown with the confirmation:

  • a tool the plan needs is missing (an AUR helper, flatpak), unless another source of that package can do without it (blocker)

  • two queued entries replace each other, such as WINE and WINE Staging (blocker: queue only one)

  • an entry replaces an installed package (warning: the package manager asks before removing it)

  • an entry needs an Arch repository that is off, such as Steam and multilib (blocker)

  • an entry is for another processor, such as Steam on an ARM64 machine (blocker)

  • an entry is for graphics hardware the machine does not have, such as the Nvidia drivers without an Nvidia card (warning)

  • the root file system has less free space than the packages’ installed sizes (pacman -Si, apt-cache show, dnf repoquery) plus what scripts say they need (blocker), or would be left with less than 1 GiB (warning)

  • another Helix Toolbox run is still going (blocker): one run at a time, so package managers are never driven twice at once; their own locks guard against other tools

Steps run one at a time, in an order that puts each after what it depends on: an entry that installs a command another step needs (Flatpak / Flathub before a Flatpak, an AUR helper before an AUR package), or one it names in needs.after; otherwise in the order queued. A tool an earlier step installs is not reported as missing. When a step fails the run stops, or with After a failed step: continue in Settings (which the command line follows too, or --keep-going for one run) goes on with the steps that do not depend on it, skipping those that do; a Ctrl-C always stops. Each step’s result is shown afterward (done, failed with its exit status, skipped, not run), the history records “1 of 3 steps done”, what did not finish stays in the Install Plan to run again, and the command line prints the helix-toolbox run … that retries it.

Entries say what they need from the machine in an optional [needs] table:

[needs]
gpu = ["nvidia"] # nvidia, amd or intel; any one will do
repositories = ["multilib"] # Arch repositories that must be enabled
space_mb = 6000 # free space beyond its packages, for downloads
after = ["paru-aur-helper"] # entries that run first when both are queued
architectures = ["x86_64"] # x86_64 or aarch64; any when left out

An entry with architectures is left out of the menu and the lists on other processors, like one whose preconditions fail. Named on the command line anyway, it is blocked before anything runs. Give it to entries whose programs are only built for one, such as Steam or DaVinci Resolve.

A package entry lists every source it has on this machine, in order: the distribution’s package, the AUR on Arch, Flathub, then the tool’s GitHub release. The plan shows the first with the others after it (sudo pacman -S --needed meld (else Flathub)). When a source fails, or finishes without the package being installed, the run says so and tries the next one; a source whose tool is missing is skipped. A command stopped with Ctrl-C stops the run instead.

Before a plan is made, the repository metadata already on the machine is asked whether it has the family’s package (pacman -Si, apt-cache show, dnf -C info, zypper --no-refresh info; no network, no root). A package it does not have is left out, and the plan says which source it uses instead (eza is not in this machine's repositories; using the GitHub release (v0.23.4)); with nothing left, the entry is not run and the plan says why. Where the metadata cannot say, as before Debian’s first apt update, the package is tried as before.

A [release] table installs a tool from its project’s GitHub release, as the last source of a package entry or on its own:

[package]
binary = "lazygit"
arch = "lazygit"
debian = "lazygit"
removable = true
[release]
repository = "jesseduffield/lazygit" # owner/name on GitHub
tag = "v0.66.2" # as the download URLs carry it
version = "0.66.2" # compared with what is installed
binary = "lazygit" # installed as ~/.local/bin/lazygit
[release.x86_64]
asset = "lazygit_0.66.2_linux_x86_64.tar.gz"
sha256 = "158efc9d…" # 64 lowercase hexadecimal digits
path = "lazygit" # the program inside the archive
[release.aarch64]
asset = "lazygit_0.66.2_linux_arm64.tar.gz"
sha256 = "79a3161e…"
path = "lazygit"

On its own, [release] takes removable = true itself. An architecture without a table has no release source there. The file is a .tar.gz, .tgz, .tar.xz or .zip, or the program itself (then without path). Every value is checked when the catalog loads: the repository is owner/name, the tag and file names have no slashes or .., the path stays inside the archive and the sum is a SHA-256.

A run downloads the file with curl into a private folder, checks it against the pinned SHA-256 before anything is unpacked, takes only path out of it (a plain file, never a link) and writes it to ~/.local/bin through a new file renamed into place, without root. A record in $XDG_STATE_HOME/helix-toolbox/releases/<entry>.toml keeps what was installed: the program, its version and its own SHA-256. Helix Toolbox never replaces or removes a file there that it did not install, or that has changed since. The status shows GitHub release 0.66.2; a catalog with a newer version makes it an Update, installed over its own file; HELIX Drift reports a recorded program that changed or went away; remove deletes the program and its record.

scripts/release-pins.py ENTRY... [--write] pins entries to their projects’ latest releases: it downloads each architecture’s file, computes its SHA-256, compares it with GitHub’s digest for the file and the release’s own checksums file, and prints or writes the new pins. It only reads the files; an archive’s listing gives the program’s path.

After a run, Helix Toolbox checks each package step: an install passes when its package manager lists one of the entry’s packages or its Flatpak is installed (the command counts only where nothing can be asked, as on Solus); a removal passes when the package is gone. The running window’s title says FINISHED · VERIFIED, the command line prints a Verified: line for each package, and the history records exit 0, verified. A run whose program succeeded but whose package cannot be found is recorded as failed, with what was missing.