diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92145dce77..380a417315 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,69 @@
# Changelog
+## [2026.6.12](https://github.com/jdx/mise/compare/v2026.6.11..v2026.6.12) - 2026-06-21
+
+### 🚀 Features
+
+- **(bootstrap)** add skip controls by @jdx in [#10497](https://github.com/jdx/mise/pull/10497)
+- **(http)** resolve cross-platform lock checksums from published metadata by @itochan in [#10509](https://github.com/jdx/mise/pull/10509)
+- **(upgrade)** fix tool removal when minimum_release_age is set by @roele in [#10466](https://github.com/jdx/mise/pull/10466)
+
+### 🐛 Bug Fixes
+
+- **(aqua)** support checksum verification fields by @risu729 in [#10473](https://github.com/jdx/mise/pull/10473)
+- **(backend)** resolve dependency executables on Windows by @risu729 in [#10514](https://github.com/jdx/mise/pull/10514)
+- **(env)** expand escaped dollar in env shell expansion by @jdx in [#10511](https://github.com/jdx/mise/pull/10511)
+- **(install)** rebuild symlinks after partial installs by @risu729 in [#10470](https://github.com/jdx/mise/pull/10470)
+- **(npm)** warn when system pnpm/bun may not support minimum_release_age by @risu729 in [#10491](https://github.com/jdx/mise/pull/10491)
+- **(pipx)** upgrade shared pip for release age installs by @risu729 in [#10472](https://github.com/jdx/mise/pull/10472)
+- **(pipx)** warn for unsupported uv exclude-newer by @risu729 in [#10510](https://github.com/jdx/mise/pull/10510)
+- **(pipx)** force pip backend for mise pipx subprocess calls by @risu729 in [#10513](https://github.com/jdx/mise/pull/10513)
+- **(system)** drop bare -- from dnf argv (breaks DNF5 install/upgrade) by @spencergilbert in [#10538](https://github.com/jdx/mise/pull/10538)
+- **(task)** align duplicate config task precedence by @risu729 in [#10471](https://github.com/jdx/mise/pull/10471)
+- **(task)** skip mise configs in task include dirs by @jdx in [#10500](https://github.com/jdx/mise/pull/10500)
+- **(task)** error on empty task shell by @jdx in [#10517](https://github.com/jdx/mise/pull/10517)
+- **(task)** honor show_full_cmd in the task command header by @JamBalaya56562 in [#10518](https://github.com/jdx/mise/pull/10518)
+- **(vfox)** resolve tools=true env path templates against the dependency toolset by @JamBalaya56562 in [#10481](https://github.com/jdx/mise/pull/10481)
+
+### 📚 Documentation
+
+- **(dotfiles)** make self-managing config first-run safe by @jdx in [#10494](https://github.com/jdx/mise/pull/10494)
+- **(env)** remove emoji checkboxes by @jdx in [#10504](https://github.com/jdx/mise/pull/10504)
+- recommend keeping mise current by @jdx in [#10505](https://github.com/jdx/mise/pull/10505)
+
+### 📦️ Dependency Updates
+
+- bump usage to 3.5.2 by @jdx in [#10498](https://github.com/jdx/mise/pull/10498)
+- update ghcr.io/jdx/mise:rpm docker digest to b5e0574 by @renovate[bot] in [#10531](https://github.com/jdx/mise/pull/10531)
+- update ghcr.io/jdx/mise:alpine docker digest to 892c324 by @renovate[bot] in [#10529](https://github.com/jdx/mise/pull/10529)
+- update jdx/mise-action digest to e6a8b39 by @renovate[bot] in [#10532](https://github.com/jdx/mise/pull/10532)
+- update rust docker digest to c681116 by @renovate[bot] in [#10533](https://github.com/jdx/mise/pull/10533)
+- update ubuntu:26.04 docker digest to e153663 by @renovate[bot] in [#10534](https://github.com/jdx/mise/pull/10534)
+- update ghcr.io/jdx/mise:deb docker digest to 3d636fa by @renovate[bot] in [#10530](https://github.com/jdx/mise/pull/10530)
+- update dependency esbuild to v0.28.1 by @renovate[bot] in [#10535](https://github.com/jdx/mise/pull/10535)
+- update ubuntu:26.04 docker digest to 53958ec by @renovate[bot] in [#10539](https://github.com/jdx/mise/pull/10539)
+
+### 📦 Registry
+
+- add aspire ([github:microsoft/aspire](https://github.com/microsoft/aspire)) by @davidfowl in [#10520](https://github.com/jdx/mise/pull/10520)
+- add nub by @colinhacks in [#10544](https://github.com/jdx/mise/pull/10544)
+- add ldc ([github:ldc-developers/ldc](https://github.com/ldc-developers/ldc)) by @slbls in [#10527](https://github.com/jdx/mise/pull/10527)
+- relax checkov test by @jdx in [#10548](https://github.com/jdx/mise/pull/10548)
+
+### New Contributors
+
+- @spencergilbert made their first contribution in [#10538](https://github.com/jdx/mise/pull/10538)
+- @slbls made their first contribution in [#10527](https://github.com/jdx/mise/pull/10527)
+- @colinhacks made their first contribution in [#10544](https://github.com/jdx/mise/pull/10544)
+- @davidfowl made their first contribution in [#10520](https://github.com/jdx/mise/pull/10520)
+
+### 📦 Aqua Registry Updates
+
+#### New Packages (2)
+
+- [`EpicGames/lore`](https://github.com/EpicGames/lore)
+- [`cdxgen/cdxgen`](https://github.com/cdxgen/cdxgen)
+
## [2026.6.11](https://github.com/jdx/mise/compare/v2026.6.10..v2026.6.11) - 2026-06-16
### 🚀 Features
diff --git a/Cargo.lock b/Cargo.lock
index ce50f0dd20..e5527b1cf5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5271,7 +5271,7 @@ dependencies = [
[[package]]
name = "mise"
-version = "2026.6.11"
+version = "2026.6.12"
dependencies = [
"age",
"aho-corasick",
diff --git a/Cargo.toml b/Cargo.toml
index 79f9734fed..86990ad922 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,7 +9,7 @@ members = [
[package]
name = "mise"
-version = "2026.6.11"
+version = "2026.6.12"
edition = "2024"
description = "Dev tools, env vars, and tasks in one CLI"
authors = ["Jeff Dickey (@jdx)"]
diff --git a/README.md b/README.md
index e4b58cafdc..f3ac01ee9c 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ $ ~/.local/bin/mise --version
/ / / / / / (__ ) __/_____/ __/ / / /_____/ /_/ / / /_/ / /__/ __/
/_/ /_/ /_/_/____/\___/ \___/_/ /_/ / .___/_/\__,_/\___/\___/
/_/ by @jdx
-2026.6.11 macos-arm64 (2026-06-16)
+2026.6.12 macos-arm64 (2026-06-21)
```
Hook mise into your shell (pick the right one for your shell):
diff --git a/completions/_mise b/completions/_mise
index 22a2457312..6285218d28 100644
--- a/completions/_mise
+++ b/completions/_mise
@@ -24,7 +24,7 @@ _mise() {
return 1
fi
- local spec_file="${TMPDIR:-/tmp}/usage__usage_spec_mise_2026_6_11.spec"
+ local spec_file="${TMPDIR:-/tmp}/usage__usage_spec_mise_2026_6_12.spec"
if [[ ! -f "$spec_file" ]]; then
mise usage >| "$spec_file"
fi
diff --git a/completions/mise.bash b/completions/mise.bash
index 5d661cd56e..5a3fd43ebc 100644
--- a/completions/mise.bash
+++ b/completions/mise.bash
@@ -9,7 +9,7 @@ _mise() {
local cur prev words cword was_split comp_args
_comp_initialize -n : -- "$@" || return
- local spec_file="${TMPDIR:-/tmp}/usage__usage_spec_mise_2026_6_11.spec"
+ local spec_file="${TMPDIR:-/tmp}/usage__usage_spec_mise_2026_6_12.spec"
if [[ ! -f "$spec_file" ]]; then
mise usage >| "$spec_file"
fi
diff --git a/completions/mise.fish b/completions/mise.fish
index c877f1a592..5cfd7c055f 100644
--- a/completions/mise.fish
+++ b/completions/mise.fish
@@ -8,7 +8,7 @@ if ! type -p usage &> /dev/null
return 1
end
set -l tmpdir (if set -q TMPDIR; echo $TMPDIR; else; echo /tmp; end)
-set -l spec_file "$tmpdir/usage__usage_spec_mise_2026_6_11.spec"
+set -l spec_file "$tmpdir/usage__usage_spec_mise_2026_6_12.spec"
if not test -f "$spec_file"
mise usage | string collect > "$spec_file"
end
diff --git a/completions/mise.ps1 b/completions/mise.ps1
index ad5cdfeab3..1d138b2c59 100644
--- a/completions/mise.ps1
+++ b/completions/mise.ps1
@@ -10,7 +10,7 @@ Register-ArgumentCompleter -Native -CommandName 'mise' -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$tmpDir = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
- $specFile = Join-Path $tmpDir "usage__usage_spec_mise_2026_6_11.kdl"
+ $specFile = Join-Path $tmpDir "usage__usage_spec_mise_2026_6_12.kdl"
if (-not (Test-Path $specFile)) {
mise usage | Out-File -FilePath $specFile -Encoding utf8
diff --git a/default.nix b/default.nix
index 9c651ec47d..9527b49444 100644
--- a/default.nix
+++ b/default.nix
@@ -2,7 +2,7 @@
rustPlatform.buildRustPackage {
pname = "mise";
- version = "2026.6.11";
+ version = "2026.6.12";
src = lib.cleanSource ./.;
diff --git a/docs/.vitepress/stars.data.ts b/docs/.vitepress/stars.data.ts
index 7ae72a424a..dc681c0fd7 100644
--- a/docs/.vitepress/stars.data.ts
+++ b/docs/.vitepress/stars.data.ts
@@ -3,7 +3,7 @@
export default {
load() {
return {
- stars: "29.5k",
+ stars: "29.8k",
};
},
};
diff --git a/docs/bootstrap/systemd.md b/docs/bootstrap/systemd.md
index c29f188679..5c8f4ca676 100644
--- a/docs/bootstrap/systemd.md
+++ b/docs/bootstrap/systemd.md
@@ -57,8 +57,8 @@ and enable without keeping the unit running.
- **User services only** — mise writes to `~/.config/systemd/user` and uses
`systemctl --user`. System services in `/etc/systemd/system` are not
supported.
-- **Target user only** — run mise as the user that owns the services, with an
- active systemd user bus. `sudo mise` is skipped because `systemctl --user`
+- **Target user only** — run mise as the user who owns the services, with a
+ reachable systemd user manager. `sudo mise` is skipped because `systemctl --user`
would target the wrong user manager.
- **Manual application only** — mise never writes or starts systemd services
implicitly; only `mise bootstrap systemd apply` and `mise bootstrap` do.
diff --git a/docs/dev-tools/backends/http.md b/docs/dev-tools/backends/http.md
index 097afaea26..5ea8114437 100644
--- a/docs/dev-tools/backends/http.md
+++ b/docs/dev-tools/backends/http.md
@@ -127,6 +127,72 @@ linux-x64 = {
}
```
+### `checksum_url`
+
+URL of a published checksum source. When set, [`mise lock`](/dev-tools/mise-lock)
+resolves checksums for every target platform — including platforms other than
+the one you are running on — **without downloading the artifacts**. This lets a
+single machine produce a complete, cross-platform lockfile.
+
+`checksum_url` is a template (supports {{ version }}, {{ os() }}, {{ arch() }}
+and is platform-specific via `platforms..checksum_url`). It may point at any
+of:
+
+- an **individual checksum file** (e.g. `.sha256`), which may contain
+ just the hash or ``;
+- a **SHASUMS**-style file listing `` for many platforms (the row
+ is matched by the artifact's filename);
+- a **manifest** (e.g. JSON), combined with `checksum_expr` below.
+
+For individual and SHASUMS checksum files, the algorithm is detected from the
+file's name (`*.sha512`, `SHA512SUMS`, `*.md5`, `*.b3`, defaulting to sha256).
+
+```toml
+# Individual checksum file (one per artifact)
+[tools."http:my-tool"]
+version = "1.0.0"
+url = "https://example.com/releases/my-tool-{{ version }}-{{ os() }}-{{ arch() }}.tar.gz"
+checksum_url = "https://example.com/releases/my-tool-{{ version }}-{{ os() }}-{{ arch() }}.tar.gz.sha256"
+
+# SHASUMS (one file lists every platform)
+[tools."http:other-tool"]
+version = "1.0.0"
+url = 'https://example.com/{{ version }}/other_{{ version }}_{{ os(macos="darwin") }}_{{ arch(x64="amd64") }}.zip'
+checksum_url = 'https://example.com/{{ version }}/other_{{ version }}_SHASUMS'
+```
+
+### `checksum_expr`
+
+When the checksum lives in a manifest (rather than a plain checksum file), use
+`checksum_expr` to extract it. The manifest body fetched from `checksum_url` is
+evaluated with [expr-lang](https://expr-lang.org). The following variables are
+available: `body` (the raw manifest), `version`, `os`, `arch`, `url` (the
+resolved artifact URL for the target), and `filename`.
+
+The expression must evaluate to a qualified `algo:hash` **string** (e.g.
+`sha256:`, `sha512:`). Build the prefix in the expression: prepend a
+literal when the algorithm is fixed (`"sha256:" + entry.hash`), or read it from
+the manifest when it varies (`entry.algo + ":" + entry.hash`).
+
+```toml
+[tools."http:my-tool"]
+version = "1.10.0"
+checksum_url = "https://example.com/versions.json"
+# Match the file whose url equals the resolved artifact url, return sha256:
+checksum_expr = '"sha256:" + filter(fromJSON(body)[version + ""].files, { #.url == url })[0].sha256'
+
+[tools."http:my-tool".platforms]
+linux-x64 = { url = "https://example.com/my-tool-{{ version }}-linux-x86_64.tar.gz" }
+macos-arm64 = { url = "https://example.com/my-tool-{{ version }}-macos-arm64.tar.gz" }
+```
+
+::: tip expr-lang gotchas
+The predicate placeholder must be written as `{ #... }` **with a space** after
+`{`, because `{#` is the Tera comment delimiter. To index a map by a runtime
+value, force evaluation with `[version + ""]` — a bare `[version]` is treated as
+the literal key `"version"`.
+:::
+
### `size`
Verify the downloaded file size:
diff --git a/e2e/backend/test_http_lock_checksum b/e2e/backend/test_http_lock_checksum
new file mode 100644
index 0000000000..5bb29614ec
--- /dev/null
+++ b/e2e/backend/test_http_lock_checksum
@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+
+# Cross-platform `mise lock` for the http backend resolves published checksums
+# for platforms other than the host, without downloading artifacts. Covers the
+# SHASUMS form (matched by filename) and the individual checksum file form, with
+# os()/arch() in the url rendered per target platform.
+
+SRV="$PWD/srv"
+mkdir -p "$SRV"
+
+# sha256 of each artifact's bytes (the string written below).
+LINUX_SHA="ae26b73f6db848ed1ccb7c35ec59234cd74ecf9d344f422732058df5be2e6a01" # "tool linux payload"
+MACOS_SHA="40a3a5cdad86d4833b06b463da7f294fae29b61298aeb729e4e9dbf5e45ba4af" # "tool macos payload"
+FILE_LINUX_SHA="508f35a45c1c4869a5949ec3d96779237619c36b7a6b51f6a83400de9fc5b01e" # "individual linux payload"
+FILE_MACOS_SHA="f4fbe0d002f0331efc717160d7ea559c0f1172685d454aac59c12c1379d2cc49" # "individual macos payload"
+# sha512 (128 hex chars) of "tool512 linux payload" — algorithm detected from the file name
+SHA512_LINUX="78b83c1f3aa14cfa6c5a551a92d90c147b3c029304429d96bc86560e0f08fc9cf69c343c2dc52fec0d7c7bceee894bb5647d4f3e3359816b231dafaee8799363"
+
+printf '%s' "tool linux payload" >"$SRV/tool_1.0.0_linux.tar.gz"
+printf '%s' "tool macos payload" >"$SRV/tool_1.0.0_macos.tar.gz"
+printf '%s' "individual linux payload" >"$SRV/file_1.0.0_linux.tar.gz"
+printf '%s' "individual macos payload" >"$SRV/file_1.0.0_macos.tar.gz"
+printf '%s' "tool512 linux payload" >"$SRV/tool512_1.0.0_linux.tar.gz"
+
+# SHASUMS file listing both platform artifacts (HashiCorp/OpenShift style)
+cat >"$SRV/tool_1.0.0_SHASUMS" <"$SRV/tool512_1.0.0_SHA512SUMS" <"$SRV/file_1.0.0_linux.tar.gz.sha256"
+echo "$FILE_MACOS_SHA" >"$SRV/file_1.0.0_macos.tar.gz.sha256"
+
+# A SHASUMS list whose entries don't match this tool's artifact name (a naming
+# mismatch). Locking must NOT fall back to the first hash in the list and
+# silently write another artifact's checksum.
+BOGUS_SHA="1111111111111111111111111111111111111111111111111111111111111111"
+cat >"$SRV/mismatch_1.0.0_SHASUMS" </dev/null || true; }
+trap cleanup EXIT
+
+wait_for_file "$PORT_FILE" "lock http port file" 30 "$SERVER_PID"
+PORT=$(cat "$PORT_FILE")
+
+cat >mise.toml <"$SRV/mytool"
+printf '%s mytool\n' "$REAL_SHA" >"$SRV/mytool_SHASUMS"
+
+# Serve the directory on an ephemeral port
+PORT_FILE="$TMPDIR/mise_lock_verify_port"
+python3 - "$SRV" "$PORT_FILE" <<'PY' &
+import http.server, socketserver, sys, os
+srv, port_file = sys.argv[1], sys.argv[2]
+os.chdir(srv)
+socketserver.TCPServer.allow_reuse_address = True
+with socketserver.TCPServer(("127.0.0.1", 0), http.server.SimpleHTTPRequestHandler) as httpd:
+ with open(port_file, "w") as f:
+ f.write(str(httpd.server_address[1]))
+ httpd.serve_forever()
+PY
+SERVER_PID=$!
+cleanup() { kill "$SERVER_PID" 2>/dev/null || true; }
+trap cleanup EXIT
+
+wait_for_file "$PORT_FILE" "lock verify port file" 30 "$SERVER_PID"
+PORT=$(cat "$PORT_FILE")
+
+cat >mise.toml <mise.lock.tmp && mv mise.lock.tmp mise.lock
+assert_fail "mise install --locked -f"
diff --git a/e2e/backend/test_http_lock_skips_unresolved b/e2e/backend/test_http_lock_skips_unresolved
new file mode 100644
index 0000000000..0ff46c0aeb
--- /dev/null
+++ b/e2e/backend/test_http_lock_skips_unresolved
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+# `mise lock` fails closed for an http platform it can't resolve: a platform with
+# no URL is reported as skipped (and not written), rather than being miscounted
+# as a successful entry while writing nothing. Mirrors test_github_matching_lock
+# for the http backend.
+
+export MISE_LOCKFILE=1
+
+cat <mise.toml
+[tools."http:lockskip-test"]
+version = "1.0.0"
+
+[tools."http:lockskip-test".platforms.linux-x64]
+url = "https://example.com/lockskip-{{ version }}-linux.tar.gz"
+EOF
+
+touch mise.lock
+
+# linux-x64 has a URL (a url-only entry is written); macos-arm64 has none.
+output="$(mise lock --platform linux-x64,macos-arm64)"
+
+# The unresolvable platform is reported as skipped, not as a success.
+assert_contains "echo '$output'" "1 skipped"
+
+# The resolvable platform is written; the skipped one is not.
+assert_contains "cat mise.lock" "platforms.linux-x64"
+assert_not_contains "cat mise.lock" "macos-arm64"
diff --git a/e2e/cli/test_bootstrap b/e2e/cli/test_bootstrap
index a5b3109be3..b9bba6d994 100644
--- a/e2e/cli/test_bootstrap
+++ b/e2e/cli/test_bootstrap
@@ -138,8 +138,9 @@ description = "sync files"
exec_start = "~/.local/bin/my-sync --watch"
restart = "on-failure"
EOF
-DBUS_SESSION_BUS_ADDRESS=unix:path=/tmp/mise-test-bus assert_succeed "mise bootstrap --dry-run"
-DBUS_SESSION_BUS_ADDRESS=unix:path=/tmp/mise-test-bus assert_contains "mise bootstrap systemd status" "my-sync"
+mkdir -p runtime/systemd/private
+XDG_RUNTIME_DIR="$PWD/runtime" assert_succeed "mise bootstrap --dry-run"
+XDG_RUNTIME_DIR="$PWD/runtime" assert_contains "mise bootstrap systemd status" "my-sync"
# unavailable system package managers are skipped, not errors
cat <mise.toml
diff --git a/e2e/tasks/test_task_show_full_cmd b/e2e/tasks/test_task_show_full_cmd
new file mode 100644
index 0000000000..7f5adc7aaa
--- /dev/null
+++ b/e2e/tasks/test_task_show_full_cmd
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+# #10469: the `task.show_full_cmd` setting (env MISE_TASK_SHOW_CMD_NO_TRUNC) must
+# echo the FULL multi-line command in the task header, not just the first real
+# command. PR #9844 added display_first_command(), which reduced the echoed header
+# to the first real command (skipping leading shebang/blank/`set ...` lines)
+# UNCONDITIONALLY — making show_full_cmd a no-op. The reduction is now gated on the
+# setting: the whole script when on, the first command when off.
+
+cat <<'EOF' >mise.toml
+[tasks.multi]
+run = """
+#!/usr/bin/env bash
+set -e
+echo first-line
+echo second-line
+"""
+EOF
+
+# show_full_cmd ON: the echoed header includes the LATER command line. The literal
+# "echo second-line" only appears in the echoed command (execution prints
+# "second-line", not "echo second-line").
+assert_contains "MISE_TASK_SHOW_CMD_NO_TRUNC=1 mise run multi 2>&1" "echo second-line"
+
+# show_full_cmd OFF (#9844 behavior preserved): the header shows the first REAL
+# command, skipping the shebang/`set` boilerplate, and later lines are truncated.
+assert_contains "mise run multi 2>&1" "echo first-line"
+assert_not_contains "mise run multi 2>&1" "echo second-line"
diff --git a/packaging/rpm/mise.spec b/packaging/rpm/mise.spec
index ed02ee4fd4..05cf142e82 100644
--- a/packaging/rpm/mise.spec
+++ b/packaging/rpm/mise.spec
@@ -1,6 +1,6 @@
Summary: Dev tools, env vars, and tasks in one CLI
Name: mise
-Version: 2026.6.11
+Version: 2026.6.12
Release: 1
URL: https://github.com/jdx/mise/
Group: System
diff --git a/registry/checkov.toml b/registry/checkov.toml
index c65f545f3a..f2039e28c1 100644
--- a/registry/checkov.toml
+++ b/registry/checkov.toml
@@ -1,3 +1,3 @@
backends = ["aqua:bridgecrewio/checkov", "asdf:bosmak/asdf-checkov"]
description = "Prevent cloud misconfigurations and find vulnerabilities during build-time in infrastructure as code, container images and open source packages with Checkov by Bridgecrew"
-test = { cmd = "checkov -v", expected = "{{version}}" }
+test = { cmd = "checkov --help", expected = "usage: checkov" }
diff --git a/registry/ldc.toml b/registry/ldc.toml
new file mode 100644
index 0000000000..bb59f6b21d
--- /dev/null
+++ b/registry/ldc.toml
@@ -0,0 +1,3 @@
+backends = ["github:ldc-developers/ldc"]
+description = "LLVM-based compiler for the D programming language"
+test = { cmd = "ldc2 --version", expected = "{{version}}" }
diff --git a/snapcraft.yaml b/snapcraft.yaml
index 1d56abb5f8..0389c73935 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -9,7 +9,7 @@
name: mise
title: mise-en-place
-version: "2026.6.11"
+version: "2026.6.12"
summary: Dev tools, env vars, and tasks in one CLI
description: |
mise-en-place prepares your development environment before each command runs.
diff --git a/src/backend/asset_matcher.rs b/src/backend/asset_matcher.rs
index e1f21b9fc3..9ba217de59 100644
--- a/src/backend/asset_matcher.rs
+++ b/src/backend/asset_matcher.rs
@@ -1069,7 +1069,7 @@ impl<'a> ChecksumFetcher<'a> {
}
/// Detect the checksum algorithm from the filename
-fn detect_checksum_algorithm(filename: &str) -> String {
+pub(crate) fn detect_checksum_algorithm(filename: &str) -> String {
let lower = filename.to_lowercase();
if lower.contains("sha512") || lower.ends_with(".sha512") || lower.ends_with(".sha512sum") {
"sha512".to_string()
diff --git a/src/backend/cargo.rs b/src/backend/cargo.rs
index c177d3119f..a70a7303e3 100644
--- a/src/backend/cargo.rs
+++ b/src/backend/cargo.rs
@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
+use std::path::PathBuf;
use std::{fmt::Debug, sync::Arc};
use async_trait::async_trait;
@@ -16,10 +17,9 @@ use crate::cli::args::BackendArg;
use crate::cmd::CmdLineRunner;
use crate::config::{Config, Settings};
use crate::env::GITHUB_TOKEN;
-use crate::file;
use crate::http::HTTP_FETCH;
use crate::install_context::InstallContext;
-use crate::toolset::{ToolRequest, ToolVersion, ToolVersionOptions};
+use crate::toolset::{ToolRequest, ToolVersion, ToolVersionOptions, Toolset};
#[derive(Debug)]
pub struct CargoBackend {
@@ -84,7 +84,7 @@ impl<'a> CargoOptions<'a> {
#[derive(Debug)]
enum BinstallStatus {
- Enabled,
+ Enabled(PathBuf),
Disabled,
Unavailable,
UnsupportedOptions(Vec<&'static str>),
@@ -183,9 +183,9 @@ impl Backend for CargoBackend {
}
cmd
} else {
- match self.binstall_status(&config, &tv).await {
- BinstallStatus::Enabled => {
- let mut cmd = CmdLineRunner::new("cargo-binstall").arg("-y");
+ match self.binstall_status(&config, Some(&ctx.ts), &tv).await {
+ BinstallStatus::Enabled(cargo_binstall) => {
+ let mut cmd = CmdLineRunner::new(cargo_binstall).arg("-y");
if let Some(token) = &*GITHUB_TOKEN {
cmd = cmd.env("GITHUB_TOKEN", token)
}
@@ -298,7 +298,12 @@ impl CargoBackend {
options
}
- async fn binstall_status(&self, config: &Arc, tv: &ToolVersion) -> BinstallStatus {
+ async fn binstall_status(
+ &self,
+ config: &Arc,
+ ts: Option<&Toolset>,
+ tv: &ToolVersion,
+ ) -> BinstallStatus {
if !Settings::get().cargo.binstall {
return BinstallStatus::Disabled;
}
@@ -307,19 +312,13 @@ impl CargoBackend {
if !cargo_install_required_options.is_empty() {
return BinstallStatus::UnsupportedOptions(cargo_install_required_options);
}
- if file::which_non_pristine("cargo-binstall").is_none() {
- match self.dependency_toolset(config).await {
- Ok(ts) => {
- if ts.which(config, "cargo-binstall").await.is_none() {
- return BinstallStatus::Unavailable;
- }
- }
- Err(_e) => {
- return BinstallStatus::Unavailable;
- }
- }
+ if let Some(cargo_binstall) = self
+ .dependency_path_for_install(config, ts, "cargo-binstall")
+ .await
+ {
+ return BinstallStatus::Enabled(cargo_binstall);
}
- BinstallStatus::Enabled
+ BinstallStatus::Unavailable
}
/// if the name is a git repo, return the git url
diff --git a/src/backend/http.rs b/src/backend/http.rs
index 6332011478..146303b052 100644
--- a/src/backend/http.rs
+++ b/src/backend/http.rs
@@ -2,10 +2,12 @@ use crate::backend::Backend;
use crate::backend::VersionInfo;
use crate::backend::backend_type::BackendType;
use crate::backend::options::BackendOptions;
+use crate::backend::platform_target::PlatformTarget;
use crate::backend::runtime_path_for_install_path;
use crate::backend::static_helpers::{
- clean_binary_name, get_filename_from_url, rename_executable_in_dir, template_string,
- verify_artifact,
+ clean_binary_name, eval_checksum_expr, fetch_checksum_from_file, fetch_checksum_from_shasums,
+ get_filename_from_url, rename_executable_in_dir, shasums_has_entries, template_string,
+ template_string_for_target, verify_artifact,
};
use crate::backend::version_list;
use crate::cli::args::BackendArg;
@@ -13,6 +15,7 @@ use crate::config::Config;
use crate::config::Settings;
use crate::http::HTTP;
use crate::install_context::InstallContext;
+use crate::lockfile::PlatformInfo;
use crate::runtime_symlinks::is_runtime_symlink;
use crate::toolset::ToolRequest;
use crate::toolset::ToolVersion;
@@ -22,6 +25,7 @@ use crate::{dirs, file, hash};
use async_trait::async_trait;
use eyre::Result;
use serde::{Deserialize, Serialize};
+use std::collections::BTreeMap;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -173,6 +177,38 @@ impl<'a> HttpOptions<'a> {
self.values.platform_string("bin_path")
}
+ fn checksum_expr(&self) -> Option<&'a str> {
+ self.values.str("checksum_expr")
+ }
+
+ // Target-aware accessors for cross-platform `mise lock`. These resolve
+ // `platforms..` for an arbitrary target rather than the host.
+ fn url_for_target(&self, target: &PlatformTarget) -> Option {
+ self.values.platform_string_for_target("url", target)
+ }
+
+ fn checksum_for_target(&self, target: &PlatformTarget) -> Option {
+ self.values.platform_string_for_target("checksum", target)
+ }
+
+ fn checksum_url_for_target(&self, target: &PlatformTarget) -> Option {
+ self.values
+ .platform_string_for_target("checksum_url", target)
+ }
+
+ fn format_for_target(&self, target: &PlatformTarget) -> Option {
+ self.values.platform_string_for_target("format", target)
+ }
+
+ fn strip_components_for_target(&self, target: &PlatformTarget) -> Option {
+ self.values
+ .platform_string_for_target("strip_components", target)
+ }
+
+ fn rename_exe_for_target(&self, target: &PlatformTarget) -> Option {
+ self.values.platform_string_for_target("rename_exe", target)
+ }
+
fn version_list_url(&self) -> Option<&'a str> {
self.values.str("version_list_url")
}
@@ -723,6 +759,88 @@ impl HttpBackend {
version_list::fetch_versions(&url, regex, json_path, version_expr).await
}
+
+ // -------------------------------------------------------------------------
+ // Cross-platform lock resolution
+ // -------------------------------------------------------------------------
+
+ /// Resolve the artifact URL for a target platform during `mise lock`.
+ /// Renders `os()`/`arch()` for the target rather than the host.
+ fn lock_url_for_target(
+ &self,
+ opts: &HttpOptions<'_>,
+ tv: &ToolVersion,
+ target: &PlatformTarget,
+ ) -> Option {
+ opts.url_for_target(target)
+ .map(|template| template_string_for_target(&template, tv, target))
+ }
+
+ /// Resolve a published checksum for a target platform without downloading
+ /// the artifact. Tries, in order: a checksum configured directly for the
+ /// platform, a manifest evaluated via `checksum_expr`, a SHASUMS file keyed
+ /// by filename, then an individual checksum file. Returns `None`
+ /// (best-effort) when no published checksum is available.
+ async fn resolve_lock_checksum(
+ &self,
+ opts: &HttpOptions<'_>,
+ tv: &ToolVersion,
+ target: &PlatformTarget,
+ url: &str,
+ ) -> Option {
+ // 1. Checksum declared directly for this platform.
+ if let Some(checksum) = opts.checksum_for_target(target) {
+ return Some(checksum);
+ }
+
+ // 2. Fetch from a declared checksum source.
+ let checksum_url_template = opts.checksum_url_for_target(target)?;
+ let checksum_url = template_string_for_target(&checksum_url_template, tv, target);
+ let filename = get_filename_from_url(url);
+
+ // 2a. Manifest with an extraction expression. The expression returns an
+ // `algo:hash` string. The manifest is the same across platforms, so use
+ // the cached fetch.
+ if let Some(expr) = opts.checksum_expr() {
+ let body = match HTTP.get_text_cached(&checksum_url).await {
+ Ok(body) => body,
+ Err(e) => {
+ debug!("failed to fetch checksum manifest {checksum_url}: {e}");
+ return None;
+ }
+ };
+ let vars = [
+ ("version", tv.version.as_str()),
+ ("os", target.os_name()),
+ ("arch", target.arch_name()),
+ ("url", url),
+ ("filename", filename.as_str()),
+ ];
+ return eval_checksum_expr(expr, &body, &vars);
+ }
+
+ // 2b. Checksum file: a SHASUMS list (filename match) first, then an
+ // individual checksum file. The algorithm is detected from its name.
+ if let Some(checksum) = fetch_checksum_from_shasums(&checksum_url, &filename).await {
+ return Some(checksum);
+ }
+ // A SHASUMS list that has entries but none matching our artifact is a
+ // naming mismatch, not an individual checksum file. Falling back to the
+ // individual-file scan would return the first hash in the list — another
+ // platform's checksum — and silently lock it. Bail so the platform is
+ // reported unresolved instead.
+ if shasums_has_entries(&checksum_url).await {
+ debug!(
+ "checksum_url {checksum_url} is a SHASUMS list with no entry for {filename}; \
+ not falling back to a first-hash scan"
+ );
+ return None;
+ }
+ let file_algo = crate::backend::asset_matcher::detect_checksum_algorithm(
+ &get_filename_from_url(&checksum_url),
+ );
+ fetch_checksum_from_file(&checksum_url, &file_algo).await
+ }
}
/// Returns install-time-only option keys for HTTP backend.
@@ -736,6 +854,8 @@ pub fn install_time_option_keys() -> Vec {
"version_expr".into(),
"format".into(),
"rename_exe".into(),
+ "checksum_url".into(),
+ "checksum_expr".into(),
]
}
@@ -768,6 +888,73 @@ impl Backend for HttpBackend {
super::http_install_operation_count(opts.checksum().is_some(), &self.get_platform_key(), tv)
}
+ /// Options that affect which artifact is downloaded, resolved for the target
+ /// platform so cross-platform lockfile entries match install-time lookups.
+ fn resolve_lockfile_options(
+ &self,
+ request: &ToolRequest,
+ target: &PlatformTarget,
+ ) -> Result> {
+ let raw_opts = request.options();
+ let opts = HttpOptions::new(&raw_opts);
+ let mut result = BTreeMap::new();
+ if let Some(format) = opts.format_for_target(target) {
+ result.insert("format".to_string(), format);
+ }
+ if let Some(strip) = opts.strip_components_for_target(target) {
+ result.insert("strip_components".to_string(), strip);
+ }
+ if let Some(rename) = opts.rename_exe_for_target(target) {
+ result.insert("rename_exe".to_string(), rename);
+ }
+ Ok(result)
+ }
+
+ /// Resolve URL + published checksum for a target platform during `mise lock`,
+ /// without downloading the artifact. Best-effort: a platform with no
+ /// resolvable URL fails closed (`Err`) so the lock run reports it as skipped
+ /// rather than writing nothing under a success count; a missing checksum
+ /// yields a url-only entry.
+ async fn resolve_lock_info(
+ &self,
+ tv: &ToolVersion,
+ target: &PlatformTarget,
+ ) -> Result {
+ let raw_opts = tv.request.options();
+ let opts = HttpOptions::new(&raw_opts);
+
+ // Fail closed when the platform can't be resolved so the lock
+ // orchestration reports it as skipped, rather than returning an empty
+ // entry that is miscounted as a successful platform (see #7113).
+ let Some(url) = self.lock_url_for_target(&opts, tv, target) else {
+ return Err(eyre::eyre!(
+ "no URL configured for {} on {}; skipping",
+ self.ba.full(),
+ target.to_key()
+ ));
+ };
+
+ let checksum = self.resolve_lock_checksum(&opts, tv, target, &url).await;
+
+ // A checksum source was configured but produced nothing for this target
+ // (manifest miss, SHASUMS naming mismatch, unreachable file, ...). The
+ // url-only entry is still written, but surface it so it isn't a silent
+ // drop of checksum verification.
+ if checksum.is_none() && opts.checksum_url_for_target(target).is_some() {
+ warn!(
+ "could not resolve a checksum for {} on {}; locking the URL without checksum verification",
+ self.ba.full(),
+ target.to_key()
+ );
+ }
+
+ Ok(PlatformInfo {
+ url: Some(url),
+ checksum,
+ ..Default::default()
+ })
+ }
+
async fn _list_remote_versions(&self, config: &Arc) -> Result> {
let versions = self.fetch_versions(config).await?;
Ok(versions
@@ -975,6 +1162,23 @@ mod tests {
crate::hash::hash_sha256_to_str(version)[..7].to_string()
}
+ #[test]
+ fn template_string_for_target_renders_target_os_arch() {
+ let tv = http_test_tv("0.40.0");
+ let template =
+ r#"sentinel_{{ version }}_{{ os(macos="darwin") }}_{{ arch(x64="amd64") }}.zip"#;
+ let linux = PlatformTarget::new(crate::platform::Platform::parse("linux-x64").unwrap());
+ assert_eq!(
+ template_string_for_target(template, &tv, &linux),
+ "sentinel_0.40.0_linux_amd64.zip"
+ );
+ let win = PlatformTarget::new(crate::platform::Platform::parse("windows-x64").unwrap());
+ assert_eq!(
+ template_string_for_target(template, &tv, &win),
+ "sentinel_0.40.0_windows_amd64.zip"
+ );
+ }
+
#[test]
fn install_symlink_path_uses_sanitized_version_pathname() {
let version = "/outside-root/mise-http-version-out/selected-prefix";
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index 22c7711f76..373576388f 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -520,6 +520,48 @@ pub(crate) fn normalize_idiomatic_contents(contents: &str) -> String {
.join("\n")
}
+fn executable_names(bin: &str) -> Vec {
+ let mut names = vec![bin.to_string()];
+ if cfg!(target_os = "windows") && Path::new(bin).extension().is_none() {
+ for ext in &Settings::get().windows_executable_extensions {
+ let name = if ext.is_empty() {
+ bin.to_string()
+ } else {
+ format!("{bin}.{ext}")
+ };
+ if !names.contains(&name) {
+ names.push(name);
+ }
+ }
+ }
+ names
+}
+
+fn which_non_pristine_executable(bin: &str) -> Option {
+ executable_names(bin)
+ .into_iter()
+ .find_map(file::which_non_pristine)
+}
+
+pub(crate) async fn configured_toolset_or_path_which(
+ config: &Arc,
+ tools: impl IntoIterator,
+ bin: &str,
+) -> Result