ADR-0136: One install script per platform, and a launcher that makes rio an application
- Status: Accepted; amended by 0141
- Date: 2026-09-22
- Deciders: jka
- Decision: D129
Context
rio was a day from its first release with three setup scripts that did not read as an install.
Two of them carried dev-deploy in the name and so announced themselves as
development-environment setup, which is not what a stranger who has just cloned rio is
looking for. Which script belonged to which operating system was visible only in the file
extension, .sh against .ps1. Both of those were jka's own observation.
The third problem was larger. rio-dev-deploy.sh stopped at packages: it installed the
Tcl/Tk toolchain, verified that it loaded, and ended. The user was left to type
wish rio-gui/rio-gui.tcl from inside the checkout — there was no rio command, no
application-menu entry and no icon anywhere but inside the running window, although
rio-gui/icons/ has carried a full set of sizes since ADR-0127.
rio-dev-deploy.ps1, meanwhile, had been a real installer all along: its own synopsis read
"set up rio for daily use on Windows 11", and it winget-installed the toolchain, set the
persistence variables and could drop a shortcut. Its name was the lie, not its content.
One constraint shapes everything below: rio has no packaging path. It runs from a
checkout — a fact ADR-0100 already leaned on when it dropped
the question of installing docs/, since the manual is already beside the code wherever
rio runs. So "installing rio" cannot mean copying rio anywhere.
Decision
Three scripts, one per platform, named for the platform.
| Script | For |
|---|---|
install-unix.sh | Linux, the BSDs, macOS |
install-windows.ps1 | Windows 11 |
install-server.sh | a headless box running only the core, edited remotely |
One script per platform, not a user script beside a developer script. jka offered
either. The developer extras stay as flags: --with-ck is labelled for contributors, and
--no-launcher installs the toolchain alone, which is what someone with several clones
wants. --launcher-only takes the other half, and doubles as the answer for a system whose
package manager rio does not know.
An install is the toolchain plus a launcher that points back into the checkout. On
POSIX that is a rio command in ~/.local/bin, rio's icon at every shipped size in
~/.local/share/icons/hicolor, and a .desktop entry in ~/.local/share/applications.
Everything is per-user, nothing needs root, nothing lands outside the account, and
--uninstall removes exactly those three things. --prefix covers a system-wide install.
The script never uninstalls packages: other things on the machine need Tcl.
The launcher is a generated wrapper script, never a symlink. Tcl's file normalize
does not resolve symlinks, so [info script] would stay the link's path — and
rio-gui.tcl's first act is to source ../rio-core/deps.tcl relative to it. A symlink in
~/.local/bin would therefore send rio looking for its core in ~/.local/ and kill it
before the dependency gate of ADR-0122
could say anything useful. The wrapper also execs wish, not tclsh, because
rio-gui.tcl calls rio::deps::gui_require, which assumes Tk is already up and never
requires Tk itself.
StartupWMClass is set to the window's real WM_CLASS. Tk derives that from the
script's name, so it is rio-gui.tcl rather than wish. It was read with xprop from a
live window before being written down, the way ADR-0127
verified _NET_WM_ICON. Without it the taskbar shows a running rio as a stray window
instead of grouping it under its own launcher.
macOS ships attempted, not verified. install-unix.sh now knows Homebrew, and knows
the two things that would otherwise make a first run fail confusingly: Homebrew's Tcl is
keg-only, so its tclsh and wish are not on PATH, and the tclsh that is on a Mac's
PATH is Apple's deprecated 8.5 without Tk or tcllib. Both the verification probe and the
launcher are pointed at Homebrew's by full path. The formula names are a guess — tcllib
may not be a formula at all — so a missing one warns rather than aborting and the
verify step decides. The run states that macOS is unverified and asks for a report.
On Windows the shortcut becomes the default (-NoShortcut opts out), a Start Menu
entry joins the Desktop one, and both carry rio's committed rio.ico instead of
inheriting wish.exe's Tk feather. Each is a shortcut to wish.exe with rio's script as an
argument, never to the .tcl file, which would follow whatever Windows currently
associates with .tcl.
rio::deps::provides is left unchanged. deps.test holds that table against
INSTALL.md §1's operating-system package names as sets, in both directions. Naming
Homebrew packages in §1's table would therefore have required naming them in the one
message a user gets when a package is missing — a claim to make after somebody has run rio
on a Mac, not before. The macOS note is prose beside the table, which that parser skips by
construction, so the guard keeps its full strength.
Alternatives considered
A separate pair of user-facing scripts, leaving the dev ones alone. jka offered this as the other option. Rejected because two scripts that both apt-install Tcl drift apart, which is the failure this project's own history records: under ADR-0131 the changelog in PITCH.md sat fifteen decisions behind, for exactly the reason that keeping it current was an instruction addressed to whoever happened to be editing.
A symlink for the launcher. One line instead of a generated file, and wrong: probed before the design was fixed, and it breaks rio's module resolution outright.
Fixing rio-gui.tcl to resolve symlinks itself. Defensible, and the wrong week — a
change to the startup path of the main file the day before a release, to enable a launcher
shape that has no advantage over a wrapper.
Adding Utility to the desktop entry's Categories. desktop-file-validate suggested
it as a hint. Refused after trying it: it produces two main categories, and the validator
then warns that rio "might appear more than once in the application menu". A hint is not a
defect; a duplicate menu entry is.
Leaving macOS to stop at "no supported package manager found". Honest, and it was weighed. Rejected because the verify step is already the script's stated arbiter for an unfamiliar system, so a labelled attempt costs nothing a wrong guess would not also cost, and gives a Mac user somewhere to start and something to report.
Consequences
A user clones rio, runs one script named after their operating system, and then has rio
on the command line and in the application menu. The scripts say what they are, and the
Windows one no longer has to be explained as the counterpart of a "dev" script.
The launcher bakes in an absolute path, so moving the checkout requires re-running the script. The scripts say so and so does INSTALL.md.
There is a name collision in the wild: a well-known terminal emulator is also called
rio. The script checks before installing and says which rio was already on PATH,
leaving the user to decide; it does not refuse.
A new register row, and the guard for it (AGENTS.md §7). A rename is invisible to every
other check: a document naming a script that no longer exists reads perfectly well and is
simply wrong, which is how rio-dev-deploy survived in WINDOWS.md for three decisions
until a reader noticed. docs.tcl check 20 holds the shipped install-* scripts against
the names the user-facing documents quote, both directions, plus INSTALL.md naming all of
them since it is the canonical home. Start joined prose_not in the same file, because
Windows' Start Menu is somebody else's proper noun and check 7 would otherwise insist rio
have a Start menu.
Unverified surface. install-windows.ps1 has not been run — the development box has no
PowerShell — and macOS has not been run by anyone. Both are stated rather than implied, in
the scripts, in INSTALL.md §8 and in the release notes.
Still open: real packaging (.deb, .apk, a Homebrew formula) remains the roadmap's
Install / packaging path; this is a deliberately smaller claim, that a checkout can feel
installed. A systemd unit for the headless core, and a MimeType association making rio a
handler for text/plain, were both left out — the latter competes for every text file on
the machine and should be the user's own choice.