Skip to content
pyjsPublic

About

Discover and provision Node, Java, and Android development tools from Python.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

sdkmgr

A Python library and command-line tool for finding development SDKs and installing missing tools through established package managers.

This is an alpha project. Java provisioning supports JDK 21 and 25 through apt, dnf, pacman, Homebrew, or WinGet. There is no JDK fallback, vendor requirement for existing installations, or exact patch-version pinning.

Node provisioning prefers existing mise, otherwise existing nvm, otherwise bootstraps mise. sdkmgr no longer downloads Node archives itself. Android provisioning delegates package installation to Google's command-line tools. The unreleased source also provisions Clang and standalone Tailwind CSS and resolves Android build-tool paths. These additions are not in PyPI 0.2.0 yet.

Install

Python 3.12 or newer is required. Install the CLI in an isolated environment:

uv tool install sdkmgr
sdkmgr --help

For a library dependency, use pip install sdkmgr in your environment. To try unreleased source, use uv tool install git+https://github.com/pyjs/sdkmgr.git instead. Installing sdkmgr itself does not download any SDK or change tool selections.

Android SDK packages

sdkmgr android list
sdkmgr android install platform-tools platforms/android-35 build-tools/35.0.1

Android provisioning uses Google's android sdk CLI only. There is no legacy sdkmanager adapter or fallback. Package IDs use the CLI's slash syntax; sdkmgr translates semicolons only when reading Google's package records. Existing SDK packages do not need to be reinstalled when changing the management tool.

Root selection is --sdk-root, then ANDROID_HOME/ANDROID_SDK_ROOT, then the standard user location on Linux, macOS, or Windows. Conflicting environment variables require an explicit root. The install command prints the SDK root to stdout; progress goes to stderr.

Installation reuses registered packages without upgrading them, verifies key files for platform/build tools, and delegates dependencies to Google. Packages such as platform-tools select Google's stable release when absent, not a pinned revision. This is not yet a reproducible package lock. The list command reads local records without network access and does not require the Android CLI. A missing SDK root produces an empty listing.

Before downloading the CLI or installing missing packages, review Google's SDK terms and the requested packages' licenses, then explicitly confirm acceptance:

sdkmgr android install --accept-sdk-license platform-tools platforms/android-35

This flag acknowledges prior acceptance; sdkmgr never supplies automatic affirmative input. Google's new CLI no longer provides the old --licenses flow, so sdkmgr requires confirmation before invoking installation, even if the CLI would proceed without prompting. Reusing already installed packages does not require the flag. Failed or incomplete installs are reported even when Google's command exits successfully.

The CLI bootstrap archive is pinned and SHA-256 verified, extracted in staging, then atomically published to a separate command-tools directory. The launcher fetches Google's CLI payload on first use. That subsequent payload and its cache are managed by Google, not pinned or independently verified by sdkmgr. Commands launched by sdkmgr include --no-metrics and an explicit --sdk. Discovery and no-op provisioning do not launch the CLI.

Existing tools are not replaced. SDK installs are serialized among sdkmgr callers; do not run Android Studio's SDK installer concurrently. Interrupted installs are rechecked on retry; registered but damaged packages need explicit repair. No external JDK is required for provisioning. Obtain one separately with ensure("java", version="21") when your build needs it.

Bootstrap covers Linux x64, macOS x64/ARM64, and Windows x64. Other hosts need compatible command-line tools installed separately. Windows/macOS support has simulated tests but still needs real installation verification. Linux bootstrap and existing SDK reuse are verified locally. See the CLI reference.

from sdkmgr import Manager

sdk = Manager().ensure_android(
    ["platform-tools", "platforms/android-35", "build-tools/35.0.1"],
    accept_sdk_license=True,  # Only after reviewing and accepting the terms.
)
adb = sdk.executable("adb")
environment = sdk.environment()

# Select exact packages; lookup never installs or selects a newer revision.
aapt2 = sdk.build_tool("aapt2", version="35.0.1")
d8 = sdk.build_tool("d8", version="35.0.1")
android_jar = sdk.platform_jar(35)

Build-tool lookup supports aapt, aapt2, aidl, d8, dexdump, zipalign, and apksigner, including Windows .exe/.bat names. Missing files receive package-specific diagnostics. Lookup also works for existing SDKs whose files are present without local package records. It does not verify package checksums or create signing keys.

