Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

## [2026.6.15](https://github.com/jdx/mise/compare/v2026.6.14..v2026.6.15) - 2026-06-26

### 🚀 Features

- **(bootstrap)** prune unmanaged brew formulae by @jdx in [#10618](https://github.com/jdx/mise/pull/10618)

### 🐛 Bug Fixes

- **(brew-cask)** handle raw binaries, $APPDIR paths, and app bundle copying by @arthurh4 in [#10626](https://github.com/jdx/mise/pull/10626)
- **(hooks)** set MISE_INSTALLED_TOOLS to [] on no-op install (keep running postinstall) by @JamBalaya56562 in [#10615](https://github.com/jdx/mise/pull/10615)
- **(install)** respect lockfile backend during locked installs by @risu729 in [#10599](https://github.com/jdx/mise/pull/10599)
- **(install)** suggest source install for unsupported arches by @risu729 in [#10627](https://github.com/jdx/mise/pull/10627)
- **(oci)** resolve host install symlinks and symlinked paths during `PATH` rebasing by @salim-b in [#10624](https://github.com/jdx/mise/pull/10624)
- stop forcing no-yjit ruby on older glibc by @jdx in [#10620](https://github.com/jdx/mise/pull/10620)

### New Contributors

- @arthurh4 made their first contribution in [#10626](https://github.com/jdx/mise/pull/10626)

### 📦 Aqua Registry Updates

#### New Packages (4)

- [`callumalpass/tasknotes-tui`](https://github.com/callumalpass/tasknotes-tui)
- `glossia.ai/cli`
- [`ogulcancelik/herdr`](https://github.com/ogulcancelik/herdr)
- [`pvolok/dekit`](https://github.com/pvolok/dekit)

## [2026.6.14](https://github.com/jdx/mise/compare/v2026.6.13..v2026.6.14) - 2026-06-25

### 🚀 Features
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ members = [

[package]
name = "mise"
version = "2026.6.14"
version = "2026.6.15"
edition = "2024"
description = "Dev tools, env vars, and tasks in one CLI"
authors = ["Jeff Dickey (@jdx)"]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ $ ~/.local/bin/mise --version
/ / / / / / (__ ) __/_____/ __/ / / /_____/ /_/ / / /_/ / /__/ __/
/_/ /_/ /_/_/____/\___/ \___/_/ /_/ / .___/_/\__,_/\___/\___/
/_/ by @jdx
2026.6.14 macos-arm64 (2026-06-25)
2026.6.15 macos-arm64 (2026-06-26)
```

Hook mise into your shell (pick the right one for your shell):
Expand Down
2 changes: 1 addition & 1 deletion completions/_mise
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ _mise() {
return 1
fi

local spec_file="${TMPDIR:-/tmp}/usage__usage_spec_mise_2026_6_14.spec"
local spec_file="${TMPDIR:-/tmp}/usage__usage_spec_mise_2026_6_15.spec"
if [[ ! -f "$spec_file" ]]; then
mise usage >| "$spec_file"
fi
Expand Down
2 changes: 1 addition & 1 deletion completions/mise.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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_14.spec"
local spec_file="${TMPDIR:-/tmp}/usage__usage_spec_mise_2026_6_15.spec"
if [[ ! -f "$spec_file" ]]; then
mise usage >| "$spec_file"
fi
Expand Down
2 changes: 1 addition & 1 deletion completions/mise.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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_14.spec"
set -l spec_file "$tmpdir/usage__usage_spec_mise_2026_6_15.spec"
if not test -f "$spec_file"
mise usage | string collect > "$spec_file"
end
Expand Down
2 changes: 1 addition & 1 deletion completions/mise.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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_14.kdl"
$specFile = Join-Path $tmpDir "usage__usage_spec_mise_2026_6_15.kdl"

if (-not (Test-Path $specFile)) {
mise usage | Out-File -FilePath $specFile -Encoding utf8
Expand Down
2 changes: 1 addition & 1 deletion default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

rustPlatform.buildRustPackage {
pname = "mise";
version = "2026.6.14";
version = "2026.6.15";

src = lib.cleanSource ./.;

Expand Down
4 changes: 3 additions & 1 deletion docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ postinstall = { run = "echo installed", run_windows = "Write-Output installed" }

For `preinstall` and `postinstall`, `script = ...` is a legacy alias for `run = ...`. If a `shell` is also set on a `script`/`scripts` hook, mise warns that the shell is ignored and still runs the script with the default inline shell. Use `run = ...` with `shell = "bash -c"` to choose the inline shell command. The `script` alias for install hooks is deprecated.

The `postinstall` hook receives a `MISE_INSTALLED_TOOLS` environment variable containing a JSON array of the tools that were just installed:
A `mise install` that finds nothing to install (all configured tools are already present) still runs the `postinstall` hook — it is not skipped on a no-op install.

The `postinstall` hook receives a `MISE_INSTALLED_TOOLS` environment variable containing a JSON array of the tools that were just installed, or `[]` when nothing was installed (e.g. a no-op install). Hooks that should only act on real installs can guard on `MISE_INSTALLED_TOOLS != "[]"`:

```toml
[hooks]
Expand Down
2 changes: 2 additions & 0 deletions e2e/config/test_hooks
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ preinstall = { run = 'echo PREINSTALL' }
postinstall = { run = 'echo POSTINSTALL' }
EOF

# preinstall + postinstall hooks fire when installing
assert_contains "mise i 2>&1" "PREINSTALL"
assert_contains "mise i dummy@1 2>&1" "POSTINSTALL"
# `mise i` with nothing missing still runs postinstall (MISE_INSTALLED_TOOLS=[], #10574)
assert_contains "mise i 2>&1" "POSTINSTALL"
assert_contains "mise i dummy@1 2>&1" "POSTINSTALL"
eval "$(mise hook-env)"
Expand Down
11 changes: 11 additions & 0 deletions e2e/config/test_hooks_installed_tools
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,14 @@ else
echo "Output: $output"
exit 1
fi

# A no-op `mise install` (everything already installed) still runs postinstall,
# now with an empty JSON array rather than an unset variable (#10574)
output2=$(mise install 2>&1)
if [[ $output2 == *'INSTALLED_TOOLS=[]'* ]]; then
echo "✓ MISE_INSTALLED_TOOLS is [] on a no-op install"
else
echo "✗ MISE_INSTALLED_TOOLS should be [] on a no-op install"
echo "Output: $output2"
exit 1
fi
2 changes: 1 addition & 1 deletion packaging/rpm/mise.spec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Summary: Dev tools, env vars, and tasks in one CLI
Name: mise
Version: 2026.6.14
Version: 2026.6.15
Release: 1
URL: https://github.com/jdx/mise/
Group: System
Expand Down
12 changes: 11 additions & 1 deletion packaging/standalone/install.envsubst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ error() {
echo "$@" >&2
exit 1
}

unsupported_arch() {
arch="$1"
warn "unsupported architecture: $arch"
warn ""
warn "mise does not provide prebuilt binaries for this platform."
warn "If Rust/Cargo is available, install from source with:"
warn " cargo install --locked mise"
exit 1
}
#endregion

#region environment setup
Expand Down Expand Up @@ -67,7 +77,7 @@ get_arch() {
elif [ "$arch" = armv7l ]; then
echo "armv7$musl"
else
error "unsupported architecture: $arch"
unsupported_arch "$arch"
fi
}

Expand Down
2 changes: 1 addition & 1 deletion snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

name: mise
title: mise-en-place
version: "2026.6.14"
version: "2026.6.15"
summary: Dev tools, env vars, and tasks in one CLI
description: |
mise-en-place prepares your development environment before each command runs.
Expand Down
6 changes: 5 additions & 1 deletion src/cli/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,11 +422,15 @@ impl Install {
let (versions, install_error) = if missing.is_empty() {
measure!("run_postinstall_hook", {
info!("all tools are installed");
hooks::run_one_hook(
// Nothing was installed, but postinstall still runs (idempotent
// project setup relies on it); MISE_INSTALLED_TOOLS is [] so hooks
// can guard on actual installs. (#10574)
hooks::run_one_hook_with_context(
&config,
config.get_toolset().await?,
Hooks::Postinstall,
None,
Some(&[]),
)
.await;
(vec![], Ok(()))
Expand Down
120 changes: 98 additions & 22 deletions src/system/packages/brew/cask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ async fn fetch_archive(cask: &Cask, pr: Option<&dyn SingleReport>) -> Result<Pat
));
if !archive.exists() {
HTTP_FETCH.download_file(&cask.url, &archive, pr).await?;
// Strip macOS quarantine so it doesn't propagate into extracted/copied artifacts.
let _ = std::process::Command::new("xattr")
.args(["-d", "com.apple.quarantine"])
.arg(&archive)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
match cask.sha256.as_deref() {
Some("no_check") => {}
Expand All @@ -291,22 +298,30 @@ fn extract_archive(cask: &Cask, archive: &Path, pr: Option<&dyn SingleReport>) -
file::un_dmg(archive, &extract_dir)?;
} else {
let format = ExtractionFormat::from_file_name(filename);
if !format.is_archive() {
if format == ExtractionFormat::Raw {
// Raw executable binary — copy it using the original URL filename so find_artifact
// can match against the binary stanza source name (e.g. "claude").
let url_filename = archive_filename(&cask.url).unwrap_or_else(|| filename.to_string());
let dest = extract_dir.join(&url_filename);
file::copy(archive, &dest)?;
file::make_executable(&dest)?;
} else if !format.is_archive() {
bail!(
"brew-cask:{}: unsupported archive type for {}",
cask.token,
filename
);
} else {
file::extract_archive(
archive,
&extract_dir,
format,
&ExtractOptions {
pr,
..Default::default()
},
)?;
}
file::extract_archive(
archive,
&extract_dir,
format,
&ExtractOptions {
pr,
..Default::default()
},
)?;
}
Ok(extract_dir)
}
Expand All @@ -316,7 +331,7 @@ fn install_app(stage: &Path, caskroom: &Path, app: &AppArtifact) -> Result<()> {
.ok_or_else(|| eyre!("brew-cask: app artifact '{}' was not found", app.source))?;
let caskroom_app = caskroom.join(app_bundle_name(app.target_name())?);
file::remove_all(&caskroom_app)?;
file::copy_dir_all(&source, &caskroom_app)?;
ditto(&source, &caskroom_app)?;
let target = app_target_path(app.target_name())?;
if let Some(parent) = target.parent() {
file::create_dir_all(parent)?;
Expand All @@ -326,9 +341,50 @@ fn install_app(stage: &Path, caskroom: &Path, app: &AppArtifact) -> Result<()> {
crate::hash::hash_to_str(&target.display().to_string())
));
file::remove_all(&tmp_target)?;
file::copy_dir_all(&caskroom_app, &tmp_target)?;
file::remove_all(&target)?;
file::rename(&tmp_target, &target)?;
ditto(&caskroom_app, &tmp_target)?;
// Atomic swap: rename existing target aside before putting the new one in place so that
// a failure during rename leaves the old app intact rather than leaving nothing.
let old_target = target.with_extension(format!(
"mise-old-{}",
crate::hash::hash_to_str(&target.display().to_string())
));
file::remove_all(&old_target)?;
if target.exists() {
file::rename(&target, &old_target)?;
}
if let Err(e) = file::rename(&tmp_target, &target) {
// Restore the old app if the swap failed.
if old_target.exists() {
let _ = file::rename(&old_target, &target);
}
return Err(e);
}
file::remove_all(&old_target)?;
// Remove macOS quarantine attribute so Gatekeeper doesn't block the app.
let _ = std::process::Command::new("xattr")
.args(["-r", "-d", "com.apple.quarantine"])
.arg(&target)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
Ok(())
}

/// Copy a directory using macOS `ditto`, which preserves resource forks, extended attributes,
/// and HFS+ metadata that a plain recursive copy would strip.
fn ditto(from: &Path, to: &Path) -> Result<()> {
let status = std::process::Command::new("ditto")
.arg(from)
.arg(to)
.status()
.wrap_err("failed to run ditto")?;
if !status.success() {
bail!(
"ditto failed copying {} to {}",
from.display(),
to.display()
);
}
Ok(())
}

Expand All @@ -345,21 +401,41 @@ fn install_pkg(stage: &Path, pkg: &PkgArtifact) -> Result<()> {
}

fn stage_binary(stage: &Path, caskroom: &Path, binary: &BinaryArtifact) -> Result<()> {
let source = find_artifact(stage, &binary.source)
.filter(|path| path.is_file())
let caskroom_binary = caskroom_binary_path(caskroom, binary)?;
file::remove_all(&caskroom_binary)?;
if let Some(parent) = caskroom_binary.parent() {
file::create_dir_all(parent)?;
}
if binary.source.contains("$APPDIR") {
// $APPDIR is the Applications directory where install_app placed the bundle.
// Symlink into the installed app so the CLI wrapper can trace back to find the app.
// Check both /Applications and $HOMEBREW_PREFIX/Applications per app_target_path().
let app_binary = [
PathBuf::from("/Applications"),
prefix::prefix().join("Applications"),
]
.iter()
.map(|appdir| PathBuf::from(binary.source.replace("$APPDIR", &appdir.to_string_lossy())))
.find(|p| p.is_file())
.ok_or_else(|| {
eyre!(
"brew-cask: binary artifact '{}' was not found",
binary.source
)
})?;
let caskroom_binary = caskroom_binary_path(caskroom, binary)?;
file::remove_all(&caskroom_binary)?;
if let Some(parent) = caskroom_binary.parent() {
file::create_dir_all(parent)?;
file::make_symlink(&app_binary, &caskroom_binary)?;
} else {
let source = find_artifact(stage, &binary.source)
.filter(|path| path.is_file())
.ok_or_else(|| {
eyre!(
"brew-cask: binary artifact '{}' was not found",
binary.source
)
})?;
file::copy(&source, &caskroom_binary)?;
file::make_executable(&caskroom_binary)?;
}
file::copy(&source, &caskroom_binary)?;
file::make_executable(&caskroom_binary)?;
Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion vendor/aqua-registry/metadata.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"repository": "aquaproj/aqua-registry",
"tag": "92c705cff220bcc5c3e545eed7f4923281b51942"
"tag": "8b9d2c6527205b4a73227cb6d82918408a73f9e5"
}
Loading
Loading