SchOS/ports/zsh/Makefile
Nexoniarz b012060699 zsh: one word in a cross-compile cache, and every $(...) hung
`X=$(echo hi)` never returned. It has been in docs/history/kernel.md section 7
as "mksh's job control still doesn't reliably progress ... root cause not yet
isolated" with three suspects, and it was none of them, and it was not the
kernel at all.

ports/zsh/Makefile carries a block of `zsh_cv_*` answers for the checks
autoconf cannot run when cross-compiling. One of them was

    zsh_cv_sys_killesrch=no

which reads like a reassuring property and is in fact the answer to the
question configure asks: *does kill(pid, 0) return ESRCH correctly?* `no` makes
configure emit BROKEN_KILL_ESRCH, and Src/zsh_system.h answers that with

    #ifdef BROKEN_KILL_ESRCH
    #undef ESRCH
    #define ESRCH EINVAL
    #endif

zsh's waitforpid() ends its wait on `errno != ESRCH` (Src/jobs.c:1569). With
ESRCH redefined to EINVAL (22) and the kernel correctly returning ESRCH (3),
that condition was true for ever: the shell sat in sigsuspend waiting for a
child it had already reaped. An instrumented zsh says the whole thing in one
line -- kill returned -1, errno is 3, and the constant it is compared against
is 22:

    WFPkill -1 3 22

**Autoconf's own cross-compilation default for that check is `yes`.** Deleting
the line would have been correct. Writing it down as "a fact about SchOS" is
what got it wrong, and the block's comment now says to read each name as the
question rather than as a property.

Verified: `X=$(echo inline)`, `$(/bin/echo external)`, backticks, nested
`$(echo $(echo deep))`, a slow child, and `$(ls /bin | wc -l)` all return.
mksh -- as /bin/sh -- was never affected.

Two corrections to yesterday's note, both confident and both wrong:

  - "Not zsh, mksh hangs the same way" was false. mksh installs as /bin/sh,
    so that test was a `command not found` whose output scrolled past. Had the
    binary been named right, this would have localised to zsh's build in one
    step instead of a day.
  - cmd/sigtest had already proven the kernel's SIGCHLD/sigsuspend contract,
    and a musl-linked probe had already shown kill(pid,0) returning -1 with
    errno == ESRCH == 3 and zsh's own loop condition evaluating false. Both
    results were right; both were read as "so it must be subtler in the
    kernel". What they actually pointed at was the one place the probe and the
    shell differed -- how each was compiled.

The lesson is narrow enough to state: when a probe that reproduces a program's
exact syscall sequence behaves correctly and the program does not, stop looking
under the syscalls and look at the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 00:29:39 +02:00

232 lines
9.9 KiB
Makefile

