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'