The returned environment sets SDK paths without changing your shell. Emulator packages and system images must be requested explicitly. Provisioning never creates an AVD or starts an emulator.

For an opt-in live smoke, after accepting the terms:

python scripts/smoke_android.py --accept-sdk-license

The smoke bootstraps tools and installs API 35 into a temporary SDK, then checks a no-op retry and removes that SDK. Google's separate CLI payload cache may persist; no packages are added to the user's existing SDK.

This bootstraps the CLI if needed, installs API 35 into a temporary SDK, verifies it, checks a no-op retry, and removes the temporary packages.

Java from the command line

sdkmgr install java@21
sdkmgr install java@25
sdkmgr which java@21
sdkmgr list

which only discovers installations. install first looks for a complete, working JDK of the requested major version. It accepts maintenance updates and does not require Temurin. A runtime without javac and jar is not enough.

Discovery checks JAVA_HOME, resolved executables on PATH, and platform installation locations. macOS also uses java_home and Homebrew's prefix; Windows also checks registered JDK uninstall locations. It verifies both java and javac with ten-second subprocess timeouts.

If the JDK is missing, sdkmgr shows the package-manager command:

JDK installation requires this command:

  /usr/bin/sudo -- /usr/bin/apt-get install openjdk-21-jdk

You can quit, run it yourself, and restart your original command.
Completed installations will be detected automatically.

Press Enter to continue, or type q to exit. Authentication is handled by sudo or the Windows installer, never by sdkmgr. On typical sudo systems, the password requested is your account password, not the root password. Package-manager confirmation and license prompts remain visible.

If you exit, run the displayed command yourself and restart the command you originally ran. Discovery happens again, so sdkmgr proceeds without repeating the completed installation. No separate resume flag or checkpoint is needed.

Without an interactive terminal, or with --non-interactive, missing Java produces instructions and exit status 2 without running an installer:

sdkmgr --non-interactive install java@21

Successful install and which print the executable path to stdout. Messages and package-manager output go to stderr. Other expected failures return status 1; interruption returns 130.

Package providers

Platform Installation command, before elevation
Debian/Ubuntu apt-get install openjdk-21-jdk
Fedora/RHEL family dnf install java-21-openjdk-devel
Arch/Omarchy pacman -S --needed jdk21-openjdk
macOS brew install openjdk@21
Windows winget install --exact --id Microsoft.OpenJDK.21 --source winget

Java 25 uses the corresponding 25 package names. Linux commands use sudo unless already running as root. Homebrew is never run through sudo by sdkmgr; Windows installers handle elevation through the OS.

The package manager must already be installed. sdkmgr does not add repositories, refresh package indexes, install Homebrew, accept licenses automatically, or silently substitute another major version. If the requested package is absent from your OS repositories, installation fails with the package-manager output. Install an appropriate JDK yourself or use a supported OS release, then retry.

