Skip to content
Docs

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.

  • Single binary, no dependencies. The Python version required Python, pip, and several packages (jinja2, pyyaml, etc.). The Rust version is one static binary — curl | bash and 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, with bulker deactivate to undo it. Your shell session stays intact.
  • No mandatory load step. bulker activate auto-fetches crates from the registry if they aren’t cached. In the Python version, forgetting to bulker load before bulker activate was 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 init required. Config is auto-generated on first use, and the container engine is auto-detected. The Python version would error if you hadn’t run bulker init first, which was an unnecessary barrier for new users.
  • New capabilities. Content-addressable crate digests for reproducibility, bulker exec for 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.

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.yamlbulker crate install ./file.yamlLocal 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 initbulker config initMoved under config subcommand. No longer required — config auto-generated on first use.
bulker listbulker crate list
bulker inspectbulker crate inspect
bulker envvarsbulker config set envvars=...Use config set, config add, config remove instead.
bulker cwl2man(removed)
CommandPurpose
bulker deactivateRestore original PATH (shell function). In Python, you exited the subshell instead.
bulker execRun a single command without a subshell. Replaces bulker run.
bulker crate digestShow content-addressable digest for reproducibility.
bulker crate compareDiff two cached crates.
bulker config showDisplay current config.
bulker config get/set/add/removeManage config values from the CLI.
bulker mock record/runRecord and replay container outputs for CI.
bulker completionsGenerate shell completion scripts.

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:

  1. A single bulker binary is installed.
  2. When a crate is activated, symlinks are created pointing back to the bulker binary (e.g., samtools → bulker).
  3. When you run samtools, the OS executes the symlink. Bulker detects that argv[0] is samtools, looks up the manifest, and constructs the docker run command on the fly.

What this means for you: No generated scripts to manage. Faster activation. Cleaner design.

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.

AspectPythonRust
Default location~/.bulker/bulker_config.yaml~/.config/bulker/bulker_config.yaml
Crate trackingcrates: map in config fileFilesystem cache in ~/.config/bulker/manifests/
Template filesJinja2 templates stored alongside configBuilt into binary (Tera templates, not user-visible)
Init requiredYes (bulker init)No — auto-generated on first use
Engine detectionManual (-e docker)Automatic

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.

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.

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 command
  • apptainer_args / apptainer_command — Apptainer-specific overrides (previously Singularity-only via singularity_args)

The Python version used the name “singularity” throughout. Since Singularity was renamed to Apptainer, the Rust version uses “apptainer” as the canonical name:

Python configRust configNotes
container_engine: singularitycontainer_engine: apptainersingularity still works but prints a deprecation warning
singularity_image_folderapptainer_image_foldersingularity_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.

  1. Install the new Rust binary: curl -sL https://bulker.io/install.sh | bash
  2. Add the shell function to your RC file: eval "$(bulker init-shell bash)"
  3. Replace bulker load → bulker crate install (or just skip it — activate auto-fetches)
  4. Replace bulker run <crate> <cmd> → bulker exec <crate> -- <cmd>
  5. Replace bulker list → bulker crate list
  6. Your existing manifest YAML files work as-is