Migrating from bulker (Python)
Bulker was rewritten from Python to Rust. Your existing manifests work as-is, and the core concepts (crates, activate, exec) are the same — but the new version is better in several important ways.
Why migrate
Section titled “Why migrate”- Single binary, no dependencies. The Python version required Python, pip, and several packages (jinja2, pyyaml, etc.). The Rust version is one static binary —
curl | bashand you’re done. No virtual environments, no version conflicts, no broken installs after a system Python upgrade. - Shimlinks instead of generated scripts. The Python version generated a shell script for every command in every crate. These scripts could get stale — if you changed your config (volumes, engine, environment variables), the already-generated scripts didn’t pick up the changes until you reloaded the crate. The Rust version uses symlinks back to the bulker binary (busybox-style) and constructs the container command at runtime from the current config, so config changes take effect immediately without reloading anything.
- No subshell. The Python version spawned a new subshell on
bulker activate, which meant losing shell history, environment variables, aliases, and other shell state. The Rust version modifies your current shell in-place via a shell function, withbulker deactivateto undo it. Your shell session stays intact. - No mandatory load step.
bulker activateauto-fetches crates from the registry if they aren’t cached. In the Python version, forgetting tobulker loadbeforebulker activatewas a common source of confusion — that entire class of errors is gone. - Runtime import resolution. In the Python version, imports were copied into the parent crate at load time, so updating an imported crate required reloading every crate that used it. The Rust version resolves imports at runtime, so updates propagate automatically.
- Zero-config start. No
bulker initrequired. Config is auto-generated on first use, and the container engine is auto-detected. The Python version would error if you hadn’t runbulker initfirst, which was an unnecessary barrier for new users. - New capabilities. Content-addressable crate digests for reproducibility,
bulker execfor one-shot commands without a shell function (useful for scripts and AI agents), mock mode for CI testing without containers, strict environment mode for reproducible builds, interactive container shells for debugging, and shell completions.
Below is a detailed reference for what changed.
Command changes
Section titled “Command changes”Renamed or replaced commands
Section titled “Renamed or replaced commands”| Old (Python) | New (Rust) | Notes |
|---|---|---|
bulker load <crate> | bulker crate install <crate> | Moved under crate subcommand. Optional — activate auto-fetches now. |
bulker load -m file.yaml | bulker crate install ./file.yaml | Local files detected by path prefix (./, /, or .yaml extension). |
bulker unload <crate> | bulker crate clean <crate> | |
bulker reload | (removed) | Re-run bulker crate install -b instead. |
bulker run <crate> <cmd> | bulker exec <crate> -- <cmd> | Note the -- separator before the command. |
bulker init | bulker config init | Moved under config subcommand. No longer required — config auto-generated on first use. |
bulker list | bulker crate list | |
bulker inspect | bulker crate inspect | |
bulker envvars | bulker config set envvars=... | Use config set, config add, config remove instead. |
bulker cwl2man | (removed) |
New commands (no Python equivalent)
Section titled “New commands (no Python equivalent)”| Command | Purpose |
|---|---|
bulker deactivate | Restore original PATH (shell function). In Python, you exited the subshell instead. |
bulker exec | Run a single command without a subshell. Replaces bulker run. |
bulker crate digest | Show content-addressable digest for reproducibility. |
bulker crate compare | Diff two cached crates. |
bulker config show | Display current config. |
bulker config get/set/add/remove | Manage config values from the CLI. |
bulker mock record/run | Record and replay container outputs for CI. |
bulker completions | Generate shell completion scripts. |
Architecture changes
Section titled “Architecture changes”Shell scripts → shimlinks
Section titled “Shell scripts → shimlinks”The Python version generated a shell script for each command in a crate (using Jinja2 templates). Each script was a docker run ... wrapper placed in the crate folder and added to PATH.
The Rust version uses a shimlink (busybox-style) pattern:
- A single
bulkerbinary is installed. - When a crate is activated, symlinks are created pointing back to the
bulkerbinary (e.g.,samtools → bulker). - When you run
samtools, the OS executes the symlink. Bulker detects thatargv[0]issamtools, looks up the manifest, and constructs thedocker runcommand on the fly.
What this means for you: No generated scripts to manage. Faster activation. Cleaner design.
Subshell → shell function
Section titled “Subshell → shell function”The Python version launched a new subshell with a modified PATH on bulker activate. You exited the subshell to deactivate.
The Rust version uses a shell function (installed via eval "$(bulker init-shell bash)"). bulker activate modifies the current shell’s PATH in-place. bulker deactivate restores it. No subshell.
Config file changes
Section titled “Config file changes”| Aspect | Python | Rust |
|---|---|---|
| Default location | ~/.bulker/bulker_config.yaml | ~/.config/bulker/bulker_config.yaml |
| Crate tracking | crates: map in config file | Filesystem cache in ~/.config/bulker/manifests/ |
| Template files | Jinja2 templates stored alongside config | Built into binary (Tera templates, not user-visible) |
| Init required | Yes (bulker init) | No — auto-generated on first use |
| Engine detection | Manual (-e docker) | Automatic |
No separate load step
Section titled “No separate load step”In Python, you had to bulker load a crate before bulker activate. In Rust, bulker activate auto-fetches from the registry if the crate isn’t cached. bulker crate install exists for pre-caching but is optional.
Import resolution
Section titled “Import resolution”Python resolved imports at load time — imported crate folders were copied into the parent crate. Rust resolves imports at runtime — updating an imported crate propagates automatically without reloading.
Manifest format
Section titled “Manifest format”The manifest YAML format is unchanged. Manifests written for the Python version work with the Rust version. The only additions are optional new fields:
no_default_volumes— skip config-level volume mounts for a specific commandapptainer_args/apptainer_command— Apptainer-specific overrides (previously Singularity-only viasingularity_args)
Apptainer/Singularity changes
Section titled “Apptainer/Singularity changes”The Python version used the name “singularity” throughout. Since Singularity was renamed to Apptainer, the Rust version uses “apptainer” as the canonical name:
| Python config | Rust config | Notes |
|---|---|---|
container_engine: singularity | container_engine: apptainer | singularity still works but prints a deprecation warning |
singularity_image_folder | apptainer_image_folder | singularity_image_folder still works but prints a deprecation warning |
singularity_args (manifest) | apptainer_args (manifest) | Per-command apptainer flags |
If you have an existing config with singularity settings, bulker will read them and warn you to update. The runtime behavior is identical — both values select the apptainer code path.
Auto-pull: The Rust version automatically pulls missing SIF images at runtime (via apptainer pull ... docker://...). The Python version required a separate bulker load -b step to pre-build images.
Quick migration checklist
Section titled “Quick migration checklist”- Install the new Rust binary:
curl -sL https://bulker.io/install.sh | bash - Add the shell function to your RC file:
eval "$(bulker init-shell bash)" - Replace
bulker load→bulker crate install(or just skip it —activateauto-fetches) - Replace
bulker run <crate> <cmd>→bulker exec <crate> -- <cmd> - Replace
bulker list→bulker crate list - Your existing manifest YAML files work as-is