Arch and Omarchy require complete system updates; partial upgrades are not supported. sdkmgr does not run pacman -Sy or perform a whole-system upgrade. If packages cannot be installed, use your normal system updater (Omarchy's updater on Omarchy), then retry the original sdkmgr command.

sdkmgr does not explicitly change Java alternatives, shell profiles, or global environment variables. Package-manager installers can have their own system side effects. Security updates remain the responsibility of the package manager and the user's update policy; ensure() does not force upgrades.

Node manager selection

sdkmgr install node@22.14.0
sdkmgr which node@22.14.0

Selection follows this order:

  1. Existing mise on PATH, in ~/.local/bin, or previously bootstrapped by sdkmgr under its private root.
  2. Existing Unix nvm or nvm-windows.
  3. Bootstrap a pinned mise if neither manager is present.

A selected manager's failure does not trigger fallback to another provider. which and list never bootstrap managers or install Node. A standalone Node on PATH does not bypass this selection order.

Mise

When mise is present, sdkmgr prefers it, including versions that are installed but not currently active. It requests an exact version through mise's built-in Node provider, validates node --version, and requires the bundled npm and npx executables. A missing version is installed in mise's storage, not sdkmgr's cache. No sudo is used.

Discovery uses mise ls --installed --json core:node; installation uses mise install core:node@VERSION --jobs=1. sdkmgr does not run mise use, activate a shell, edit project configuration, or switch global defaults. Returned paths bypass shims, and environment() selects Node only for the subprocess receiving that environment.

The adapter requires mise's --no-config and --no-hooks options. These prevent unrelated project configuration or hooks from running. It also disables source compilation and the default extra-npm-packages file. Existing environment settings, including mise's data directory, remain available. Configuration-file-only customizations are intentionally not loaded.

A broken or incompatible mise installation produces an error instead of a silent fallback to another installer. Upgrade or repair mise and retry. Discovery commands have a 30-second timeout; installation has ten minutes and sends progress to stderr. find() never installs a missing version.

Bootstrap downloads mise 2026.9.12 from its official GitHub release and checks the binary against a SHA-256 pinned in sdkmgr before executing it. No shell installer runs. Linux uses the musl binary; macOS and Windows use their native binaries. Both x64 and ARM64 assets are pinned. The executable is installed atomically under ROOT/managers/, without changing PATH or shell profiles. Concurrent bootstrap calls share a lock. A corrupt cached binary produces an error rather than being silently overwritten.

Nvm

Unix discovery checks NVM_DIR, ~/.nvm, XDG_CONFIG_HOME/nvm, and common Homebrew/Arch script locations. Inactive versions are reusable. Installation sources the existing script in a noninteractive Bash subprocess without loading shell profiles. It requests a prebuilt Node and skips default extra npm packages. To avoid nvm creating a default alias, installation uses a temporary NVM_DIR; only the validated version is moved into the user's store.

On Windows, sdkmgr discovers nvm.exe through PATH or NVM_HOME, then asks nvm-windows v1 or v2 for its installation root. It installs missing versions without invoking nvm use. Existing manager settings still apply, including v2's configured extra npm packages. Version 2 automatically activates its first Node installation, so an empty v2 store produces manual-install instructions and exit status 2. Run the displayed command and retry. sdkmgr does not modify registry preferences or undo manager-owned state changes.

--non-interactive disables prompts for privileged Java installation; ordinary Node provisioning and mise bootstrap remain automatic, including in CI.

Python integration

import subprocess

from sdkmgr import Manager

manager = Manager()
jdk = manager.ensure("java", version="21")

subprocess.run(
    [jdk.executable("javac"), "Example.java"],
    env=jdk.environment(),
    check=True,
)

environment() returns a new complete environment with the selected JDK's bin directory first in PATH and its home in JAVA_HOME. Pass an explicit mapping to environment(base) if needed. Neither form mutates os.environ. Installation.version records the detected runtime version, such as 21.0.12, rather than just the requested major.

find("java", version="21") returns an installation or None without installing anything. installed() returns discovered Java 21/25 installations and versions from the selected Node manager. Old prototype Node caches are not used or removed.

For unattended callers, disable prompting explicitly:

from sdkmgr import InstallationRequired, Manager

manager = Manager(interactive=False)
try:
    jdk = manager.ensure("java", version="21")
except InstallationRequired as error:
    required_command = error.command
    raise

InstallationRequired.command is an argument tuple; no shell parsing is needed. InstallationRequired is an SDKError. Library calls never terminate the calling process. Even interactive=True cannot launch an installer without terminal input and terminal stderr. Installer processes inherit stdin; sdkmgr never reads, stores, forwards, or logs a password.

After an installer reports success, discovery runs again. If its installation location is not recognized, set JAVA_HOME and retry. Long-running interactive package installations are not given an arbitrary timeout while authentication or user confirmation may be pending.

Clang and LLVM toolchains (unreleased)

sdkmgr clang which
sdkmgr clang install
sdkmgr clang install --sanitizer address --sanitizer undefined
from sdkmgr import Manager

manager = Manager(interactive=False)
compiler = manager.ensure_clang()
clang = compiler.executable()
environment = compiler.environment()

find_clang() discovers and checks the compiler version without installing or compiling. Selection is an explicit executable=... (CLI --executable), then CLANG, then PATH and platform locations. Explicit selections fail rather than falling back. Apple Clang is accepted; its reported version is not an upstream LLVM version guarantee.

ensure_clang() installs missing Clang through apt, dnf, pacman, Homebrew, or WinGet with the same confirmation and manual-retry flow as Java. It uses the OS repository's version, not an exact LLVM pin. It does not bootstrap package managers, add repositories, switch compiler alternatives, or repair an existing compiler automatically.

Linux installation requests C development tools. Debian/Ubuntu use clang and build-essential, relying on the distribution's compiler dependencies; they do not assume a cross-release sanitizer runtime package name. Requested sanitizer probes report missing runtimes for manual installation. Fedora/Arch also request compiler-rt. Homebrew installs llvm; macOS also needs Xcode Command Line Tools. WinGet installs LLVM.LLVM; Windows additionally needs Visual Studio C++ Build Tools and a Windows SDK. Those macOS/Windows prerequisites must already be present or installed manually. A developer terminal may be needed on Windows.

After discovery or installation, ensure_clang() compiles and links a small C executable using libc and a separate LLVM IR shared library. It never runs the outputs and removes the temporary files. Requested sanitizer checks cover linking, not sanitizer runtime execution. Existing compiler validation is also available as compiler.validate(sanitizers=("address",)).

These checks diagnose missing headers, linkers, and runtime libraries; they do not establish support for every LLVM IR feature, target architecture, or application ABI. PyJS retains ownership of those checks and its compile flags. Clang paths have no dependency on PyJS environment variables: the integration can forward PYJS_CLANG explicitly.

For Clang and Tailwind, environment() prepends the executable's entire directory to PATH, including shared directories such as /usr/bin. This can shadow unrelated user tools or version-manager shims in child processes. To preserve PATH ordering, invoke the absolute executable() path and omit env, or pass your unchanged environment. No parent environment is modified.

Standalone Tailwind CSS (unreleased)

sdkmgr install tailwindcss@4.3.3
sdkmgr which tailwindcss@4.3.3
tailwind = manager.ensure("tailwindcss", version="4.3.3")
executable = tailwind.executable()
environment = tailwind.environment()

An exact-version TAILWINDCSS override or executable on PATH is reused. An invalid explicit override fails rather than silently downloading a replacement. Otherwise sdkmgr downloads the official standalone executable into its private tool cache. Node is not required.

Downloads are versioned, locked, staged, checked against the release's sha256sums.txt, and version-validated before publication to the cache. Cached binaries are rehashed on lookup. Interrupted downloads are discarded; corrupt caches receive a repair diagnostic instead of being overwritten. The checksum manifest comes from the same upstream release as the executable; it is not an independently pinned signature or trust root.

Supported downloads cover Linux x64/ARM64 with glibc or musl, macOS x64/ARM64, and Windows x64. Windows ARM64 needs an explicitly installed compatible executable; sdkmgr does not silently substitute the x64 build. CSS input, application dependencies, and build execution remain the caller's concern.

The generic list command still lists Node and Java installations only. Use clang which, which tailwindcss@X.Y.Z, and android list for the other tools.

Development

Python 3.12 or newer is required. Activate the environment after uv sync:

uv sync
source .venv/bin/activate
pytest
ruff check .
python scripts/check_release.py

The default tests use simulated SDKs and installers. They do not install packages, ask for passwords, or depend on installed Java, mise, or nvm versions. The CI configuration covers Python 3.12 and 3.14 on Linux, macOS, and Windows. The release check builds both distributions in temporary storage, audits their contents, installs each into a fresh environment, and runs the packaged tests outside the checkout. It does not publish anything. uv build creates the wheel and source archive in dist/ when you are ready to retain artifacts. The Python 3.12/3.14 tests and installed-artifact checks pass on all three systems. Real mise/Node smokes pass on Linux, macOS, and Windows; real nvm/Node smokes pass on Linux and macOS. Real OS JDK installation still needs verification on those platforms. Do not run an automatic source formatter.

Separate CI jobs exercise real mise bootstrap and Node installation on Linux, macOS, and Windows, plus Unix nvm on Linux and macOS. These networked smoke tests are outside the default pytest run:

python scripts/smoke_node.py mise
python scripts/smoke_node.py nvm

The mise smoke uses temporary storage. The nvm smoke installs into the detected nvm store: use a disposable NVM_DIR when testing missing versions. Native nvm-windows installation still needs a real Windows smoke test.

Additional opt-in smokes cover the new tools:

python scripts/smoke_tools.py clang --sanitizer address
python scripts/smoke_tools.py tailwindcss

The Clang smoke requires an existing compiler and never installs OS packages. The Tailwind smoke uses a temporary cache, or reuses an exact-version tool already on PATH. Both clean up their temporary files. Real Clang linking and Tailwind download, reuse, and CSS generation pass on Linux, macOS, and Windows. Clang installation through OS package managers still needs live validation. The tool-provider CI job runs these smokes on all three operating systems when the Tests workflow is dispatched manually; it does not run on every push.

About

Discover and provision Node, Java, and Android development tools from Python.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages