Skip to content

refactor(runtimes): split provider files to slim shim binary #265

@CalvinAllen

Description

@CalvinAllen

Context

Follow-up to #262 / #264. After the -trimpath flag win, the shim is at 5.70 MB. Symbol analysis shows ~70% of that is dead weight pulled in transitively from blank-imports of the runtime providers:

Package Symbols in shim Used by shim?
klauspost/compress 235 No
bodgit/sevenzip 173 No
net/http 114 No
ulikunitz/xz, pierrec/lz4, andybalholm/brotli, compress/*, archive/zip, crypto/tls, afero, briandowns/spinner, schollz/progressbar ~370 combined No
Plus 1.3 MB of embedded JSON manifests (internal/manifest/data/*.json) No

PR #75 narrowed the call site with the ShimProvider interface, but the linker still keeps Install/Uninstall/ListAvailable reachable because the registered concrete types (*node.Provider etc.) carry those methods. The fix has to happen at the source level: make the heavy methods physically uncompiled in the shim build.

Target

Push the shim toward ~3.0–3.5 MB (a measured floor for a stub doing only the shim's hot path is 2.3 MB).

Approach

Use Go build tags to split each runtime provider into a "shim-relevant" half and a "full" half, then build the shim with -tags shim.

Per src/runtimes/<name>/:

  • Keep in provider.go: Name, DisplayName, Shims, ExecutablePath, IsInstalled, InstallPath, ShouldReshimAfter, GetEnvironment, the Provider struct, NewProvider, and init(). These need only os, filepath, runtime, internal/config, internal/constants.
  • Move to a new provider_full.go with //go:build !shim: Install, Uninstall, ListInstalled, ListAvailable, DetectInstalled, GlobalPackages, InstallGlobalPackages, ManualPackageInstallCommand, plus all install helpers (downloadAndExtract, getDownloadURL, createShims, etc.) and the global/local/current version setters that need only internal/config. Heavy imports (internal/download, internal/manifest) live only in this file.

internal/runtime/registry.go:

  • Build-tag the storage type. Under //go:build shim the registry stores ShimProvider; otherwise it stores Provider. Register accepts ShimProvider in shim builds and Provider in main builds (since Provider embeds ShimProvider, no caller change needed in main builds).

internal/shim/manager.go:

  • Rehash and RuntimeShims reach into the full Provider interface via runtime.Get. The shim binary doesn't call them. Move both to a //go:build !shim file so they're not compiled in.

Build configuration:

  • Add -tags shim to the build-shim task in rnr.yaml and to the corresponding steps in .github/workflows/build.yml and .github/workflows/release.yml.

Acceptance criteria

  • go build -tags shim ./src/cmd/shim produces a working shim binary in the 3.0–3.5 MB range on Windows amd64
  • go build ./src (the main CLI) is unchanged in behavior and size
  • All existing tests pass under the default (no-tag) build
  • Shim runtime behavior is identical (verified via python --version, node --version, etc. against a configured runtime, plus the "no version configured" fallback path)
  • Symbol audit (go tool nm) confirms klauspost/compress, sevenzip, net/http, etc. are no longer present in the shim binary
  • CI green across all five release platforms

Risk

Mostly mechanical. Watch points:

  • Any code outside cmd/shim that calls full-Provider methods needs to live in !shim files so the shim build compiles.
  • Test files in the runtime packages may exercise full-Provider methods; they should not need a build tag because tests aren't run with -tags shim.
  • The internal/migration packages register full-Provider types into their own registry; need to verify they don't get pulled into the shim transitively.

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    choreMaintenance and housekeeping tasksgoPull requests that update go code

    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