Skip to content

RFC: Complete Bash-to-Rust Migration with Eval Protocol #439

Open

Description

@youngledo

RFC: Complete Bash-to-Rust Migration with Eval Protocol

Summary

This RFC proposes a three-phase plan to migrate all remaining Bash commands in sdkman-cli to Rust native binaries. The key enabler is an eval protocol that allows Rust binaries to emit shell environment mutations (PATH, JAVA_HOME, etc.) that the Bash wrapper evaluates on their behalf.

Background

SDKMAN's CLI is currently split between:

  • sdkman-cli (Bash): The sdk() shell function and ~23 Bash scripts
  • sdkman-cli-native (Rust): 6 native binaries (current, default, help, home, uninstall, version)

The migration has been stalled since 2024 because the remaining commands (use, install, list, update, upgrade, env) need to modify the calling shell's environment variables. A subprocess cannot directly modify its parent's environment, which is the fundamental blocker.

As discussed in #189, the solution is to have Rust binaries output eval-able shell code that the wrapper script executes.

Proposal

Eval Protocol Design

Rust binaries emit two types of output on stdout:

  1. Display lines (no prefix) — human-readable output, printed to terminal
  2. Eval lines (prefixed with SDKMAN_EVAL:) — shell commands to be eval'd by the wrapper

Example for sdk use java 17.0.0-tem:

Using java version 17.0.0-tem in this shell.
SDKMAN_EVAL:export JAVA_HOME=/home/user/.sdkman/candidates/java/17.0.0-tem
SDKMAN_EVAL:export PATH=/home/user/.sdkman/candidates/java/17.0.0-tem/bin:/usr/bin:...

The Bash wrapper (sdkman-main.sh) would be updated to:

output=$("$native_command" "${@:2}" 2>&1)
display=$(echo "$output" | grep -v "^SDKMAN_EVAL:")
eval_lines=$(echo "$output" | grep "^SDKMAN_EVAL:" | sed 's/^SDKMAN_EVAL://')
echo "$display"
eval "$eval_lines"

Feature Detection

Each native binary gets a --supports-eval-protocol flag so the shell wrapper can detect support. This ensures backward compatibility with older binaries.

Three Phases

Phase 1: Pure Filesystem Commands (no protocol needed)

Command Complexity Operations
flush Low Delete tmp/metadata/version dirs, report freed space
config Very Low Open $SDKMAN_DIR/etc/config in $EDITOR
cache Very Low Validate candidates cache file

These follow the exact same pattern as existing commands — no architectural changes required.

Phase 2: Eval Protocol + use Command

This is the critical phase that proves the protocol works:

  • Add --supports-eval-protocol flag to all binaries
  • Implement use command with eval protocol output
  • Update sdkman-main.sh routing to parse eval lines
  • Cross-shell testing (bash, zsh)

Phase 3: Network + Install Commands

With the protocol proven, migrate the remaining commands:

Command Key Dependencies Challenge Level
list reqwest (HTTP) Medium — remote API + local scan
update reqwest (HTTP) Medium — fetch + diff cache
install reqwest, zip/tar, sha2 High — download, verify, extract, prompt
upgrade (delegates to install) Medium — iterate + batch
env (delegates to use/install) High — .sdkmanrc parsing, 4 subcommands

New Rust Dependencies

Crate Purpose Phase
reqwest + rustls-tls HTTP client (no OpenSSL) Phase 3
zip, tar, flate2 Archive extraction Phase 3
sha2, md-5 Checksum verification Phase 3
dialoguer Interactive prompts Phase 3

Coordinated Release

The eval protocol requires a coordinated release of both sdkman-cli (updated shell wrapper) and sdkman-cli-native (new binaries). The existing sdkman_native_enable config flag provides a safe rollback mechanism.

What Remains as Bash

These components cannot be migrated to Rust and will permanently remain as shell scripts:

  • sdkman-init.sh — must be sourced by .bashrc/.zshrc to bootstrap the shell environment
  • sdkman-main.sh — the sdk() function dispatcher (will become thinner over time)

Alternatives Considered

  1. fd 3 (separate file descriptor): Cleaner separation but fragile to set up across bash/zsh/platforms.
  2. JSON output: Requires jq dependency or slow pure-Bash JSON parsing.
  3. Shim/proxy binary approach (like aqua): Would require a complete architectural redesign.
  4. GraalVM + Picocli (as suggested in Will there be a native version of sdk? #189): Would add JVM build complexity. Rust is already established in this project.

Open Questions

  1. Should the eval prefix be SDKMAN_EVAL: or something shorter like EVAL:?
  2. Should the shell wrapper detect eval protocol support per-binary, or assume all new binaries support it?
  3. How should the Rust install command handle post-install hooks (download and source via eval, or interpret in Rust)?
  4. Should we version the eval protocol in case it evolves?

Related

Activity

  1. marc0der commented on May 13, 2026

    @marc0der
    Member

    The migration has been stalled since 2024 because the remaining commands (use, install, list, update, upgrade, env) need to modify the calling shell's environment variables. A subprocess cannot directly modify its parent's environment, which is the fundamental blocker.

    @youngledo the assumption is incorrect. The reason I stalled development of the Rust port until now was due to (a) the chatty nature of the existing bash CLI, and (b) fundamental problems with the underlying data model regarding vendors. I've spent a lot of time rewriting the entire backend to prepare the ground, which will allow the new Rust CLI to:

    • work in an offline-by-default mode, much like apt or git does.
    • handle Java vendor distributions as first-class citizens

    The work is almost drawing to a close now, with only testing of the new backend and a final data migration pending.

    It does, however, mean that we will pick up the Rust migration work again pretty soon, and I'll certainly use methods like the ones you propose above.

  2. youngledo commented on May 13, 2026

    @youngledo
    Author

    Thanks for the context @marc0der! Great to hear the backend rewrite is nearing completion. The offline-by-default model and first-class vendor support sound like exactly the right foundation for the Rust CLI. Looking forward to seeing the new backend land.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions