diff --git a/pkgs/bun/bun.pkg.jsonc b/pkgs/bun/bun.pkg.jsonc new file mode 100644 index 00000000000..dc0908a319f --- /dev/null +++ b/pkgs/bun/bun.pkg.jsonc @@ -0,0 +1,30 @@ +// Package definition for Bun, installed from its GitHub releases. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "bun", + "description": "JavaScript runtime, bundler, test runner and package manager", + "homepage": "https://bun.sh", + "license": "MIT", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + "arch_map": { "x86_64": "x64", "aarch64": "aarch64" }, + + "versions": { "type": "github_releases", "repo": "oven-sh/bun", "strip_prefix": "bun-v" }, + + "install": { + "type": "archive", + "url": "https://github.com/oven-sh/bun/releases/download/bun-v{{.Version}}/bun-{{.OS}}-{{.Arch}}.zip", + "checksum": { + "type": "sha256_url", + "url": "https://github.com/oven-sh/bun/releases/download/bun-v{{.Version}}/SHASUMS256.txt" + }, + "strip_components": 1 + }, + + // bunx is a link to bun, as Bun's own installer creates it. + "hooks": { "post_install": ["ln -sf bun bunx"] }, + + "bin": ["."], + "binaries": ["bun", "bunx"] +} diff --git a/pkgs/deno/deno.pkg.jsonc b/pkgs/deno/deno.pkg.jsonc new file mode 100644 index 00000000000..6147c6b2974 --- /dev/null +++ b/pkgs/deno/deno.pkg.jsonc @@ -0,0 +1,34 @@ +// Package definition for Deno, installed from its GitHub releases. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "deno", + "description": "JavaScript, TypeScript and WebAssembly runtime", + "homepage": "https://deno.com", + "license": "MIT", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + // Rust target triples. + "os_map": { "darwin": "apple-darwin", "linux": "unknown-linux-gnu" }, + "arch_map": { "x86_64": "x86_64", "aarch64": "aarch64" }, + + "versions": { "type": "github_releases", "repo": "denoland/deno" }, + + "install": { + "type": "archive", + "url": "https://github.com/denoland/deno/releases/download/v{{.Version}}/deno-{{.Arch}}-{{.OS}}.zip", + "checksum": { "type": "sha256_url", "url": "{{.URL}}.sha256sum" }, + "version_overrides": [ + { + // Releases before 2.0.1 published no checksums and their GitHub + // assets have no digest; the first install records one. + "versions": "<2.0.1", + "checksum": { "type": "none" } + } + ] + }, + + // The zip holds just the deno executable. + "bin": ["."], + "binaries": ["deno"] +} diff --git a/pkgs/go/go.pkg.jsonc b/pkgs/go/go.pkg.jsonc index e607b4f6606..103bfbe1492 100644 --- a/pkgs/go/go.pkg.jsonc +++ b/pkgs/go/go.pkg.jsonc @@ -33,8 +33,13 @@ }, "bin": ["bin"], - "binaries": ["go", "gofmt"] + "binaries": ["go", "gofmt"], - // GOROOT is intentionally not set: the go binary locates its own root and - // upstream discourages setting it. + // Run the version in devbox.json even when go.mod asks for a newer + // toolchain, instead of downloading that one (the same default as the + // nixpkgs go). GOROOT is intentionally not set: the go binary locates its + // own root and upstream discourages setting it. + "env": { + "GOTOOLCHAIN": "local" + } } diff --git a/pkgs/gradle/gradle.pkg.jsonc b/pkgs/gradle/gradle.pkg.jsonc new file mode 100644 index 00000000000..33fd2da336b --- /dev/null +++ b/pkgs/gradle/gradle.pkg.jsonc @@ -0,0 +1,33 @@ +// Package definition for Gradle, installed from services.gradle.org. Gradle +// runs on the JVM, so the project also needs java. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "gradle", + "description": "Build automation tool for the JVM", + "homepage": "https://gradle.org", + "license": "Apache-2.0", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + + // One JSON document lists every version with its download checksum. + "versions": { + "type": "http_json", + "url": "https://services.gradle.org/versions/all", + "select": ".[] | select(.snapshot or .nightly or .releaseNightly or .broken or .rcFor != \"\" or .milestoneFor != \"\" | not) | .version" + }, + + "install": { + "type": "archive", + "url": "https://services.gradle.org/distributions/gradle-{{.Version}}-bin.zip", + "checksum": { + "type": "http_json", + "url": "https://services.gradle.org/versions/all", + "select": ".[] | select(.version == \"{{.Version}}\") | .checksum" + }, + "strip_components": 1 + }, + + "bin": ["bin"], + "binaries": ["gradle"] +} diff --git a/pkgs/helm/helm.pkg.jsonc b/pkgs/helm/helm.pkg.jsonc new file mode 100644 index 00000000000..cff02698221 --- /dev/null +++ b/pkgs/helm/helm.pkg.jsonc @@ -0,0 +1,24 @@ +// Package definition for Helm, installed from get.helm.sh. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "helm", + "description": "The package manager for Kubernetes", + "homepage": "https://helm.sh", + "license": "Apache-2.0", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + + "versions": { "type": "github_releases", "repo": "helm/helm" }, + + "install": { + "type": "archive", + "url": "https://get.helm.sh/helm-v{{.Version}}-{{.OS}}-{{.Arch}}.tar.gz", + "checksum": { "type": "sha256_url", "url": "{{.URL}}.sha256sum" }, + "strip_components": 1 + }, + + // The tarball holds -/helm. + "bin": ["."], + "binaries": ["helm"] +} diff --git a/pkgs/java/java.pkg.jsonc b/pkgs/java/java.pkg.jsonc new file mode 100644 index 00000000000..2a07d1f22c1 --- /dev/null +++ b/pkgs/java/java.pkg.jsonc @@ -0,0 +1,49 @@ +// Package definition for Java, installed as the Eclipse Temurin JDK from +// Adoptium. Versions are OpenJDK version strings such as "21.0.5+11", so +// `java@21` picks the newest Java 21 build. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "java", + "description": "Eclipse Temurin, the Adoptium build of the OpenJDK Java Development Kit", + "homepage": "https://adoptium.net", + "license": "GPL-2.0-with-classpath-exception", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + "os_map": { "darwin": "mac", "linux": "linux" }, + "arch_map": { "x86_64": "x64", "aarch64": "aarch64" }, + + // The API pages its results 50 at a time, newest first. Long-term + // support builds carry a ".0.LTS" suffix, which is dropped. + "versions": { + "type": "http_json", + "url": "https://api.adoptium.net/v3/info/release_versions?release_type=ga&vendor=eclipse&image_type=jdk&sort_order=DESC&page_size=50&page={{.Page}}", + "select": ".versions[].semver | rtrimstr(\".0.LTS\") | rtrimstr(\".LTS\")" + }, + + // Download names can't be derived from the version (respins are renamed, + // Java 8 uses "8u432b06"), so the artifact and its checksum come from the + // API's list of releases for the version's feature release. Java 8 has no + // build for Apple silicon; the API returns 404 and that platform is + // skipped. + "install": { + "type": "archive", + "lookup": { + "url": "https://api.adoptium.net/v3/assets/feature_releases/{{ index (splitList \".\" .Version) 0 }}/ga?os={{.OS}}&architecture={{.Arch}}&image_type=jdk&jvm_impl=hotspot&heap_size=normal&vendor=eclipse&page_size=50&sort_order=DESC", + "select": ".[] | select((.version_data.semver | rtrimstr(\".0.LTS\") | rtrimstr(\".LTS\")) == \"{{.Version}}\") | .binaries[0].package | {url: .link, checksum: .checksum}" + }, + "strip_components": 1, + // macOS builds are bundles; the JDK is in /Contents/Home. + "overrides": { + "aarch64-darwin": { "strip_components": 3 }, + "x86_64-darwin": { "strip_components": 3 } + } + }, + + "bin": ["bin"], + "binaries": ["java", "javac", "jar"], + + "env": { + "JAVA_HOME": "{{ .InstallDir }}" + } +} diff --git a/pkgs/kubectl/kubectl.pkg.jsonc b/pkgs/kubectl/kubectl.pkg.jsonc new file mode 100644 index 00000000000..239efb132c1 --- /dev/null +++ b/pkgs/kubectl/kubectl.pkg.jsonc @@ -0,0 +1,24 @@ +// Package definition for kubectl, installed from the Kubernetes release +// downloads at dl.k8s.io. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "kubectl", + "description": "Kubernetes command-line tool", + "homepage": "https://kubernetes.io/docs/reference/kubectl/", + "license": "Apache-2.0", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + + // Kubernetes releases are GitHub releases; alphas, betas and release + // candidates are marked as pre-releases. + "versions": { "type": "github_releases", "repo": "kubernetes/kubernetes" }, + + "install": { + "type": "binary", + "url": "https://dl.k8s.io/release/v{{.Version}}/bin/{{.OS}}/{{.Arch}}/kubectl", + "checksum": { "type": "sha256_url", "url": "{{.URL}}.sha256" } + }, + + "binaries": ["kubectl"] +} diff --git a/pkgs/maven/maven.pkg.jsonc b/pkgs/maven/maven.pkg.jsonc new file mode 100644 index 00000000000..a23eccab279 --- /dev/null +++ b/pkgs/maven/maven.pkg.jsonc @@ -0,0 +1,34 @@ +// Package definition for Apache Maven, installed from Maven Central. Maven +// runs on the JVM, so the project also needs java. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "maven", + "description": "Build automation and dependency management for Java", + "homepage": "https://maven.apache.org", + "license": "Apache-2.0", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + + "versions": { + "type": "http_json", + "url": "https://search.maven.org/solrsearch/select?q=g:org.apache.maven+AND+a:apache-maven&core=gav&rows=500&wt=json", + "select": ".response.docs[].v" + }, + + // Downloads come from Maven Central's CDN. Central only has digests for + // some releases, so the sha512 comes from the Apache release archive, + // which publishes one for every release since 3.5. + "install": { + "type": "archive", + "url": "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/{{.Version}}/apache-maven-{{.Version}}-bin.tar.gz", + "checksum": { + "type": "sha512_url", + "url": "https://archive.apache.org/dist/maven/maven-{{ index (splitList \".\" .Version) 0 }}/{{.Version}}/binaries/apache-maven-{{.Version}}-bin.tar.gz.sha512" + }, + "strip_components": 1 + }, + + "bin": ["bin"], + "binaries": ["mvn"] +} diff --git a/pkgs/nodejs/nodejs.pkg.jsonc b/pkgs/nodejs/nodejs.pkg.jsonc new file mode 100644 index 00000000000..901ec4bdad7 --- /dev/null +++ b/pkgs/nodejs/nodejs.pkg.jsonc @@ -0,0 +1,43 @@ +// Package definition for Node.js, installed from the official builds on +// nodejs.org. npm ships with every release; add pnpm or yarn as their own +// packages. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "nodejs", + "description": "JavaScript runtime built on Chrome's V8 engine, with npm", + "homepage": "https://nodejs.org", + "license": "MIT", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + "arch_map": { "x86_64": "x64", "aarch64": "arm64" }, + + // index.json lists every release, newest first, as "v22.11.0". + "versions": { + "type": "http_json", + "url": "https://nodejs.org/dist/index.json", + "select": ".[].version | ltrimstr(\"v\")" + }, + + // Releases that predate a platform (no darwin-arm64 before 16) have no + // entry in SHASUMS256.txt and are skipped for that platform. + "install": { + "type": "archive", + "url": "https://nodejs.org/dist/v{{.Version}}/node-v{{.Version}}-{{.OS}}-{{.Arch}}.tar.gz", + "checksum": { + "type": "sha256_url", + "url": "https://nodejs.org/dist/v{{.Version}}/SHASUMS256.txt" + }, + "strip_components": 1 + }, + + "bin": ["bin"], + "binaries": ["node", "npm", "npx"], + + // `npm install --global` writes into the project instead of the shared + // install, which every project using this version sees. + "env": { + "NPM_CONFIG_PREFIX": "{{ .Virtenv }}/npm-global", + "PATH": "{{ .Virtenv }}/npm-global/bin:$PATH" + } +} diff --git a/pkgs/pnpm/pnpm.pkg.jsonc b/pkgs/pnpm/pnpm.pkg.jsonc new file mode 100644 index 00000000000..99e981b3843 --- /dev/null +++ b/pkgs/pnpm/pnpm.pkg.jsonc @@ -0,0 +1,43 @@ +// Package definition for pnpm, installed as the standalone executable that +// bundles its own Node.js, so it works with or without nodejs installed. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "pnpm", + "description": "Fast, disk space efficient package manager", + "homepage": "https://pnpm.io", + "license": "MIT", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + "arch_map": { "x86_64": "x64", "aarch64": "arm64" }, + + "versions": { "type": "github_releases", "repo": "pnpm/pnpm" }, + + // Since v11 every GitHub release has a tarball per platform holding the + // pnpm executable and its dist/ directory. GitHub records a digest for + // each asset. + "install": { + "type": "archive", + "url": "https://github.com/pnpm/pnpm/releases/download/v{{.Version}}/pnpm-{{.OS}}-{{.Arch}}.tar.gz", + "checksum": { "type": "github_release", "repo": "pnpm/pnpm" }, + "version_overrides": [ + { + // Before v11 the executable was published to npm as + // @pnpm/-, and npm has a sha512 for every + // version. (The GitHub assets of that era have no digest.) + "versions": "<11", + "url": "https://registry.npmjs.org/@pnpm/{{ .OS | replace \"darwin\" \"macos\" }}-{{.Arch}}/-/{{ .OS | replace \"darwin\" \"macos\" }}-{{.Arch}}-{{.Version}}.tgz", + "checksum": { + "type": "http_json", + "url": "https://registry.npmjs.org/@pnpm/{{ .OS | replace \"darwin\" \"macos\" }}-{{.Arch}}/{{.Version}}", + "select": ".dist.integrity" + }, + "strip_components": 1 + } + ] + }, + + // Both layouts put the executable at the root of the install. + "bin": ["."], + "binaries": ["pnpm"] +} diff --git a/pkgs/python/python.pkg.jsonc b/pkgs/python/python.pkg.jsonc new file mode 100644 index 00000000000..233b3655bb3 --- /dev/null +++ b/pkgs/python/python.pkg.jsonc @@ -0,0 +1,50 @@ +// Package definition for CPython, installed from python-build-standalone, +// the relocatable builds that uv and mise install. The index of builds +// (URL and sha256 per version and platform) is the one uv maintains. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "python", + "description": "CPython. Devbox creates a virtual environment in $VENV_DIR (default .venv) and activates it, so pip installs stay out of the shared install. Switching versions recreates a virtual environment that Devbox created.", + "homepage": "https://www.python.org", + "license": "PSF-2.0", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + "arch_map": { "x86_64": "x86_64", "aarch64": "aarch64" }, + + // Versions are "3.13.1", or "3.14.0a1" for pre-releases. + "versions": { + "type": "http_json", + "url": "https://raw.githubusercontent.com/astral-sh/uv/main/crates/uv-python-managed/download-metadata.json", + "select": ".[] | select(.name == \"cpython\" and .variant == null) | \"\\(.major).\\(.minor).\\(.patch)\\(.prerelease)\"" + }, + + // The default build for a platform: no free-threading or debug variant, + // the baseline CPU level, glibc on Linux. + "install": { + "type": "archive", + "lookup": { + "url": "https://raw.githubusercontent.com/astral-sh/uv/main/crates/uv-python-managed/download-metadata.json", + "select": ".[] | select(.name == \"cpython\" and .variant == null and .arch.variant == null and .os == \"{{.OS}}\" and .arch.family == \"{{.Arch}}\" and (.libc == \"gnu\" or .libc == \"none\") and \"\\(.major).\\(.minor).\\(.patch)\\(.prerelease)\" == \"{{.Version}}\") | {url, checksum: .sha256}" + }, + "strip_components": 1 + }, + + "bin": ["bin"], + "binaries": ["python3", "pip3"], + + "env": { + "VENV_DIR": "{{ .DevboxProjectDir }}/.venv" + }, + "shell": { + "init_hook": [ + // Create the virtual environment, or recreate it when it was made + // by another version installed by Devbox. Leave any other one alone. + // Hooks run with `set -e`, hence `|| true` when there is no venv yet. + "_devbox_venv_home=\"$(sed -n 's/^home = //p' \"$VENV_DIR/pyvenv.cfg\" 2>/dev/null || true)\"", + "case \"$_devbox_venv_home\" in \"{{ .InstallDir }}/bin\") ;; \"{{ .CacheDir }}/python/\"*) \"{{ .InstallDir }}/bin/python3\" -m venv --clear \"$VENV_DIR\" ;; *) [ -e \"$VENV_DIR\" ] || \"{{ .InstallDir }}/bin/python3\" -m venv \"$VENV_DIR\" ;; esac", + "if [ \"$(sed -n 's/^home = //p' \"$VENV_DIR/pyvenv.cfg\" 2>/dev/null)\" = \"{{ .InstallDir }}/bin\" ]; then . \"$VENV_DIR/bin/activate\"; else echo \"devbox: not activating $VENV_DIR because it was not created from python {{ .Version }}; remove it to have Devbox recreate it\" >&2; fi", + "unset _devbox_venv_home" + ] + } +} diff --git a/pkgs/rust/rust.pkg.jsonc b/pkgs/rust/rust.pkg.jsonc new file mode 100644 index 00000000000..f20259cd491 --- /dev/null +++ b/pkgs/rust/rust.pkg.jsonc @@ -0,0 +1,51 @@ +// Package definition for Rust. Rust's own installer is rustup, which also +// provides `rustup target add` and `rustup component add`, so this +// definition installs rustup-init (verified against static.rust-lang.org) +// and has it install the requested toolchain. rustup verifies the +// toolchain against the channel manifest. +// +// Toolchains live in one shared RUSTUP_HOME in the Devbox cache, and +// RUSTUP_TOOLCHAIN selects the version, so switching versions in +// devbox.json switches the toolchain. RUSTUP_TOOLCHAIN takes precedence +// over a rust-toolchain.toml file. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "rust", + "description": "The Rust toolchain (rustc, cargo, rustfmt, clippy) managed by rustup", + "homepage": "https://www.rust-lang.org", + "license": "MIT OR Apache-2.0", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + // Rust target triples. + "os_map": { "darwin": "apple-darwin", "linux": "unknown-linux-gnu" }, + "arch_map": { "x86_64": "x86_64", "aarch64": "aarch64" }, + + // Every stable release is a GitHub release tagged "1.82.0". + "versions": { "type": "github_releases", "repo": "rust-lang/rust", "strip_prefix": "" }, + + // The rustup version is pinned so that installs are reproducible; it + // does not limit which toolchains can be installed. + "install": { + "type": "binary", + "url": "https://static.rust-lang.org/rustup/archive/1.29.2/{{.Arch}}-{{.OS}}/rustup-init", + "checksum": { "type": "sha256_url", "url": "{{.URL}}.sha256" }, + "bin_name": "rustup-init" + }, + + "hooks": { + "post_install": [ + // Installs rustup and its proxies (cargo, rustc, ...) into bin/ and + // the toolchain into the shared RUSTUP_HOME. + "RUSTUP_HOME=\"{{ .CacheDir }}/rustup\" CARGO_HOME=\"$DEVBOX_PKG_INSTALL_DIR\" RUSTUP_INIT_SKIP_PATH_CHECK=yes ./bin/rustup-init -y --no-modify-path --profile minimal --component rustfmt,clippy --default-toolchain {{.Version}}" + ] + }, + + "bin": ["bin"], + "binaries": ["rustup", "rustc", "cargo", "rustfmt", "cargo-clippy"], + + "env": { + "RUSTUP_HOME": "{{ .CacheDir }}/rustup", + "RUSTUP_TOOLCHAIN": "{{ .Version }}" + } +} diff --git a/pkgs/terraform/terraform.pkg.jsonc b/pkgs/terraform/terraform.pkg.jsonc new file mode 100644 index 00000000000..bf6eeca32fe --- /dev/null +++ b/pkgs/terraform/terraform.pkg.jsonc @@ -0,0 +1,29 @@ +// Package definition for Terraform, installed from releases.hashicorp.com. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "terraform", + "description": "Infrastructure as code tool", + "homepage": "https://www.terraform.io", + "license": "BUSL-1.1", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + + "versions": { + "type": "http_json", + "url": "https://releases.hashicorp.com/terraform/index.json", + "select": ".versions | keys[]" + }, + + "install": { + "type": "archive", + "url": "https://releases.hashicorp.com/terraform/{{.Version}}/terraform_{{.Version}}_{{.OS}}_{{.Arch}}.zip", + "checksum": { + "type": "sha256_url", + "url": "https://releases.hashicorp.com/terraform/{{.Version}}/terraform_{{.Version}}_SHA256SUMS" + } + }, + + "bin": ["."], + "binaries": ["terraform"] +} diff --git a/pkgs/uv/uv.pkg.jsonc b/pkgs/uv/uv.pkg.jsonc new file mode 100644 index 00000000000..633c053cfda --- /dev/null +++ b/pkgs/uv/uv.pkg.jsonc @@ -0,0 +1,27 @@ +// Package definition for uv, installed from its GitHub releases. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "uv", + "description": "An extremely fast Python package and project manager", + "homepage": "https://docs.astral.sh/uv", + "license": "MIT OR Apache-2.0", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + // Rust target triples. + "os_map": { "darwin": "apple-darwin", "linux": "unknown-linux-gnu" }, + "arch_map": { "x86_64": "x86_64", "aarch64": "aarch64" }, + + // Tags have no "v" prefix. + "versions": { "type": "github_releases", "repo": "astral-sh/uv", "strip_prefix": "" }, + + "install": { + "type": "archive", + "url": "https://github.com/astral-sh/uv/releases/download/{{.Version}}/uv-{{.Arch}}-{{.OS}}.tar.gz", + "checksum": { "type": "sha256_url", "url": "{{.URL}}.sha256" }, + "strip_components": 1 + }, + + "bin": ["."], + "binaries": ["uv", "uvx"] +} diff --git a/pkgs/yarn/yarn.pkg.jsonc b/pkgs/yarn/yarn.pkg.jsonc new file mode 100644 index 00000000000..8634ccc1fb8 --- /dev/null +++ b/pkgs/yarn/yarn.pkg.jsonc @@ -0,0 +1,54 @@ +// Package definition for Yarn, installed from the npm registry. Yarn is +// JavaScript, so the project also needs nodejs. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "yarn", + "description": "Package manager for JavaScript", + "homepage": "https://yarnpkg.com", + "license": "BSD-2-Clause", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + + // Yarn 2 and later ("berry") is published as @yarnpkg/cli-dist; Yarn 1 + // ("classic") stays in the yarn package. + "versions": [ + { + "type": "http_json", + "url": "https://registry.npmjs.org/@yarnpkg/cli-dist", + "select": ".versions | keys[]" + }, + { + "type": "http_json", + "url": "https://registry.npmjs.org/yarn", + "select": ".versions | keys[]" + } + ], + + "install": { + "type": "archive", + "url": "https://registry.npmjs.org/@yarnpkg/cli-dist/-/cli-dist-{{.Version}}.tgz", + "checksum": { + "type": "http_json", + "url": "https://registry.npmjs.org/@yarnpkg/cli-dist/{{.Version}}", + "select": ".dist.integrity" + }, + "strip_components": 1, + "version_overrides": [ + { + "versions": "<2", + "url": "https://registry.npmjs.org/yarn/-/yarn-{{.Version}}.tgz", + "checksum": { + "type": "http_json", + "url": "https://registry.npmjs.org/yarn/{{.Version}}", + "select": ".dist.integrity" + } + } + ] + }, + + "bin": ["bin"], + // The berry tarball ships bin/yarn without the executable bit; listing + // it here has Devbox set it. + "binaries": ["yarn"] +} diff --git a/pkgs/zig/zig.pkg.jsonc b/pkgs/zig/zig.pkg.jsonc new file mode 100644 index 00000000000..0f4f106a4eb --- /dev/null +++ b/pkgs/zig/zig.pkg.jsonc @@ -0,0 +1,35 @@ +// Package definition for Zig, installed from ziglang.org. +// Format: see specs/package-definitions.md. +{ + "schema": 1, + "name": "zig", + "description": "General-purpose programming language and toolchain", + "homepage": "https://ziglang.org", + "license": "MIT", + + "platforms": ["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"], + "os_map": { "darwin": "macos", "linux": "linux" }, + "arch_map": { "x86_64": "x86_64", "aarch64": "aarch64" }, + + // index.json is keyed by version, plus a "master" entry for nightlies. + "versions": { + "type": "http_json", + "url": "https://ziglang.org/download/index.json", + "select": "keys[] | select(. != \"master\")" + }, + + // The index has the tarball URL and its sha256 per platform. Tarball + // names changed in 0.14 ("zig-macos-aarch64" became + // "zig-aarch64-macos"), which the lookup absorbs. + "install": { + "type": "archive", + "lookup": { + "url": "https://ziglang.org/download/index.json", + "select": ".[\"{{.Version}}\"][\"{{.Arch}}-{{.OS}}\"] // empty | {url: .tarball, checksum: .shasum}" + }, + "strip_components": 1 + }, + + "bin": ["."], + "binaries": ["zig"] +} diff --git a/specs/package-definitions.md b/specs/package-definitions.md index 7bb998ec1ec..bfe69b92556 100644 --- a/specs/package-definitions.md +++ b/specs/package-definitions.md @@ -8,8 +8,9 @@ lands. Let a Devbox project install a tool from the upstream vendor's own binary distribution (go.dev, nodejs.org, GitHub releases, ...) instead of nixpkgs, -driven by a small declarative *package definition* file. Go is the first -package (see [`pkgs/go/go.pkg.jsonc`](../pkgs/go/go.pkg.jsonc)). Later, the +driven by a small declarative *package definition* file. Go was the first +package (see [`pkgs/go/go.pkg.jsonc`](../pkgs/go/go.pkg.jsonc)); the initial +set of 16 is listed under [Shipped packages](#shipped-packages). Later, the search service returns these definitions so that `devbox add go@1.22` transparently picks the vendor build. @@ -130,25 +131,38 @@ hatch. | `description`, `homepage`, `license` | no | Informational. `homepage` is shown by `devbox info`. | | `platforms` | yes | Non-empty list of nix system names the definition supports. | | `os_map`, `arch_map` | no | Overrides for the `{{.OS}}` and `{{.Arch}}` template variables. Keys are the nix OS (`darwin`, `linux`) and nix arch (`x86_64`, `aarch64`). | -| `versions` | yes | A version source. See below. | +| `versions` | yes | A version source, or a list of them. See below. | | `install` | yes | An install strategy. See below. | | `bin` | no | Directories relative to the install root that go on `PATH`. Default `["bin"]`. | -| `binaries` | no | Executable names expected on `PATH` after install. | +| `binaries` | no | Executable names expected in a `bin` directory after install. Install marks them executable (npm tarballs drop the bit) and fails if one is missing. | | `env`, `shell`, `create_files` | no | Plugin-compatible activation. See Activation. | -| `hooks.post_install` | no | Shell commands run once per installed version. | +| `hooks.post_install` | no | Shell commands run once per installed version by `/bin/sh` in the install root, with `DEVBOX_PKG_INSTALL_DIR`, `DEVBOX_PKG_VERSION` and `DEVBOX_PKG_SYSTEM` set and `/usr/bin:/bin` appended to `PATH`. | ### Version sources (`versions`) | `type` | Fields | Behaviour | |---|---|---| | `static` | `versions: [string]` | The list is the universe of versions. | -| `http_json` | `url`, `select` | GET `url`, parse JSON, evaluate the jq expression in `select`. Every string (or number) it emits is a version. | -| `github_releases` | `repo` (`owner/name`), `strip_prefix` (default `"v"`) | List releases through the GitHub API, skip drafts and pre-releases, strip the prefix from tag names. Honors `GITHUB_TOKEN`. | +| `http_json` | `url`, `select` | GET `url`, parse JSON, evaluate the jq expression in `select`. Every string (or number) it emits is a version. When `url` contains `{{.Page}}`, pages 0, 1, ... are fetched until one returns 404 or no versions. | +| `github_releases` | `repo` (`owner/name`), `strip_prefix` (default `"v"`) | List releases through the GitHub API, 100 per page, skip drafts and pre-releases, strip the prefix from tag names. Honors `GITHUB_TOKEN`. | | `script` | `run` | Planned. Run `run` with `sh -c`; one version per output line. | +`versions` may also be a list of sources, whose versions are combined. Yarn +needs this: 1.x is the `yarn` npm package and 2.x onward is +`@yarnpkg/cli-dist`. + +Paginated sources (`github_releases`, and `http_json` with `{{.Page}}`) are +read lazily: resolution stops at the first page that has a version matching +the request, so `latest` costs one request and `kubectl@1.30` reads as many +pages as it takes to reach 1.30. Sources should list newest versions first. +At most 20 pages are read. + Version strings are compared as dotted numeric segments, falling back to -string comparison. Versions containing a hyphen (`1.23.0-rc1`) are treated as -pre-releases and only match when requested exactly. +string comparison. Build metadata after `+` (OpenJDK's `21.0.5+11`) only +breaks ties, so a vendor respin (`21.0.12+101`) sorts after the original +build (`21.0.12+8`). Versions whose part before `+` contains a hyphen or a +letter (`1.23.0-rc1`, `3.14.0a1`) are pre-releases and only match when +requested exactly. Matching a requested version against the list: @@ -157,21 +171,77 @@ Matching a requested version against the list: | `latest` | The highest non-pre-release version. | | `1.22` | The highest version equal to `1.22` or starting with `1.22.`. | | `1.22.3` | That version exactly (also by the prefix rule, so `1.22.3` matches `1.22.3.1`-style versions if the exact one is absent). | +| `21.0.5` | The highest `21.0.5+`. | ### Install strategies (`install`) -`install.type` picks how a version lands on disk. `install.overrides` is a -map from nix system to a partial `install` object whose fields replace the -defaults for that system (for example a different `url` or `strip_components` -on one platform). +`install.type` picks how a version lands on disk. | `type` | Fields | Behaviour | |---|---|---| -| `archive` | `url`, `checksum`, `strip_components` (default `0`) | Download the archive, verify it, extract it into the install root. Supports `.tar.gz`, `.tgz`, `.tar.xz`, `.tar.bz2`, `.tar.zst` and `.zip`. | -| `binary` | `url`, `checksum`, `bin_name` (default: `name`) | Download a single executable to `/bin/` and mark it executable. | +| `archive` | `url` and `checksum`, or `lookup`; `strip_components` (default `0`) | Download the archive, verify it, extract it into the install root. Supports `.tar.gz`, `.tgz`, `.tar.xz`, `.tar.bz2`, `.tar.zst` and `.zip`. | +| `binary` | `url` and `checksum`, or `lookup`; `bin_name` (default: `name`) | Download a single executable to `/bin/` and mark it executable. | | `manager` | `requires`, `env`, `install`, `locate` | Planned. Delegate to another definition that owns version management (see below). | | `script` | `run` | Planned. Escape hatch: run `run` with the install root as `$DEVBOX_PKG_INSTALL_DIR`. | +#### Looking up the artifact in an index (`install.lookup`) + +Some vendors' download names can't be derived from the version: +python-build-standalone puts a build date in every file name, Temurin renames +respins (`21.0.12+101` ships as `..._21.0.12.1_1.tar.gz`), and zig renamed +its tarballs in 0.14. Their indexes list the exact URL and checksum instead. +`lookup` replaces `url` and `checksum`: + +```jsonc +"install": { + "type": "archive", + "lookup": { + "url": "https://ziglang.org/download/index.json", + "select": ".[\"{{.Version}}\"][\"{{.Arch}}-{{.OS}}\"] // empty | {url: .tarball, checksum: .shasum}" + } +} +``` + +`url` and `select` are templates. `select` is a jq expression that must +produce an object with `url` and, optionally, `checksum` (any digest format +below). If it produces nothing, or `url` returns 404, the vendor has no +artifact of that version for that system. + +#### Overrides + +`install.overrides` maps a nix system to a partial `install` object whose +fields replace the defaults for that system (for example a different +`strip_components` for macOS bundles). + +`install.version_overrides` is a list of partial `install` objects for vendors +that changed how they ship artifacts. Each has a `versions` constraint: one or +more comparisons with `<`, `<=`, `>`, `>=`, `=` or `!=`, separated by spaces +or commas, that must all hold (`"<11"`, `">=2, <4"`). A bare version matches +itself and every more specific version. The first entry that matches applies. +An entry may have its own `overrides` per system but no nested +`version_overrides`. + +```jsonc +"install": { + "type": "archive", + "url": "https://github.com/pnpm/pnpm/releases/download/v{{.Version}}/pnpm-{{.OS}}-{{.Arch}}.tar.gz", + "checksum": { "type": "github_release", "repo": "pnpm/pnpm" }, + "version_overrides": [ + { + "versions": "<11", + "url": "https://registry.npmjs.org/@pnpm/{{ .OS | replace \"darwin\" \"macos\" }}-{{.Arch}}/-/...", + "checksum": { "type": "http_json", "url": "...", "select": ".dist.integrity" }, + "strip_components": 1 + } + ] +} +``` + +Overrides apply in this order: the matching version override, the +definition's override for the system, then the version override's own +override for the system. `url` and `lookup` are alternatives, so an override +that sets one clears the other. + Why two real strategies: `archive` covers more than it first appears. Go, nodejs, deno, bun, zig, Temurin JDK and python (via python-build-standalone, the same builds uv installs) all ship per-platform archives, so fnm, nvm and @@ -179,6 +249,11 @@ pyenv are not needed for them. `manager` is for ecosystems where the vendor ships no usable archive or the manager is the ecosystem's contract: rustup for rust, ruby (no official binaries), php on Linux. +Until `manager` exists, the shipped `rust` definition gets the same result +with `binary` plus a `post_install` hook: it installs the checksummed +`rustup-init` and runs it to install the requested toolchain into a shared +`RUSTUP_HOME`, and its `env` sets `RUSTUP_TOOLCHAIN` to the version. + A manager-backed definition will look like this once implemented: ```jsonc @@ -217,14 +292,33 @@ verification through sum.golang.org, so it stays an alternate definition. | `type` | Fields | Behaviour | |---|---|---| -| `http_json` | `url`, `select` | GET `url`, evaluate the jq expression; the first string result is the hex sha256. | +| `http_json` | `url`, `select` | GET `url`, evaluate the jq expression; the first string result is the digest. Covers JSON indexes and the npm registry's `dist.integrity`. | | `sha256_url` | `url` | GET `url`. The body is either a bare hex digest or a `sha256sum`-style list (` ` per line) matched by `{{.Filename}}`. Covers nodejs `SHASUMS256.txt` and the common `.sha256` convention. | +| `sha512_url` | `url` | Like `sha256_url` for sha512 digests (Apache's `.sha512` files). | +| `github_release` | `repo`, `tag` (default `v{{.Version}}`) | The digest GitHub computes for every release asset, read through the GitHub API (honors `GITHUB_TOKEN`). Assets uploaded before GitHub started computing digests (mid 2025) have none, which is treated like `none`. | | `none` | | No vendor-published checksum. `devbox add` records the digest it observes for the current system (trust on first use). Other systems are filled in lazily when a machine of that system installs, matching how nix store paths are backfilled today. | +Digests may be sha256 or sha512, as hex (the algorithm follows from the +length), as `sha256:`/`sha512:`, or as an SRI string +(`sha512-`). + Checksums are mandatory when installing from an existing lockfile entry: the -downloaded artifact must match the `sha256` recorded for the system, or the +downloaded artifact must match the digest recorded for the system, or the install fails. +#### Platforms a version is not published for + +Vendors add and drop platforms over time: Node.js had no Apple silicon +build before 16, Java 8 has none at all, Temurin 27 has no Intel macOS +build. When the checksum source shows that an artifact does not exist (the +checksum file or lookup index returns 404, a checksum list has no entry for +the file, a jq expression matches nothing, the GitHub release has no such +asset), that system is left out of the lock entry. Locking fails only when no +platform has an artifact. Installing on a system that the lock entry leaves +out fails with an error that names the platforms the version is available +for. Definitions with checksum `none` cannot tell, so their lock entries +list every platform. + ### Template variables Templates use Go `text/template` with `missingkey=error`. Available @@ -238,10 +332,13 @@ variables: | `{{.URL}}`, `{{.Filename}}` | `checksum`, `hooks` | The rendered artifact URL and its last path segment. | | `{{.InstallDir}}` | `env`, `shell`, `create_files`, `hooks` | Install root for this version and system. | | `{{.CacheDir}}` | `env`, `shell`, `hooks`, `install.env` | `$XDG_CACHE_HOME/devbox/pkgs`. | +| `{{.Page}}` | `versions.url` (`http_json`) | The 0-based page number. See Version sources. | -Functions: `trimPrefix`, `trimSuffix`, `replace`, `lower`, `upper`. Arguments -follow the sprig order with the string last, so pipelines read naturally: -`{{ .Version | trimPrefix "v" }}`, `{{ replace "." "_" .Version }}`. +Functions: `trimPrefix`, `trimSuffix`, `replace`, `lower`, `upper`, +`splitList`, plus the `text/template` builtins (`eq`, `index`, `urlquery`, +...). Arguments follow the sprig order with the string last, so pipelines read +naturally: `{{ .Version | trimPrefix "v" }}`, `{{ replace "." "_" .Version }}`, +`{{ index (splitList "." .Version) 0 }}` for the major version. ### Activation @@ -284,8 +381,11 @@ for every install strategy. refuses to install from a definition whose hash differs. For local definitions it is informational; edits are picked up through the state hash and trigger a reinstall. -- `systems.*.url` and `systems.*.sha256` are new `omitempty` fields on the - existing per-system struct. `lockfile_version` stays `1`. +- `systems.*.url`, `systems.*.sha256` and `systems.*.sha512` are new + `omitempty` fields on the existing per-system struct. `lockfile_version` + stays `1`. +- A system missing from `systems` means the vendor does not publish this + version for it. - Older Devbox binaries reject the `pkg:` package string. Projects that adopt package definitions should set `devbox_version` in `devbox.json`. @@ -295,8 +395,9 @@ cache. Garbage collection of unused versions is a follow-up (`devbox cache gc`). Each install directory carries a marker with the *install fingerprint* of -the definition: a hash of the install strategy, `bin`, `hooks`, `os_map` -and `arch_map`. When a definition's install recipe changes for a version +the definition: a hash of the install strategy that applies to the version +and system (after overrides), `bin`, `binaries`, `hooks`, `os_map` and +`arch_map`. When a definition's install recipe changes for a version that is already installed, the next install redoes it. Activation-only edits (`env`, `shell`, `create_files`) do not trigger a reinstall; they are applied at shell time. @@ -305,6 +406,36 @@ Trust-on-first-use digests are written to `devbox.lock` only by the install step (`devbox add`, `devbox install`, `devbox update`), never while computing the environment for `devbox shell` or `devbox shellenv`. +## Shipped packages + +Definitions live in `pkgs//.pkg.jsonc`, each next to a +testscript (`.test.txt`) that installs two versions and switches +between them. All of them support the four platforms; a version a vendor does +not publish for a platform is skipped for it (see above). + +| Package | Source | Versions from | Checksum | Notes | +|---|---|---|---|---| +| `go` | go.dev | go.dev JSON index | index (`http_json`) | Sets `GOTOOLCHAIN=local` like the nixpkgs go, so the pinned version runs even when go.mod asks for a newer one. | +| `nodejs` | nodejs.org | `index.json` | `SHASUMS256.txt` | Includes npm (and corepack before 25). `npm install -g` goes to the project's virtenv. | +| `pnpm` | GitHub releases (11+), npm `@pnpm/-` (<11) | GitHub releases | GitHub asset digest; npm sha512 integrity | Standalone executable with its own Node.js. `version_overrides` for the layout change in 11. | +| `yarn` | npm `@yarnpkg/cli-dist` (2+), `yarn` (1.x) | both npm packages (list of sources) | npm sha512 integrity | Needs `nodejs`. `binaries` restores the executable bit npm drops. | +| `python` | python-build-standalone | uv's `download-metadata.json` | index (`lookup`) | Creates and activates `$VENV_DIR` (default `.venv`), recreating it when the version changes, so pip never writes to the shared install. | +| `uv` | GitHub releases | GitHub releases | `.sha256` | | +| `rust` | static.rust-lang.org (rustup-init) | rust-lang/rust GitHub releases | `rustup-init.sha256`, then rustup's manifest | `post_install` runs rustup-init; toolchains share `RUSTUP_HOME` in the cache; `RUSTUP_TOOLCHAIN` selects the version. | +| `java` | Adoptium (Eclipse Temurin JDK) | Adoptium API, paged | index (`lookup`) | Versions are `21.0.5+11`. `JAVA_HOME` is set. macOS builds are bundles, hence a per-system `strip_components`. | +| `gradle` | services.gradle.org | `versions/all` | index (`http_json`) | Needs `java`. | +| `maven` | Maven Central | Maven Central search API | Apache archive `.sha512` | Needs `java`. Central has digests for only some releases. | +| `deno` | GitHub releases | GitHub releases | `.sha256sum` | Releases before 2.0.1 have no checksum (trust on first use). | +| `bun` | GitHub releases | GitHub releases | `SHASUMS256.txt` | `post_install` links `bunx`. | +| `zig` | ziglang.org | `index.json` | index (`lookup`) | | +| `terraform` | releases.hashicorp.com | `index.json` | `SHA256SUMS` | | +| `kubectl` | dl.k8s.io | kubernetes/kubernetes GitHub releases | `.sha256` | | +| `helm` | get.helm.sh | helm/helm GitHub releases | `.sha256sum` | | + +Not shipped yet: ruby and php (no official binaries; `manager`), dotnet, +erlang/elixir, and tools that are a one-line `binary` definition each (gh, +jq, just, ...). + ## Security The trust model equals plugins today: a definition is remote JSON that can @@ -367,6 +498,38 @@ flag is off, `pkg:` strings are not recognized at all. - A definition whose `platforms` exclude the current system fails at install time with a hint to use `--exclude-platform`. +### PR 4: initial package set + +- `pkgs//.pkg.jsonc` for the packages in + [Shipped packages](#shipped-packages). +- This spec: lists of version sources, paginated sources, versions with + `+build`, `install.lookup`, `install.version_overrides`, `sha512_url` and + `github_release` checksums, sha512 and SRI digests, platforms a version is + not published for, and `binaries` checks. + +### PR 5: support for the initial package set + +- `internal/pkgdef`: everything listed under PR 4, plus memoized metadata + requests so locking four platforms fetches an index once, `/bin/sh` and a + fallback `PATH` for hooks, and an install fingerprint per version. +- `lock.SystemInfo.Sha512`; lock entries omit systems without an artifact, + and installing on such a system is an error that lists where the version + is available. +- `ensureStateIsUpToDate` reloads plugin configs after installing, so a + version changed in devbox.json gets its definition's env and init hook in + the same run. + +### PR 6: tests for the initial package set + +- `pkgs//.test.txt`: install a version from devbox.json, switch + with `devbox add`, switch back by editing devbox.json, checking the + version (and env) each time. Run by `TestPackageDefinitions` when + `DEVBOX_RUN_PKGDEF_TESTS=1`, and by a CI workflow on Linux and macOS when + `pkgs/` or the implementation changes and weekly to catch vendor changes. +- Every shipped definition parses and is named after its directory (always + runs); every shipped definition resolves `latest` and locks it against the + real vendor (network, same flag). + ### Later - Search service: `/v2/resolve` returns `source: "pkg-def"` plus the @@ -389,6 +552,18 @@ flag is off, `pkg:` strings are not recognized at all. entry today because canonical names differ (`pkg:./go.pkg.jsonc` vs `go`). Decide whether it should once search returns definitions. - `devbox global` shares `computeEnv` and should work unchanged; verify. +- Vendor metadata is a dependency: python's index is a file in uv's + repository (it moved once already), Java depends on the Adoptium API, and + Maven on search.maven.org. A broken source fails `devbox add` and + `devbox update` only; locked projects keep installing from the recorded + URLs. The weekly CI run is the early warning; the search service could + serve these indexes later. +- Version lookups through the GitHub API are limited to 60 requests an hour + without `GITHUB_TOKEN`. Pagination is lazy and responses are memoized, so a + typical `devbox add` makes one or two requests. +- Hooks run with the user's environment. The rust definition depends on + rustup-init flags staying compatible; the `manager` strategy would make + that contract explicit. - Windows and `devbox generate` images are out of scope for the first cut. ## Status @@ -401,6 +576,9 @@ flag is off, `pkg:` strings are not recognized at all. | Lockfile fields `definition_hash`, `systems.*.url`, `systems.*.sha256` | in review (PR 3) | | Activation through the plugin manager (`env`, `shell`, `create_files`) | in review (PR 3) | | Trust-on-first-use checksum recording | in review (PR 3) | +| Initial package set (16 definitions) | in review (PR 4) | +| `lookup`, `version_overrides`, version source lists, pagination, sha512, `github_release` checksums, per-platform availability | in review (PR 5) | +| Per-package install and version-switch tests, CI workflow | in review (PR 6) | | `manager` strategy | not started | | `script` strategy | not started | | `github:` refs | not started |