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."