Skip to content

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

@youngledo

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions