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..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,9 +1659,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 - 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 new file mode 100644 index 0000000..18256f4 --- /dev/null +++ b/tests/gh-r-musl-detection.zsh @@ -0,0 +1,95 @@ +#!/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 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 + +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 +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 +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 [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 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=() + .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 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 - a host without a musl loader chose $with_curl instead of $gnu_asset" + exit 1 + } +fi + +# 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'