From 15f1e7076f02db96422b7df7b4497af6d84be4bb Mon Sep 17 00:00:00 2001
From: Sal <59910950+ss-o@users.noreply.github.com>
Date: Wed, 30 Sep 2026 16:34:43 +0100
Subject: [PATCH 1/2] fix(gh-r): detect musl from its loader, not from curl
.zi-get-latest-gh-r-url-part set HAS_MUSL to linux-musl whenever curl
was installed, and its fallback probe could never be false: find exits
0 whether or not anything matches. On a glibc host every release that
publishes both builds therefore resolved to the musl asset.
Detect musl by globbing /lib for its loader instead. A regression test
serves a gnu and a musl asset offline and checks that installing curl
does not change the choice, and that a host with no musl loader picks
the glibc build.
Closes #576
---
.github/workflows/zsh-n.yml | 12 +++++++
lib/zsh/install.zsh | 6 +++-
tests/gh-r-musl-detection.zsh | 65 +++++++++++++++++++++++++++++++++++
3 files changed, 82 insertions(+), 1 deletion(-)
create mode 100644 tests/gh-r-musl-detection.zsh
diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml
index 33aefaf..7d62b5f 100644
--- a/.github/workflows/zsh-n.yml
+++ b/.github/workflows/zsh-n.yml
@@ -26,6 +26,7 @@ on:
- "tests/archive-extraction.zsh"
- "tests/completion-refresh.zsh"
- "tests/disk-ice-resolution.zsh"
+ - "tests/gh-r-musl-detection.zsh"
- "tests/home-preparation.zsh"
- "tests/hook-ownership.zsh"
- "tests/ice-tokenizer.zsh"
@@ -278,6 +279,17 @@ jobs:
- name: Test disk ice resolution
run: zsh -f tests/disk-ice-resolution.zsh
+ gh-r-musl-detection:
+ name: gh-r musl detection
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - name: Install Zsh
+ run: sudo apt update && sudo apt-get install -yq zsh
+ - name: Test gh-r musl detection
+ run: zsh -f tests/gh-r-musl-detection.zsh
+
parallel-update:
name: Parallel Update
runs-on: ubuntu-latest
diff --git a/lib/zsh/install.zsh b/lib/zsh/install.zsh
index 2054c74..7d6adaa 100755
--- a/lib/zsh/install.zsh
+++ b/lib/zsh/install.zsh
@@ -1650,7 +1650,11 @@ builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || { builtin print -P "${ZI[col
local url=https://$urlpart
fi
- if (( ${+commands[curl]} )) || find /lib/ -maxdepth 1 -name '*musl*' >/dev/null 2>&1; then
+ # A musl host ships its loader in /lib (ld-musl-*.so.1). Match the file
+ # itself: find exits 0 with no match, and whether curl is installed says
+ # nothing about the C library.
+ local -a musl_libs=( /lib/*musl*(N) )
+ if (( $#musl_libs )); then
HAS_MUSL='linux-musl'
fi
diff --git a/tests/gh-r-musl-detection.zsh b/tests/gh-r-musl-detection.zsh
new file mode 100644
index 0000000..69f4d15
--- /dev/null
+++ b/tests/gh-r-musl-detection.zsh
@@ -0,0 +1,65 @@
+#!/usr/bin/env zsh
+# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*-
+# vim: ft=zsh sw=2 ts=2 et
+# Verify that gh-r asset selection does not depend on whether curl is
+# installed (z-shell/zi#576), without network access or caller state.
+builtin emulate -R zsh
+setopt pipe_fail
+
+typeset project_root=${0:A:h:h}
+typeset temp_root=$(command mktemp -d "${TMPDIR:-/tmp}/zi-gh-r-musl.XXXXXXXX") || exit 1
+trap 'command rm -rf -- "$temp_root"' EXIT INT TERM
+export HOME=$temp_root/home ZDOTDIR=$temp_root/home TMPDIR=$temp_root/tmp
+command mkdir -p -- "$HOME" "$TMPDIR" || exit 1
+typeset -gA ZI=( HOME_DIR $temp_root/data CACHE_DIR $temp_root/cache CONFIG_DIR $temp_root/config )
+typeset -gx ZPFX=$temp_root/prefix
+source "$project_root/zi.zsh" || exit 1
+
+# A release that publishes both a glibc and a musl build for this machine.
+typeset gnu_asset=/owner/tool/releases/download/v1/tool-${CPUTYPE:-x86_64}-unknown-linux-gnu.tar.gz
+typeset musl_asset=/owner/tool/releases/download/v1/tool-${CPUTYPE:-x86_64}-unknown-linux-musl.tar.gz
+.zi-download-file-stdout() {
+ print -r -- "gnu"
+ print -r -- "musl"
+}
+typeset -gA ICE=( ver v1 )
+
+# pick_with_curl 0|1: the asset chosen with curl absent or present. The
+# function also runs grep, find and uname, so each case gets a PATH holding
+# only those, plus a curl for the second case.
+pick_with_curl() {
+ local bin=$temp_root/bin$1 tool
+ command mkdir -p -- "$bin" || return 1
+ for tool in grep find uname; do
+ command ln -sf -- "${commands[$tool]:?$tool not found}" "$bin/$tool" || return 1
+ done
+ if (( $1 )); then
+ print -r -- '#!/bin/sh' > "$bin/curl" && command chmod +x -- "$bin/curl" || return 1
+ fi
+ (
+ path=( $bin )
+ builtin hash -r
+ reply=()
+ .zi-get-latest-gh-r-url-part owner tool >/dev/null 2>&1 || exit 1
+ print -r -- $reply[1]
+ )
+}
+
+typeset without_curl with_curl
+without_curl=$(pick_with_curl 0) || { print -u2 -r -- 'not ok - no asset chosen without curl'; exit 1; }
+with_curl=$(pick_with_curl 1) || { print -u2 -r -- 'not ok - no asset chosen with curl'; exit 1; }
+
+[[ $with_curl == $without_curl ]] || {
+ print -u2 -r -- "not ok - installing curl changed the chosen asset: $without_curl -> $with_curl"
+ exit 1
+}
+
+# On a host without musl in /lib, the glibc build must win.
+typeset -a musl_libs=( /lib/*musl*(N) )
+if (( ! $#musl_libs )) && [[ $OSTYPE == linux* ]]; then
+ [[ $with_curl == $gnu_asset ]] || {
+ print -u2 -r -- "not ok - glibc host chose $with_curl instead of $gnu_asset"
+ exit 1
+ }
+fi
+print -r -- 'ok - gh-r musl detection is independent of curl'
From d69ea82266fe36b0d616395379ed0f20d17e86ae Mon Sep 17 00:00:00 2001
From: Sal <59910950+ss-o@users.noreply.github.com>
Date: Wed, 30 Sep 2026 16:52:17 +0100
Subject: [PATCH 2/2] fix(gh-r): match only the musl loader, and test the musl
path
Review of the first head found that /lib/*musl* also matched a musl
toolchain directory, which makes a glibc host with a musl toolchain look
like musl. Detection now globs ld-musl-* through a small helper,
.zi-has-musl-loader, so it can be tested against fixture directories.
The regression test sources install.zsh explicitly, checks the helper
against a loader, a toolchain directory and an empty directory, and
checks that a musl host picks the musl build even when the glibc build is
listed first. Its comments no longer claim that glibc is actively
preferred: without a musl loader the musl filter simply does not apply,
and the asset order decides between two matching builds.
Refs #576
---
lib/zsh/install.zsh | 21 +++++++++-----
tests/gh-r-musl-detection.zsh | 52 +++++++++++++++++++++++++++--------
2 files changed, 55 insertions(+), 18 deletions(-)
diff --git a/lib/zsh/install.zsh b/lib/zsh/install.zsh
index 7d6adaa..62b0fe9 100755
--- a/lib/zsh/install.zsh
+++ b/lib/zsh/install.zsh
@@ -1626,6 +1626,15 @@ builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || { builtin print -P "${ZI[col
}
# ]]]
+# FUNCTION: .zi-has-musl-loader [[[
+# Succeeds when DIR ($1) holds a musl dynamic loader (ld-musl-.so.1).
+.zi-has-musl-loader() {
+ builtin emulate -LR zsh
+ local -a loaders=( $1/ld-musl-*(N) )
+ (( $#loaders ))
+}
+# ]]]
+
# FUNCTION: .zi-get-latest-gh-r-url-part [[[
# Gets version string of latest release of given Github package.
# Connects to Github releases page.
@@ -1650,13 +1659,11 @@ builtin source "${ZI[BIN_DIR]}/lib/zsh/side.zsh" || { builtin print -P "${ZI[col
local url=https://$urlpart
fi
- # A musl host ships its loader in /lib (ld-musl-*.so.1). Match the file
- # itself: find exits 0 with no match, and whether curl is installed says
- # nothing about the C library.
- local -a musl_libs=( /lib/*musl*(N) )
- if (( $#musl_libs )); then
- HAS_MUSL='linux-musl'
- fi
+ # A musl host ships its dynamic loader in /lib as ld-musl-.so.1.
+ # Match that file: find exits 0 with no match, a broader *musl* also
+ # matches a musl toolchain directory on a glibc host, and whether curl
+ # is installed says nothing about the C library.
+ .zi-has-musl-loader /lib && HAS_MUSL='linux-musl'
local -A matchstr
matchstr=(
diff --git a/tests/gh-r-musl-detection.zsh b/tests/gh-r-musl-detection.zsh
index 69f4d15..18256f4 100644
--- a/tests/gh-r-musl-detection.zsh
+++ b/tests/gh-r-musl-detection.zsh
@@ -1,8 +1,9 @@
#!/usr/bin/env zsh
# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*-
# vim: ft=zsh sw=2 ts=2 et
-# Verify that gh-r asset selection does not depend on whether curl is
-# installed (z-shell/zi#576), without network access or caller state.
+# Verify gh-r musl detection (z-shell/zi#576) without network access or
+# caller state: it looks for the musl loader, and does not depend on
+# whether curl is installed.
builtin emulate -R zsh
setopt pipe_fail
@@ -14,6 +15,22 @@ command mkdir -p -- "$HOME" "$TMPDIR" || exit 1
typeset -gA ZI=( HOME_DIR $temp_root/data CACHE_DIR $temp_root/cache CONFIG_DIR $temp_root/config )
typeset -gx ZPFX=$temp_root/prefix
source "$project_root/zi.zsh" || exit 1
+builtin source "$project_root/lib/zsh/install.zsh" >/dev/null || { print -u2 -r -- 'not ok - source install library'; exit 1; }
+
+# Detection: a directory with the loader is musl; a musl toolchain
+# directory or an empty directory is not.
+typeset lib_musl=$temp_root/lib-musl lib_toolchain=$temp_root/lib-toolchain lib_empty=$temp_root/lib-empty
+command mkdir -p -- "$lib_musl" "$lib_toolchain/musl" "$lib_empty" || exit 1
+: > "$lib_musl/ld-musl-${CPUTYPE:-x86_64}.so.1" || exit 1
+.zi-has-musl-loader "$lib_musl" || { print -u2 -r -- 'not ok - a musl loader was not detected'; exit 1; }
+if .zi-has-musl-loader "$lib_toolchain"; then
+ print -u2 -r -- 'not ok - a musl toolchain directory was taken for a musl host'
+ exit 1
+fi
+if .zi-has-musl-loader "$lib_empty"; then
+ print -u2 -r -- 'not ok - an empty directory was taken for a musl host'
+ exit 1
+fi
# A release that publishes both a glibc and a musl build for this machine.
typeset gnu_asset=/owner/tool/releases/download/v1/tool-${CPUTYPE:-x86_64}-unknown-linux-gnu.tar.gz
@@ -24,19 +41,21 @@ typeset musl_asset=/owner/tool/releases/download/v1/tool-${CPUTYPE:-x86_64}-unkn
}
typeset -gA ICE=( ver v1 )
-# pick_with_curl 0|1: the asset chosen with curl absent or present. The
-# function also runs grep, find and uname, so each case gets a PATH holding
-# only those, plus a curl for the second case.
+# pick_with_curl 0|1 [musl]: the asset chosen with curl absent or present.
+# The function runs grep and uname, so each case gets a PATH holding only
+# those, plus a curl for the second case. With "musl", the host is taken
+# to have a musl loader.
pick_with_curl() {
local bin=$temp_root/bin$1 tool
command mkdir -p -- "$bin" || return 1
- for tool in grep find uname; do
+ for tool in grep uname; do
command ln -sf -- "${commands[$tool]:?$tool not found}" "$bin/$tool" || return 1
done
if (( $1 )); then
print -r -- '#!/bin/sh' > "$bin/curl" && command chmod +x -- "$bin/curl" || return 1
fi
(
+ [[ $2 == musl ]] && .zi-has-musl-loader() { return 0; }
path=( $bin )
builtin hash -r
reply=()
@@ -54,12 +73,23 @@ with_curl=$(pick_with_curl 1) || { print -u2 -r -- 'not ok - no asset chosen wit
exit 1
}
-# On a host without musl in /lib, the glibc build must win.
-typeset -a musl_libs=( /lib/*musl*(N) )
-if (( ! $#musl_libs )) && [[ $OSTYPE == linux* ]]; then
+# On a Linux host without a musl loader, the musl preference must not
+# apply, so the musl build is not chosen over the glibc build listed
+# first. Which of two matching builds wins otherwise is the asset order;
+# this test does not claim that glibc is actively preferred.
+if [[ $OSTYPE == linux* ]] && ! .zi-has-musl-loader /lib; then
[[ $with_curl == $gnu_asset ]] || {
- print -u2 -r -- "not ok - glibc host chose $with_curl instead of $gnu_asset"
+ print -u2 -r -- "not ok - a host without a musl loader chose $with_curl instead of $gnu_asset"
exit 1
}
fi
-print -r -- 'ok - gh-r musl detection is independent of curl'
+
+# On a musl host the musl build wins even when the glibc build is listed
+# first, so the detection result reaches the asset filter.
+typeset on_musl
+on_musl=$(pick_with_curl 0 musl) || { print -u2 -r -- 'not ok - no asset chosen on a musl host'; exit 1; }
+[[ $on_musl == $musl_asset ]] || {
+ print -u2 -r -- "not ok - a musl host chose $on_musl instead of $musl_asset"
+ exit 1
+}
+print -r -- 'ok - gh-r musl detection looks for the loader and ignores curl'