TL;DR: A go-tool-base command self-registers using a package-level init() that Go runs before main(). Rust deliberately has no code that runs before main(), so that mechanism simply isn’t available. rust-tool-base gets the same outcome with linkme distributed slices: the #[distributed_slice] macro places each command into a dedicated linker section, and the linker stitches them into one contiguous slice. No startup phase, no global mutable state, and the registry is assembled before the program even runs.
What self-registration buys
A command in go-tool-base lives in its own file, and that file puts the command into the framework itself. There is no central list of commands to keep in sync. You add a file, the command appears. You delete the file, it’s gone. Nothing else changes.
That property is worth protecting. The alternative, a hand-maintained registry that every new command has to be threaded into, is exactly the sort of central file that turns into a merge-conflict magnet and quietly falls out of date. So when go-tool-base moved to Rust, self-registration was firmly in the column of things that had to survive.
The way Go did it was not.
How Go does it
A Go package can declare an init() function, and the runtime guarantees every init() runs before main() starts. A go-tool-base command file uses this to append itself to a package-level slice:
func init() {
registry.Register(&DeployCommand{})
}
By the time main() runs, every command file’s init() has already fired and the registry slice is populated. It’s a tidy trick and it leans entirely on a Go feature: code that executes before main().
Rust doesn’t have that
Rust has no init(). There is no language-blessed phase that runs your code before main(). This is a deliberate decision, not an oversight. Code running before main() across many files has no well-defined order, and a startup phase whose ordering you can’t see is a classic source of subtle bugs. Rust closed that door on purpose.
Which leaves a real question. If nothing runs before main(), how does a command file insert itself into a registry without a central list editing it in?
Distributed slices
The answer is a crate called linkme, and the mechanism is the linker rather than a runtime phase.
You declare a slice the framework will collect into:
#[distributed_slice]
pub static BUILTIN_COMMANDS: [fn() -> Box<dyn Command>];
A command file then contributes one entry to it:
struct Greet;
impl Command for Greet { /* ... */ }
#[distributed_slice(BUILTIN_COMMANDS)]
fn register_greet() -> Box<dyn Command> {
Box::new(Greet)
}
Here is the part that makes it work. The #[distributed_slice] attribute doesn’t generate any code that runs at startup. It places each entry into a dedicated section of the compiled object file. When the linker builds the final binary, it gathers everything in that section and lays it out as one contiguous array. BUILTIN_COMMANDS is that array.
So by the time the program exists as a binary on disk, the registry is already assembled. main() doesn’t build it. No init() builds it. The linker built it, statically, as part of producing the executable. At runtime the framework iterates a slice that was complete before the process ever started.
What you get from it
The outcome is the one Go’s init() gave, and then some.
A command still lives in one file and still self-registers. Adding a command is still adding a file. There is still no central list.
But there is no startup phase to reason about, because there isn’t one. There is no global mutable slice being appended to as init()s fire, because nothing is appended at runtime; the slice is immutable and finished. There is no ordering question, because the linker isn’t running your code, it’s collecting data. And it costs nothing at runtime: assembling the registry happened at link time, so program start just reads it.
It’s the same idea go-tool-base had, expressed by the tool Rust actually gives you. Go reaches the registry through a controlled phase before main(). Rust reaches it without any phase at all, because the linker did the assembly while the binary was still being built.
In short
Self-registration, where a command file inserts itself into the framework with no central list, is a property worth keeping. go-tool-base achieves it with a package-level init(), leaning on Go’s guarantee that such functions run before main().
Rust has no equivalent and wants none, because code running before main() has no clear ordering. rust-tool-base uses linkme distributed slices instead: each command is placed into a dedicated linker section, and the linker assembles them into one contiguous, immutable slice as it builds the binary. The registry is complete before the program runs. Same outcome as Go’s init(), no life before main required.