# SPDX-License-Identifier: Apache-2.0
# zsh -- the Z shell, MIT-like: https://www.zsh.org/
#
# Installed as /bin/zsh, *beside* mksh, which keeps /bin/sh.
#
# That is deliberate and it is not about taste. schinit's no-jobs fallback
# execs /bin/sh. If zsh were /bin/sh and zsh failed to start, the fallback
# would be the same broken binary and the machine would have no shell at all.
# Two different programs means the recovery path is a different program.
# It is also what real systems do: a small POSIX /bin/sh for scripts, a large
# interactive shell for people. docs/history/kernel.md section 2 says "exactly
# one name per role" -- these are two roles.
#
# ---------------------------------------------------------------------------
# Licensing
#
# zsh's LICENCE is MIT-like, with one condition that has to be honoured rather
# than noted: a handful of shell functions are GPL, and the licence says so
# explicitly, adding that "none of the core functions are affected by this, so
# those files may simply be omitted."
#
# So they are omitted. GPL_FILES below is that list -- four files, deleted
# after extraction and verified absent before install. Every one completes a
# command SchOS does not have (darcs, qdbus, osc, zypper), which is why this
# costs nothing at all.
include ../mk/port.mk
ZSH_VERSION := 5.9
ZSH_URL := https://sourceforge.net/projects/zsh/files/zsh/$(ZSH_VERSION)/zsh-$(ZSH_VERSION).tar.xz/download
ZSH_SHA256 := 9b8d1ecedd5b5e81fbf1918e876752a7dd948e05c1a0dba10ab863842d45acd5
TARBALL := $(DISTDIR)/zsh-$(ZSH_VERSION).tar.xz
# The GPL-licensed functions. Removed from the source tree, not merely skipped
# at install: a file that is not there cannot be shipped by a later edit to
# this Makefile.
#
# This list is four files, and it was audited rather than grepped. The obvious
# search -- `grep -rl GPL` -- returns three more, and all three are wrong:
#
# _composer completes SPDX licence identifiers, so the word
# "licence" is data, not a licence
# _luarocks has '--license=... (e.g. "MIT/X11" or "GNU GPL
# v3")' as a completion string, same thing
# VCS_INFO_get_data_hg says "Distributed under the same BSD-ish license
# as zsh itself" -- it matched on HGPLAIN, a
# Mercurial environment variable with GPL in the
# middle of it
#
# Deleting those three would cost nothing and would still be a false claim in
# a licensing file, which is the kind of thing that is believed later.
GPL_FILES := \
Completion/Unix/Command/_darcs \
Completion/Linux/Command/_qdbus \
Completion/openSUSE/Command/_osc \
Completion/openSUSE/Command/_zypper
# Completion sets for systems that are not this one. Dropping them is size,
# not licensing -- together they are most of Completion/ and none of them can
# ever match a command that exists here.
FOREIGN_COMP := Completion/openSUSE Completion/Mandriva Completion/Redhat \
Completion/Debian Completion/Solaris Completion/Cygwin \
Completion/AIX Completion/BSD Completion/Darwin
HOSTCC_ZSH ?= gcc
GCC_INC := $(shell $(HOSTCC_ZSH) -print-file-name=include)
LIBGCC := $(shell $(HOSTCC_ZSH) -print-libgcc-file-name)
# Split the way autoconf consumes them -- see ports/ncurses/Makefile, which
# had to learn the same lesson: crt objects ride in LDFLAGS (before the object
# being linked), libraries in LIBS (after it).
ZSH_CPPFLAGS := -nostdinc -isystem $(MUSL)/include -isystem $(GCC_INC) \
-isystem $(NCURSES)/include -D__SCHOS__
# zsh 5.9 is from 2022; GCC 14 and 15 promoted three long-standing warnings to
# errors by default, and old autoconf projects fail their own feature probes on
# them. The probe that matters here is
#
# char **test = boolcodes; puts(*test);
#
# which ncurses answers with `const char * const boolcodes[]`. GCC 15 rejects
# the conversion outright, so the test "fails", HAVE_BOOLCODES goes undefined,
# and zsh helpfully declares its own `static char *boolcodes[]` -- which then
# collides with the declaration in term.h that was there the whole time. The
# error surfaces in termcap.c, three steps from the cause.
#
# These restore the diagnostic level the code was written against. They are
# scoped to this port and nothing else in the tree relaxes them.
ZSH_COMPAT := -Wno-incompatible-pointer-types -Wno-implicit-function-declaration \
-Wno-int-conversion
ZSH_CFLAGS := -O2 $(CFLAGS_BT) $(ZSH_COMPAT)
ZSH_LDFLAGS := -static -nostdlib -L$(MUSL)/lib -L$(NCURSES)/lib \
$(MUSL)/lib/crt1.o $(MUSL)/lib/crti.o
ZSH_LIBS := -lncursesw $(MUSL)/lib/libc.a $(LIBGCC) $(MUSL)/lib/libc.a \
$(MUSL)/lib/crtn.o
# Answers configure cannot work out without running a program on the target.
# Each one is a fact about SchOS, so each one is written down here rather than
# guessed by autoconf's cross-compilation defaults.
#
# **Read each name as the question configure asks, not as a property.** These
# are `zsh_cv_*` cache entries for AC_CACHE_CHECK, and the value answers the
# *check's* wording. `zsh_cv_sys_killesrch` is "does kill(pid, 0) return ESRCH
# correctly", so `yes` is the healthy answer and `no` means broken.
#
# It was `no` here, and that single word cost every `$(command substitution)`
# in the shell. `no` makes configure emit BROKEN_KILL_ESRCH, and zsh_system.h
# answers that with `#undef ESRCH / #define ESRCH EINVAL`. zsh's waitforpid()
# ends its wait on `errno != ESRCH`; with ESRCH redefined to EINVAL (22) and
# the kernel correctly returning ESRCH (3), that test was true for ever and
# the shell waited on a child it had already reaped. Every `X=$(...)` hung.
#
# The cruel part: autoconf's own cross-compilation default for this one is
# `yes`. Leaving the line out entirely would have been correct.
ZSH_CACHE := \
zsh_cv_shared_environ=yes \
zsh_cv_shared_tgetent=no \
zsh_cv_shared_tigetstr=no \
zsh_cv_sys_nis=no \
zsh_cv_sys_nss=no \
zsh_cv_func_realpath_accepts_null=yes \
zsh_cv_sys_path_dev_fd=/dev/fd \
zsh_cv_sys_fifo=yes \
zsh_cv_sys_killesrch=yes \
zsh_cv_sys_signal_handler=void \
zsh_cv_prog_tcsetpgrp=yes \
ac_cv_func_mmap_fixed_mapped=yes \
ac_cv_func_setpgrp_void=yes
# --disable-dynamic: SchOS links everything static, and zsh's module system
# would otherwise want dlopen. Every module is compiled in instead.
# --with-term-lib: name it, so configure cannot quietly decide it needs none
# and leave the line editor unable to move a cursor.
# --enable-etcdir: where zsh looks for zshenv/zshrc. NOT --sysconfdir, which
# autoconf accepts, zsh ignores, and configure reports no complaint about --
# the shell simply starts with its default prompt and no completion, looking
# like a broken config file rather than a config file that was never read.
ZSH_CONFIGURE := \
--host=x86_64-linux-musl \
--build=x86_64-pc-linux-gnu \
--prefix=/usr \
--bindir=/bin \
--enable-etcdir=/etc/zsh \
--disable-dynamic \
--enable-multibyte \
--with-term-lib="ncursesw" \
--disable-gdbm \
--disable-pcre \
--disable-cap \
--disable-largefile \
--enable-zsh-secure-free \
--enable-fndir=/usr/share/zsh/$(ZSH_VERSION)/functions \
--enable-scriptdir=/usr/share/zsh/$(ZSH_VERSION)/scripts
$(TARBALL): | $(DISTDIR)
curl -fsSL -o $@ $(ZSH_URL)
sh $(SHA256CHECK) "$(ZSH_SHA256)" "$@"
download: $(TARBALL)
# rm -rf before the move, and touch after it. Both matter:
#
# Without the rm, `mv zsh-5.9 src` when src already exists moves the tree
# *inside* it, producing src/zsh-5.9 -- and zsh's configure globs */*/*.mdd,
# so it finds the nested copy and writes modfile=zsh-5.9/Src/zsh.mdd into
# config.modules. The build then fails a long way from the cause.
#
# Without the touch, this rule re-runs whenever the directory's mtime is
# older than the tarball's, which is how src came to exist in the first
# place. Editing files *inside* a directory does not update the directory.
$(SRCDIR): $(TARBALL)
mkdir -p $(PORTDIR)
rm -rf $@ $(PORTDIR)/zsh-$(ZSH_VERSION)
tar -xJf $< -C $(PORTDIR)
mv $(PORTDIR)/zsh-$(ZSH_VERSION) $@
@echo "zsh: removing GPL-licensed functions named by zsh's own LICENCE"
@cd $@ && rm -f $(GPL_FILES)
@cd $@ && rm -rf $(FOREIGN_COMP)
touch $@
extract: $(SRCDIR)
$(BUILDDIR)/config.status: | $(SRCDIR)
@test -f $(NCURSES)/lib/libncursesw.a || { \
echo "zsh: build ports/ncurses first -- no libncursesw.a" >&2; exit 1; }
mkdir -p $(BUILDDIR)
cd $(BUILDDIR) && \
CC="$(HOSTCC_ZSH)" \
CPPFLAGS="$(ZSH_CPPFLAGS)" \
CFLAGS="$(ZSH_CFLAGS)" \
LDFLAGS="$(ZSH_LDFLAGS)" \
LIBS="$(ZSH_LIBS)" \
$(ZSH_CACHE) \
$(SRCDIR)/configure $(ZSH_CONFIGURE)
$(BUILDDIR)/Src/zsh: $(BUILDDIR)/config.status
$(MAKE) -C $(BUILDDIR)
build: $(BUILDDIR)/Src/zsh
# Refuse to install if a GPL file is somehow present. The rm above should have
# made this impossible; the check is here because "should have" is not a
# licence audit, and this runs before every image is built.
.PHONY: licence-check
licence-check: | $(SRCDIR)
@bad=0; for f in $(GPL_FILES); do \
if [ -e "$(SRCDIR)/$$f" ]; then echo "zsh: GPL file present: $$f" >&2; bad=1; fi; \
done; \
if [ $$bad -ne 0 ]; then echo "zsh: refusing to install" >&2; exit 1; fi; \
echo "zsh: no GPL-licensed files in the tree"
# licence-check runs *inside* the rule, not as a prerequisite.
#
# It is .PHONY, and a phony prerequisite is always out of date -- so having it
# on the left of the colon re-ran the whole install on every `make`, which is
# slow, and which eventually failed outright when zsh's installfns.sh tripped
# over the installfnsdir its previous run had left behind.
$(SYSROOT)/bin/zsh: $(BUILDDIR)/Src/zsh | $(SYSROOT)/bin
$(MAKE) licence-check
rm -rf $(BUILDDIR)/installfnsdir
$(MAKE) -C $(BUILDDIR) install DESTDIR=$(SYSROOT)
rm -rf $(SYSROOT)/usr/share/man $(SYSROOT)/usr/share/zsh/$(ZSH_VERSION)/help
chmod 755 $@
install: $(SYSROOT)/bin/zsh
clean:
rm -rf $(PORTDIR)
distclean: clean
rm -f $(TARBALL)