Skip to content

Version resolution internals ​

This page describes how osdk turns project declarations or CLI input into an installable exact version. Version resolution chooses a tool and version only; source ranking and installation transactions happen in later stages.

From request to exact version ​

The entry point is gather_requests. Explicit arguments such as node@20 are parsed by ToolRequest::parse. With no explicit tools, the CLI combines [tools] configuration, project package-manager declarations, and Node project metadata. Selecting npm, pnpm, Yarn, or a dynamic npm:<package> tool makes inject_node_dependency include Node in the same operation; it uses latest when the project has no Node declaration.

npm:<package> is parsed before the generic tool@version split so scoped npm:@scope/name@version requests remain intact. Its dynamic backend ID is canonicalized to npm:<package>, while bare npm remains the package-manager backend. See npm developer tool implementation.

cargo: uses the same URL-aware syntax parser with a stricter namespace schema. Registry subjects accept exact/latest/numeric-prefix selectors; canonical HTTPS Git subjects accept only latest, tag, branch, or a full lowercase revision. A Cargo request also injects or preserves exactly one configured/explicit exact Rust request. Rust is resolved first and its exact version is bound to every Cargo request before Cargo resolution continues. See Cargo developer tool implementation.

go: has a dedicated namespace schema for canonical module/command paths, semantic or pseudo-versions, tags, and a restricted build environment. It injects or preserves one managed Go request, resolves that runtime first, and then discovers the longest module root through ranked Go proxy metadata. See Go developer tool implementation.

version/mod.rs defines VersionSpec:

  • empty, latest, stable, and current mean the newest stable release;
  • lts and lts/<name> mean the newest or named LTS line;
  • a complete semver, including prerelease or build metadata, is exact;
  • a leading =, as in =android-36, is a verbatim pin: it requires a character-for-character match among the published versions, never falls back to a looser tier, and does not require the text to be valid semver. This is distinct from "exact", which still degrades through the tiers below when the literal does not match. It exists because some catalogues use identifiers that are not versions and are not mutually exclusive under prefix matching: android-36 and android-36.1 are two different API levels, and dotted-component prefix matching makes the former match the latter. A verbatim pin also counts as an explicit request, so it can reach a pre-release without widening the policy for anything else;
  • incomplete numbers such as 20 or 20.11 are component prefixes;
  • Node project metadata may produce npm-style semver ranges with ||;
  • system is a reserved version spec. Generic backends do not currently resolve it to a PATH executable, and the Rust backend currently maps it to stable; it should not be presented as a working unmanaged/PATH mode.

Candidate lists are expected in ascending order. select_version scans backwards for the highest match: latest and ranges select stable releases, while prefix matching compares dotted components rather than raw string prefixes. A verbatim pin is compared character-for-character and fails to resolve when nothing matches. Exact requests match in three tiers: literal equality first, then semver core comparison ignoring build metadata (21.0.12 matches 21.0.12+8, prerelease identifiers must agree, and the highest build wins), and finally a dotted-component prefix fallback so 21.0.12 reaches a four-part PSU such as 21.0.12.1+1 only when no same-core release exists. The last tier mainly serves Java with its build numbers and PSU fourth segments; strict three-part semver backends never reach it. Before the component-prefix scan runs, the request text is checked against the published versions outright and a hit is returned directly: otherwise android-36 would select android-36.1 whenever both exist. select_version_with_prerelease provides the documented policy semantics for backends that opt into it; a verbatim pin counts as explicit, just like an exact version carrying a prerelease identifier. Python, GitHub, and npm-package-backed custom resolvers apply prerelease policy explicitly; the generic resolver and some backends still use select_version, so current policy behavior is backend-specific.

Working-directory precedence ​

resolve_active walks from the current directory to its ancestors, but precedence is globally grouped by source kind rather than simply choosing the nearest file:

  1. [tools] in osdk.toml or .osdk.toml;
  2. .tool-versions;
  3. backend-declared idiomatic files, preserving the backend's filename order;
  4. Node package.json#engines.node or devEngines.runtime;
  5. user-global [tools].

Consequently, a parent osdk.toml beats a child .nvmrc. Plain idiomatic files use the first non-empty, non-comment value and strip a leading v; go.mod and rust-toolchain.toml have format-aware parsers. Regression coverage lives beside the implementation in version/resolver.rs.

Project package managers ​

resolve_package_manager first walks for npm, pnpm, or yarn under osdk.toml [tools], then walks for package.json#packageManager, and finally reads devEngines.packageManager. Only those three managers and exact semver values are accepted. Missing versions, URLs, paths, hash suffixes, and build suffixes fail explicitly. packageManager wins over devEngines.packageManager.

Backend resolution and exceptions ​

After applying version aliases and one-shot backend options, the CLI calls Backend::resolve_version. The default implementation returns exact versions without querying a remote list and preserves every request option; non-exact requests call list_remote_versions and select_version. Skipping the version list for an exact request does not itself establish a cryptographic guarantee: installation applies the active checksum/attestation policy and may proceed with no evidence when require_checksums=false.

Some backends override the default. Node handles target architecture and npm ranges; Python handles implementations, variants, catalogs, and prerelease policy; Java handles distributions and JDK/JRE; Rust passes channels or versions to its isolated rustup. Cargo registry tools fetch paired metadata/index source data, remove yanked releases, and resolve exact/latest/numeric-prefix selectors, while Cargo Git selectors are retained verbatim. Go command tools resolve exact/latest/numeric-prefix/pseudo-version selectors through ranked Go proxies and bind the discovered module root. See node.rs, python.rs, java.rs, rust.rs, cargo_package.rs, and go_package.rs.

Lockfile fast path and boundary ​

osdk install with neither explicit tools nor extra options first reads the nearest osdk.lock. locked_requests restores saved version strings, public options, and locked artifact data for the current platform; normal project resolution is used when that platform section is absent. Most backends save an exact version. Floating Rust values such as stable, beta, and nightly remain channel names, so a later rustup install may obtain a newer toolchain. Platform-specific sections let Linux, macOS, and Windows resolutions coexist.

After a successful fallback to project configuration, install records that exact resolution in the current-platform section; replaying an existing lock does not rewrite it. Project use upserts the selected tool and injected runtimes after installation, rolling its [tools] edit back if the lock cannot be written. Explicit install TOOL... changes no project declaration and therefore writes no lock.

The lock records a resolution and artifact identity, not a claim that existing bytes are trusted. Reinstallation reruns any available or policy-required checksum/attestation checks. If the lock has no digest/evidence and require_checksums=false, installation may still proceed without cryptographic integrity verification. Supplying tools or -o bypasses the lockfile read fast path; the latter still records new results after a successful bare project install.

Verifiable invariants ​

  • Precedence, ancestor walking, structured files, and invalid package-manager values are covered by the resolver unit tests.
  • Prefix, range, LTS, and prerelease behavior is covered by the version unit tests.
  • The exact-version option-preservation regression test is in backend/mod.rs.
  • Package-manager auto-selection, Node injection, and lock restoration are covered by isolated_cli.rs and lockfile.rs.

Released under the MIT License