From 2a66b5b0d18829318acc04cf1aaa874dba9c11bf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 10:05:06 +0000 Subject: [PATCH] feature(mise): add mise dev container feature Installs mise from its GitHub release into /usr/local/bin, verifying the release checksums against their minisign signature with the mise public key vendored in the feature, the same key and method the mise image uses. The glibc build is installed where it runs and the statically linked musl build otherwise, so releases that outgrew an older Debian's glibc still work there. Tool installs and the download cache go to /var/lib/mise and /var/cache/mise, which the feature mounts volumes on and points MISE_DATA_DIR and MISE_CACHE_DIR at, with the shims directory on PATH. Both directories are owned by a mise system group with the setgid bit, following the claude-code feature, so they stay writable after updateRemoteUserUID renumbers the remote user. The minisign key is registered with update-material.sh so the weekly workflow refreshes it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Ti123BqzFGF4RaFf4qM5XJ --- src/mise/NOTES.md | 154 ++++++++++++++++++++ src/mise/README.md | 178 +++++++++++++++++++++++ src/mise/devcontainer-feature.json | 37 +++++ src/mise/install.sh | 223 +++++++++++++++++++++++++++++ src/mise/mise-minisign.pub | 2 + src/mise/smoke_test.sh | 67 +++++++++ 6 files changed, 661 insertions(+) create mode 100644 src/mise/NOTES.md create mode 100644 src/mise/README.md create mode 100644 src/mise/devcontainer-feature.json create mode 100755 src/mise/install.sh create mode 100644 src/mise/mise-minisign.pub create mode 100755 src/mise/smoke_test.sh diff --git a/src/mise/NOTES.md b/src/mise/NOTES.md new file mode 100644 index 0000000..85ba0f3 --- /dev/null +++ b/src/mise/NOTES.md @@ -0,0 +1,154 @@ +The official release binary from [GitHub Releases](https://github.com/jdx/mise/releases) +is installed as `/usr/local/bin/mise`, and the directories mise installs tools into +and caches downloads in are placed on volumes the feature declares. + +## Requirements + +- A `linux-x64` or `linux-arm64` image. The glibc build is installed where the + image's glibc is new enough to run it, and the statically linked musl build + otherwise, so any Debian or Ubuntu release works. +- `wget` (or `curl`), `minisign`, `sha256sum` and a CA bundle, for the download and + the signature check. Any of those the image is missing are installed with + `apt-get`, so an image without them has to be Debian or Ubuntu based; an image + that already has them needs no package manager at all. + +## Usage + +The defaults install the newest release. To pin a release instead: + +```json +"features": { + "ghcr.io/bare-devcontainer/features/mise:1": { + "version": "2026.9.10" + } +} +``` + +`version` accepts `latest` or an exact version such as `2026.9.10`. `latest` is +resolved at install time, so a rebuild picks up newer releases. + +Not every project needs this feature: on the +[`ghcr.io/bare-devcontainer/mise`](https://github.com/bare-devcontainer/images/tree/main/mise) +image, mise is already there. The feature is for adding mise to any other image, such +as a language image that needs a second toolchain or a linter the project pins. + +## Installed software + +- mise, installed as `/usr/local/bin/mise`, owned by root. + +Nothing else: no tool is installed until the project asks for it. `mise install` +installs everything a project's `mise.toml` declares, and `mise exec @ +-- ` runs a one-off without declaring anything. To install the project's tools +when the container is created rather than on first use, run `mise install` from a +`postCreateCommand`. + +The shims directory is on `PATH`, so a tool resolves as soon as mise installs it, +for editors and other programs that do not load the shell configuration as well as +in the terminal. `mise activate` can be added to the shell configuration for the +[features shims do not cover](https://mise.jdx.dev/dev-tools/shims.html), such as +environment variables declared in `mise.toml`. + +## Tool installs and download cache + +mise keeps its tool installs in `MISE_DATA_DIR` and its download cache in +`MISE_CACHE_DIR`. The feature declares a volume for each, so a rebuild neither +reinstalls the tools nor downloads them again, and points mise at them: + +```json +"containerEnv": { + "MISE_DATA_DIR": "/var/lib/mise", + "MISE_CACHE_DIR": "/var/cache/mise", + "PATH": "/var/lib/mise/shims:${PATH}" +}, +"mounts": [ + { + "source": "${devcontainerId}-mise-data", + "target": "/var/lib/mise", + "type": "volume" + }, + { + "source": "${devcontainerId}-mise-cache", + "target": "/var/cache/mise", + "type": "volume" + } +] +``` + +A feature's mount target is static and cannot reference the remote user's home +directory, so the volumes are mounted under `/var` and the environment variables +move the directories there. That keeps the mounts working whatever the image uses +as its remote user. The shims directory moves with `MISE_DATA_DIR`, which is why +`PATH` names it under `/var/lib/mise`. + +The volumes are seeded from the image on first use, ownership included. The Dev +Containers CLI renumbers the remote user to the host user's UID and GID on Linux, +but only chowns the home directory, so a directory owned by a build-time UID would +end up unreachable. The install script therefore creates a `mise` system group, +adds the remote user to it, and makes both directories group-writable and setgid: +group membership is recorded by name and is unaffected by the renumbering. + +One case is not covered. If an existing volume is reused after the host user's UID +changed, files written under the old UID keep their owner-only modes and mise cannot +replace them; clear them with `rm -rf /var/lib/mise/* /var/cache/mise/*` (this +removes the installed tools) or remove the volumes. Files created afterwards are +unaffected. + +The volumes are per dev container (`${devcontainerId}`) and are not shared between +projects. Mounts declared by a feature cannot be disabled from `devcontainer.json`, +but `containerEnv` there takes precedence over a feature's, so setting +`MISE_DATA_DIR` or `MISE_CACHE_DIR` points mise somewhere else and leaves the volume +mounted and unused. `PATH` then needs the new shims directory as well. + +mise's configuration (`~/.config/mise`) and state, including which `mise.toml` +files have been trusted (`~/.local/state/mise`), stay in the remote user's home +directory and are recreated by a rebuild. + +## Not installed + +- **No language runtime or tool.** mise resolves the versions the project declares + in `mise.toml` or in idiomatic per-language files such as `.node-version`. +- **No development headers beyond libc.** A backend that downloads prebuilt + binaries works as-is; one that builds a runtime from source needs the `-dev` + packages of the libraries it links against. +- **No shell activation.** The shell configuration is left untouched; the shims + directory on `PATH` is what makes installed tools resolve. + +## Supply chain + +The binary is downloaded from `https://github.com/jdx/mise/releases/`, and its +checksum is verified against `SHASUMS256.txt`, whose minisign signature is verified +with mise's public key. The key is vendored with the feature and read from there, +so the signature is checked against a key reviewed in this repository rather than +one fetched at install time. It is a copy of +[`minisign.pub`](https://github.com/jdx/mise/blob/main/minisign.pub) from the mise +repository, refreshed by this repository's `Update Trusted Material` workflow. + +To see the key before pinning the feature: + +```sh +cat src/mise/mise-minisign.pub +``` + +This covers the mise binary only. Tools that mise installs at runtime are fetched +from their own upstreams under mise's own verification, which +[`MISE_PARANOID`](https://mise.jdx.dev/paranoid.html) makes stricter. + +Everything downloaded during installation goes to a temporary directory that is +removed when the script exits. + +## Tips + +- For the tags this feature is published under, see + [Versions and pinning](https://github.com/bare-devcontainer/features#versions-and-pinning). +- To have mise re-verify the provenance of the tools it installs on every install + and refuse untrusted configuration, set `MISE_PARANOID` in `containerEnv`: + + ```json + "containerEnv": { + "MISE_PARANOID": "1" + } + ``` + +- The project's `mise.toml` has to be trusted before mise reads it, and a rebuild + clears that trust. Set `MISE_TRUSTED_CONFIG_PATHS` to the workspace folder in + `containerEnv` to trust it up front. diff --git a/src/mise/README.md b/src/mise/README.md new file mode 100644 index 0000000..730209e --- /dev/null +++ b/src/mise/README.md @@ -0,0 +1,178 @@ + +# mise (mise) + +Installs mise, the polyglot tool and runtime manager, from its GitHub release, verified against the mise minisign key, with its tool installs and download cache kept on volumes. + +## Example Usage + +```json +"features": { + "ghcr.io/bare-devcontainer/features/mise:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| version | mise version to install. Use "latest" for the newest release, or an exact version such as "2026.9.10". | string | latest | + +The official release binary from [GitHub Releases](https://github.com/jdx/mise/releases) +is installed as `/usr/local/bin/mise`, and the directories mise installs tools into +and caches downloads in are placed on volumes the feature declares. + +## Requirements + +- A `linux-x64` or `linux-arm64` image. The glibc build is installed where the + image's glibc is new enough to run it, and the statically linked musl build + otherwise, so any Debian or Ubuntu release works. +- `wget` (or `curl`), `minisign`, `sha256sum` and a CA bundle, for the download and + the signature check. Any of those the image is missing are installed with + `apt-get`, so an image without them has to be Debian or Ubuntu based; an image + that already has them needs no package manager at all. + +## Usage + +The defaults install the newest release. To pin a release instead: + +```json +"features": { + "ghcr.io/bare-devcontainer/features/mise:1": { + "version": "2026.9.10" + } +} +``` + +`version` accepts `latest` or an exact version such as `2026.9.10`. `latest` is +resolved at install time, so a rebuild picks up newer releases. + +Not every project needs this feature: on the +[`ghcr.io/bare-devcontainer/mise`](https://github.com/bare-devcontainer/images/tree/main/mise) +image, mise is already there. The feature is for adding mise to any other image, such +as a language image that needs a second toolchain or a linter the project pins. + +## Installed software + +- mise, installed as `/usr/local/bin/mise`, owned by root. + +Nothing else: no tool is installed until the project asks for it. `mise install` +installs everything a project's `mise.toml` declares, and `mise exec @ +-- ` runs a one-off without declaring anything. To install the project's tools +when the container is created rather than on first use, run `mise install` from a +`postCreateCommand`. + +The shims directory is on `PATH`, so a tool resolves as soon as mise installs it, +for editors and other programs that do not load the shell configuration as well as +in the terminal. `mise activate` can be added to the shell configuration for the +[features shims do not cover](https://mise.jdx.dev/dev-tools/shims.html), such as +environment variables declared in `mise.toml`. + +## Tool installs and download cache + +mise keeps its tool installs in `MISE_DATA_DIR` and its download cache in +`MISE_CACHE_DIR`. The feature declares a volume for each, so a rebuild neither +reinstalls the tools nor downloads them again, and points mise at them: + +```json +"containerEnv": { + "MISE_DATA_DIR": "/var/lib/mise", + "MISE_CACHE_DIR": "/var/cache/mise", + "PATH": "/var/lib/mise/shims:${PATH}" +}, +"mounts": [ + { + "source": "${devcontainerId}-mise-data", + "target": "/var/lib/mise", + "type": "volume" + }, + { + "source": "${devcontainerId}-mise-cache", + "target": "/var/cache/mise", + "type": "volume" + } +] +``` + +A feature's mount target is static and cannot reference the remote user's home +directory, so the volumes are mounted under `/var` and the environment variables +move the directories there. That keeps the mounts working whatever the image uses +as its remote user. The shims directory moves with `MISE_DATA_DIR`, which is why +`PATH` names it under `/var/lib/mise`. + +The volumes are seeded from the image on first use, ownership included. The Dev +Containers CLI renumbers the remote user to the host user's UID and GID on Linux, +but only chowns the home directory, so a directory owned by a build-time UID would +end up unreachable. The install script therefore creates a `mise` system group, +adds the remote user to it, and makes both directories group-writable and setgid: +group membership is recorded by name and is unaffected by the renumbering. + +One case is not covered. If an existing volume is reused after the host user's UID +changed, files written under the old UID keep their owner-only modes and mise cannot +replace them; clear them with `rm -rf /var/lib/mise/* /var/cache/mise/*` (this +removes the installed tools) or remove the volumes. Files created afterwards are +unaffected. + +The volumes are per dev container (`${devcontainerId}`) and are not shared between +projects. Mounts declared by a feature cannot be disabled from `devcontainer.json`, +but `containerEnv` there takes precedence over a feature's, so setting +`MISE_DATA_DIR` or `MISE_CACHE_DIR` points mise somewhere else and leaves the volume +mounted and unused. `PATH` then needs the new shims directory as well. + +mise's configuration (`~/.config/mise`) and state, including which `mise.toml` +files have been trusted (`~/.local/state/mise`), stay in the remote user's home +directory and are recreated by a rebuild. + +## Not installed + +- **No language runtime or tool.** mise resolves the versions the project declares + in `mise.toml` or in idiomatic per-language files such as `.node-version`. +- **No development headers beyond libc.** A backend that downloads prebuilt + binaries works as-is; one that builds a runtime from source needs the `-dev` + packages of the libraries it links against. +- **No shell activation.** The shell configuration is left untouched; the shims + directory on `PATH` is what makes installed tools resolve. + +## Supply chain + +The binary is downloaded from `https://github.com/jdx/mise/releases/`, and its +checksum is verified against `SHASUMS256.txt`, whose minisign signature is verified +with mise's public key. The key is vendored with the feature and read from there, +so the signature is checked against a key reviewed in this repository rather than +one fetched at install time. It is a copy of +[`minisign.pub`](https://github.com/jdx/mise/blob/main/minisign.pub) from the mise +repository, refreshed by this repository's `Update Trusted Material` workflow. + +To see the key before pinning the feature: + +```sh +cat src/mise/mise-minisign.pub +``` + +This covers the mise binary only. Tools that mise installs at runtime are fetched +from their own upstreams under mise's own verification, which +[`MISE_PARANOID`](https://mise.jdx.dev/paranoid.html) makes stricter. + +Everything downloaded during installation goes to a temporary directory that is +removed when the script exits. + +## Tips + +- For the tags this feature is published under, see + [Versions and pinning](https://github.com/bare-devcontainer/features#versions-and-pinning). +- To have mise re-verify the provenance of the tools it installs on every install + and refuse untrusted configuration, set `MISE_PARANOID` in `containerEnv`: + + ```json + "containerEnv": { + "MISE_PARANOID": "1" + } + ``` + +- The project's `mise.toml` has to be trusted before mise reads it, and a rebuild + clears that trust. Set `MISE_TRUSTED_CONFIG_PATHS` to the workspace folder in + `containerEnv` to trust it up front. + + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/bare-devcontainer/features/blob/main/src/mise/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/mise/devcontainer-feature.json b/src/mise/devcontainer-feature.json new file mode 100644 index 0000000..8e7d446 --- /dev/null +++ b/src/mise/devcontainer-feature.json @@ -0,0 +1,37 @@ +{ + "id": "mise", + "version": "1.0.0", + "name": "mise", + "documentationURL": "https://github.com/bare-devcontainer/features/tree/main/src/mise", + "description": "Installs mise, the polyglot tool and runtime manager, from its GitHub release, verified against the mise minisign key, with its tool installs and download cache kept on volumes.", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest" + ], + "default": "latest", + "description": "mise version to install. Use \"latest\" for the newest release, or an exact version such as \"2026.9.10\"." + } + }, + "containerEnv": { + "MISE_DATA_DIR": "/var/lib/mise", + "MISE_CACHE_DIR": "/var/cache/mise", + "PATH": "/var/lib/mise/shims:${PATH}" + }, + "mounts": [ + { + "source": "${devcontainerId}-mise-data", + "target": "/var/lib/mise", + "type": "volume" + }, + { + "source": "${devcontainerId}-mise-cache", + "target": "/var/cache/mise", + "type": "volume" + } + ], + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ] +} diff --git a/src/mise/install.sh b/src/mise/install.sh new file mode 100755 index 0000000..e729626 --- /dev/null +++ b/src/mise/install.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash +# +# Installs mise into /usr/local/bin from the official GitHub release, mirroring +# the setup of the ghcr.io/bare-devcontainer/mise image: the release binary is +# checked against SHASUMS256.txt, whose minisign signature is verified with the +# vendored mise public key. The directories mise installs tools into and caches +# downloads in are prepared for the remote user, at the paths the feature +# mounts volumes on and points MISE_DATA_DIR and MISE_CACHE_DIR at. +# +# Expected environment variables, from this feature's own options: +# +# VERSION mise version to install: "latest", or an exact version +# such as "2026.9.10". +# +# and from the Dev Container specification, injected by the CLI: +# +# _REMOTE_USER The account the container is attached as, and therefore +# the one mise installs tools as. + +set -euo pipefail + +VERSION="${VERSION:-latest}" + +FEATURE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PUBLIC_KEY="${FEATURE_DIR}/mise-minisign.pub" +PREFIX="/usr/local" +RELEASES_URL="https://github.com/jdx/mise/releases" +# Coupled to containerEnv and mounts in devcontainer-feature.json. +DATA_DIR="/var/lib/mise" +CACHE_DIR="/var/cache/mise" +GROUP="mise" + +if [ "$(id -u)" -ne 0 ]; then + echo "(!) This feature must be installed as root." >&2 + exit 1 +fi + +# Every download lands here, so nothing the install fetches is left in the +# image, whichever path the script exits by. +tmpdir="$(mktemp -d)" +trap 'rm -rf "${tmpdir}"' EXIT + +# Debian package providing each command the installation needs. +package_for() { + case "$1" in + wget) echo "wget" ;; + minisign) echo "minisign" ;; + sha256sum) echo "coreutils" ;; + *) echo "$1" ;; + esac +} + +# Installs whatever the base image is missing, and nothing it already has. +install_prerequisites() { + local required=(minisign sha256sum) missing=() cmd + + # Either downloader will do, so one is only pulled in when neither is there. + if ! command -v curl >/dev/null 2>&1; then + required+=(wget) + fi + + for cmd in "${required[@]}"; do + if ! command -v "${cmd}" >/dev/null 2>&1; then + missing+=("$(package_for "${cmd}")") + fi + done + + # HTTPS access to github.com needs a CA bundle. + if [ ! -e /etc/ssl/certs/ca-certificates.crt ]; then + missing+=(ca-certificates) + fi + + if [ "${#missing[@]}" -eq 0 ]; then + return + fi + + if ! command -v apt-get >/dev/null 2>&1; then + echo "(!) Missing prerequisites and no apt-get to install them with: ${missing[*]}" >&2 + exit 1 + fi + + export DEBIAN_FRONTEND=noninteractive + apt-get update -y + apt-get install -y --no-install-recommends "${missing[@]}" + rm -rf /var/lib/apt/lists/* +} + +download() { + local url="$1" destination="$2" + + if command -v wget >/dev/null 2>&1; then + wget -q -T 30 -t 3 -O "${destination}" "${url}" + else + curl -fsSL --connect-timeout 30 --retry 3 -o "${destination}" "${url}" + fi +} + +# Prints the Location header a URL redirects to, without following it. +redirect_target() { + local url="$1" + + if command -v wget >/dev/null 2>&1; then + # wget treats the redirect it was told not to follow as a failure, but + # has printed the response headers by then. + (wget -q -T 30 -t 3 --max-redirect=0 -S -O /dev/null "${url}" 2>&1 || true) \ + | sed -n 's/^ *Location: *//p' | head -n1 + else + curl -fsSI --connect-timeout 30 --retry 3 -o /dev/null -w '%{redirect_url}' "${url}" + fi +} + +# Turns the requested version into the exact "vYYYY.M.N" the release is +# published under. "latest" is resolved through the redirect GitHub serves for +# a repository's latest release, which lands on that release's tag page. +resolve_version() { + local requested="${1#v}" location resolved + + if [[ "${requested}" =~ ^[0-9]{4}\.[0-9]+\.[0-9]+$ ]]; then + echo "v${requested}" + return + fi + + if [ "${requested}" != "latest" ]; then + echo "(!) Unrecognised version '$1'. Use \"latest\" or an exact version such as \"2026.9.10\"." >&2 + exit 1 + fi + + location="$(redirect_target "${RELEASES_URL}/latest")" + resolved="${location##*/tag/}" + + if [ -z "${location}" ] || [[ ! "${resolved}" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+$ ]]; then + echo "(!) Could not resolve the latest mise release from ${RELEASES_URL}/latest (got '${location}')." >&2 + exit 1 + fi + + echo "${resolved}" +} + +mise_arch() { + case "$(uname -m)" in + x86_64) echo "x64" ;; + aarch64 | arm64) echo "arm64" ;; + *) + echo "(!) Unsupported architecture: $(uname -m)." >&2 + exit 1 + ;; + esac +} + +# Downloads the named release binary, checks it against the verified +# SHASUMS256.txt and installs it as ${PREFIX}/bin/mise. +install_binary() { + local binary="$1" + + download "${RELEASES_URL}/download/${mise_version}/${binary}" "${tmpdir}/${binary}" + + # SHASUMS256.txt names each file as ./, so the entry is rewritten to + # point at the copy just downloaded. + grep " \\./${binary}\$" "${tmpdir}/SHASUMS256.txt" \ + | sed "s| \\./${binary}\$| ${tmpdir}/${binary}|" \ + | sha256sum -c - + + install -m 755 "${tmpdir}/${binary}" "${PREFIX}/bin/mise" +} + +# Runs the installed mise once, with every directory it might write to pointed +# into the temporary directory so the check leaves nothing behind. +mise_runs() { + env HOME="${tmpdir}/home" \ + MISE_DATA_DIR="${tmpdir}/home/data" \ + MISE_CACHE_DIR="${tmpdir}/home/cache" \ + MISE_CONFIG_DIR="${tmpdir}/home/config" \ + MISE_STATE_DIR="${tmpdir}/home/state" \ + "${PREFIX}/bin/mise" --version +} + +install_prerequisites + +mise_version="$(resolve_version "${VERSION}")" +arch="$(mise_arch)" + +echo "Installing mise ${mise_version} (linux-${arch}) into ${PREFIX}/bin..." +download "${RELEASES_URL}/download/${mise_version}/SHASUMS256.txt" "${tmpdir}/SHASUMS256.txt" +download "${RELEASES_URL}/download/${mise_version}/SHASUMS256.txt.minisig" "${tmpdir}/SHASUMS256.txt.minisig" + +# The checksums are signed with mise's minisign key. The public key is vendored +# with the feature, so the signature is checked against the key reviewed in +# this repository rather than one fetched at install time. +minisign -V -m "${tmpdir}/SHASUMS256.txt" -p "${PUBLIC_KEY}" + +# The glibc build is what upstream's installer picks on a glibc image, but a +# release can require a newer glibc than the image provides, in which case it +# fails to start at all. The statically linked musl build runs on any image, so +# it is installed in that case, verified against the same signed checksums. +install_binary "mise-${mise_version}-linux-${arch}" +if ! output="$(mise_runs 2>&1)"; then + echo "(*) The glibc build of mise ${mise_version} does not run on this image; installing the musl build instead." >&2 + echo "${output}" | sed 's/^/ /' >&2 + install_binary "mise-${mise_version}-linux-${arch}-musl" + mise_runs >/dev/null +fi + +username="${_REMOTE_USER:-root}" +if ! getent passwd "${username}" >/dev/null; then + echo "(!) Remote user '${username}' was not found in the password database." >&2 + exit 1 +fi + +# The volumes are seeded from these directories on first use, ownership +# included, but the Dev Containers CLI renumbers the remote user to the host +# user's UID without touching anything outside the home directory. A dedicated +# group keeps the directories writable through that: membership is recorded by +# name, and the setgid bit keeps entries created later in the group. +if ! getent group "${GROUP}" >/dev/null; then + groupadd --system "${GROUP}" +fi +usermod -aG "${GROUP}" "${username}" + +mkdir -p "${DATA_DIR}" "${CACHE_DIR}" +chown "${username}:${GROUP}" "${DATA_DIR}" "${CACHE_DIR}" +chmod 2775 "${DATA_DIR}" "${CACHE_DIR}" + +echo "Installed mise $("${PREFIX}/bin/mise" --version)." diff --git a/src/mise/mise-minisign.pub b/src/mise/mise-minisign.pub new file mode 100644 index 0000000..d76941d --- /dev/null +++ b/src/mise/mise-minisign.pub @@ -0,0 +1,2 @@ +untrusted comment: minisign public key 64113EDF160FDEC2 +RWTC3g8W3z4RZK3V3qv7fa1QY4JEWyBtqIHW+85QlJpZc5yG+uNYNBSZ diff --git a/src/mise/smoke_test.sh b/src/mise/smoke_test.sh new file mode 100755 index 0000000..0d16ad8 --- /dev/null +++ b/src/mise/smoke_test.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -euo pipefail + +failures=0 + +# Runs a command and records the outcome instead of aborting, so a single run +# reports every broken check rather than only the first one. +check() { + local label="$1" + shift + + local output + local status=0 + # The `||` keeps the assignment out of `set -e`'s reach, which would + # otherwise abort the whole run on the first failing check. + output="$("$@" 2>&1)" || status=$? + + if [ "${status}" -eq 0 ]; then + echo "ok - ${label}" + else + echo "FAIL - ${label} (exit ${status})" + if [ -n "${output}" ]; then + echo "${output}" | sed 's/^/ /' + fi + failures=$((failures + 1)) + fi +} + +check "mise is on PATH" command -v mise +check "mise reports a version" mise --version +check "mise is installed under /usr/local" \ + bash -c 'test "$(command -v mise)" = /usr/local/bin/mise' +check "the installed binary is owned by root" \ + bash -c 'test "$(stat -c %u /usr/local/bin/mise)" = 0' +check "MISE_DATA_DIR points at the mounted volume" \ + bash -c 'test "${MISE_DATA_DIR:-}" = /var/lib/mise' +check "MISE_CACHE_DIR points at the mounted volume" \ + bash -c 'test "${MISE_CACHE_DIR:-}" = /var/cache/mise' +check "the shims directory is on PATH" \ + bash -c 'tr ":" "\n" <<< "${PATH}" | grep -qx /var/lib/mise/shims' +check "remote user belongs to the mise group" \ + bash -c 'id -nG | tr " " "\n" | grep -qx mise' +check "data and cache directories are group-writable and setgid" \ + bash -c 'test "$(stat -c "%A %G" /var/lib/mise)" = "drwxrwsr-x mise" + test "$(stat -c "%A %G" /var/cache/mise)" = "drwxrwsr-x mise"' +check "data and cache directories are writable by the remote user" \ + bash -c 'touch /var/lib/mise/.write-test /var/cache/mise/.write-test + rm /var/lib/mise/.write-test /var/cache/mise/.write-test' +check "files created there are owned by the remote user" \ + bash -c 'touch /var/lib/mise/.owner-test + owner="$(stat -c %u /var/lib/mise/.owner-test)" + rm /var/lib/mise/.owner-test + test "${owner}" = "$(id -u)"' +check "mise installs and runs a tool" \ + mise exec python@3 -- python -c 'print("Hello, world!")' +check "the tool was installed into the data directory" \ + bash -c 'test -d /var/lib/mise/installs/python' +check "the tool's shim was written to the shims directory" \ + bash -c 'test -x /var/lib/mise/shims/python' + +if [ "${failures}" -ne 0 ]; then + echo "${failures} check(s) failed." + exit 1 +fi + +echo "All checks passed."