kernel: processes have an environment now, and zsh is the shell

Porting zsh found four defects in the kernel and one in the filesystem. None
of them were about zsh; all of them had been there the whole time.

**exec threw the environment away.** kernel/fs/exec.c wrote a single zero for
envp with the comment "(no environment)" -- not yet, but ever, for every
process. musl has been passing a perfectly good envp to execve (Linux 59, which
SchOS answers) and it was never read. So $TERM was unset, $PATH was whatever
each program invented, and a variable exported in a shell did not survive the
exec meant to carry it. exec() takes an envp, sys_exec reads argument three,
and schinit manifests grew an Environment key. The userland two-argument
exec(path, argv) stays as a wrapper that passes null explicitly -- the raw stub
is execve now, so a short caller cannot leave %rdx holding a stray pointer.

**Which overflowed the kernel stack.** Three pointer arrays, ~1.8 KB, on a
4096-byte stack, under exec -> loaduvm -> readi -> SchFS -> btree. Pid 1 went
down on the first exec. The canary caught it and named the process, which is
what it is for. They live in one kalloc'd page now.

**waitpid returned a bare -1** for both "no children" and "interrupted". musl
reads [-4095,-1] as -errno, so userspace saw EPERM; zsh printed "wait failed:
operation not permitted" and abandoned the rest of /etc/zsh/zshrc. -ECHILD and
-EINTR now. Third time a bare -1 has cost real time -- on a system with no
permission model, EPERM should be read as "a syscall returned -1".

**NVMA was 64.** musl's allocator takes a mapping per size class and does not
release them eagerly, so compinit ran out of slots and got -1, which is EPERM,
which malloc turns into NULL, which zsh reports as "fatal error: out of memory"
-- on a machine with 1000 MB free. 256 slots, and -ENOMEM.

**And a small write to SchFS costs 58 ms.** 200 writes to one open file take
11.75 s; the same to /dev/null take 0.14 s. That is the design stated at the
top of kmount.c -- one transaction group per operation, and a commit is two
flushes and an uberblock -- not a bug. But compdump writes its digest with
thousands of separate prints, so it takes minutes and looks like a hang.
compinit -D, which skips the dump, finishes in 3.6 s. This one is a decision
rather than a fix, so it is written up and left: /etc/zsh/zshrc keeps compinit
switched off and explains exactly why, with the measurements.

zsh runs as /bin/zsh and is the console shell. ZLE works -- arrow keys recall
history through terminfo, $COLUMNS reads the real 160 because TIOCGWINSZ was
fixed, colour and prompt expansion work, startup is 0.14 s. mksh keeps /bin/sh,
so schinit's no-jobs fallback is a different program from the one that could
have failed.

Also fixed while here: AT_CLKTCK was the literal 100 and is USER_HZ; the
extract rule in four ports could move a source tree inside itself when make
re-ran it, which is how zsh's configure came to write modfile=zsh-5.9/... into
config.modules and fail three steps later.

schfs-check passes. Full writeup in docs/history/kernel.md section 14.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-12 23:26:50 +02:00
commit 14f0e4d0e2
22 changed files with 684 additions and 24 deletions

View file

@ -899,8 +899,9 @@ $(MKFS): tools/mkfs.c include/fs.h include/stat.h include/param.h
# /etc/passwd and /etc/motd and should not become true of the boot # /etc/passwd and /etc/motd and should not become true of the boot
# configuration as well. # configuration as well.
SCHINIT_JOBS = $(wildcard etc/schinit/*.job) etc/schinit/README SCHINIT_JOBS = $(wildcard etc/schinit/*.job) etc/schinit/README
ETC_ZSH = $(wildcard etc/zsh/*)
ETC_FILES = $(wildcard etc/*) ETC_FILES = $(wildcard etc/*)
ETC_PLAIN = $(filter-out etc/schinit,$(ETC_FILES)) ETC_PLAIN = $(filter-out etc/schinit etc/zsh,$(ETC_FILES))
# The mark as characters, for anything that draws a logo into a terminal -- # The mark as characters, for anything that draws a logo into a terminal --
# fastfetch takes it with --file-raw. Installed from docs/branding rather than # fastfetch takes it with --file-raw. Installed from docs/branding rather than
@ -912,12 +913,13 @@ ETC_PLAIN = $(filter-out etc/schinit,$(ETC_FILES))
# anything with more room. # anything with more room.
LOGO_ASCII = docs/branding/Ascii/ascii_50.txt LOGO_ASCII = docs/branding/Ascii/ascii_50.txt
etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(LOGO_ASCII) etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(ETC_ZSH) $(LOGO_ASCII)
@mkdir -p $(SYSROOT)/etc/schinit $(SYSROOT)/usr/share/schos @mkdir -p $(SYSROOT)/etc/schinit $(SYSROOT)/etc/zsh $(SYSROOT)/usr/share/schos
@cp $(ETC_PLAIN) $(SYSROOT)/etc/ @cp $(ETC_PLAIN) $(SYSROOT)/etc/
@cp $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/ @cp $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/
@cp $(ETC_ZSH) $(SYSROOT)/etc/zsh/
@cp $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt @cp $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt
@echo "installed $(words $(ETC_PLAIN)) /etc file(s) and $(words $(SCHINIT_JOBS)) schinit manifest(s)" @echo "installed $(words $(ETC_PLAIN)) /etc file(s), $(words $(ETC_ZSH)) zsh file(s) and $(words $(SCHINIT_JOBS)) schinit manifest(s)"
# The mount points. Created here rather than by init, because a mkdir on a # The mount points. Created here rather than by init, because a mkdir on a
# fresh SchFS is a whole transaction -- a copy-on-write rewrite of the path to # fresh SchFS is a whole transaction -- a copy-on-write rewrite of the path to
@ -927,7 +929,7 @@ etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(LOGO_ASCII)
# #
# Not committed as empty directories with a .gitkeep, because /sys and /run are # Not committed as empty directories with a .gitkeep, because /sys and /run are
# not mounted over and the marker file would be visible in them. # not mounted over and the marker file would be visible in them.
SYSROOT_DIRS = dev proc sys run tmp SYSROOT_DIRS = dev proc sys run tmp root
sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS)) etc-install sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS)) etc-install
@mkdir -p $(addprefix $(SYSROOT)/, $(SYSROOT_DIRS)) @mkdir -p $(addprefix $(SYSROOT)/, $(SYSROOT_DIRS))

26
NOTICE
View file

@ -50,6 +50,32 @@ Portions derived from the Public Domain Korn Shell (pdksh)
Built from ports/mksh. Supplies /bin/sh. The MirOS Licence is permissive and Built from ports/mksh. Supplies /bin/sh. The MirOS Licence is permissive and
requires the copyright notice be retained. requires the copyright notice be retained.
--------------------------------------------------------------------------------
zsh 5.9 MIT-like
--------------------------------------------------------------------------------
Copyright (c) 1992-2017 Paul Falstad, Richard Coleman, Zoltan Hidvegi,
Andrew Main, Peter Stephenson, Sven Wischnowsky, and others
Built from ports/zsh. Installed as /bin/zsh and used as the interactive shell;
/bin/sh remains mksh.
zsh's LICENCE notes that a few shell functions are licensed under the GNU
General Public Licence and states that, since no core function is affected,
those files may simply be omitted. They are. ports/zsh/Makefile removes four
files after extraction -- completions for darcs, qdbus, osc and zypper -- and
refuses to install if any of them is still present. No GPL-licensed file is
shipped in a SchOS image.
--------------------------------------------------------------------------------
ncurses 6.4 MIT License
--------------------------------------------------------------------------------
Copyright 2018-2022 Thomas E. Dickey
Copyright 1998-2018 Free Software Foundation, Inc.
Built from ports/ncurses. Linked into zsh so that its line editor can drive a
terminal, and into sysroot/bin/tinfotest. Built with --disable-database and
compiled-in fallbacks, so no terminfo files are installed.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
lwIP 2.2.0 BSD 3-Clause lwIP 2.2.0 BSD 3-Clause
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View file

@ -104,12 +104,22 @@ outn(int v)
// Jobs // Jobs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#define MAXENVV 16 // Environment lines honoured per job
struct job { struct job {
char label[MAXLABEL]; char label[MAXLABEL];
char program[MAXPATH]; char program[MAXPATH];
char *argv[MAXARGS + 1]; char *argv[MAXARGS + 1];
char argstore[MAXLINE]; char argstore[MAXLINE];
// Environment, one "KEY=VALUE" per Environment line, repeatable. Stored
// as pointers into envstore so the job owns its own strings -- the parse
// buffer is gone by the time anything execs.
char *envp[MAXENVV + 1];
char envstore[MAXLINE * 2];
int envlen;
int envn;
char stdin_path[MAXPATH]; char stdin_path[MAXPATH];
char stdout_path[MAXPATH]; char stdout_path[MAXPATH];
char stderr_path[MAXPATH]; char stderr_path[MAXPATH];
@ -192,6 +202,27 @@ split_args(struct job *j, const char *value)
// into sock_path rather than treated as a job key. // into sock_path rather than treated as a job key.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// One "KEY=VALUE" onto the job's environment. Repeatable, so a manifest reads
//
// Environment = TERM=linux
// Environment = PATH=/bin:/usr/bin
//
// rather than inventing a separator that a value might contain.
static void
add_env(struct job *j, const char *kv)
{
if (j->envn >= MAXENVV)
return;
int n = slen(kv);
if (j->envlen + n + 1 > (int)sizeof(j->envstore))
return;
char *dst = j->envstore + j->envlen;
scopy(dst, kv, n + 1);
j->envp[j->envn++] = dst;
j->envp[j->envn] = 0;
j->envlen += n + 1;
}
static char * static char *
trim(char *s) trim(char *s)
{ {
@ -267,6 +298,7 @@ load_job(const char *path)
else if (sieq(key, "Disabled")) j->disabled = truthy(val); else if (sieq(key, "Disabled")) j->disabled = truthy(val);
else if (sieq(key, "ThrottleInterval")) j->throttle = parse_int(val); else if (sieq(key, "ThrottleInterval")) j->throttle = parse_int(val);
else if (sieq(key, "ControllingTerminal")) j->own_tty = truthy(val); else if (sieq(key, "ControllingTerminal")) j->own_tty = truthy(val);
else if (sieq(key, "Environment")) add_env(j, val);
else if (sieq(key, "StandardIn")) scopy(j->stdin_path, val, MAXPATH); else if (sieq(key, "StandardIn")) scopy(j->stdin_path, val, MAXPATH);
else if (sieq(key, "StandardOut")) scopy(j->stdout_path, val, MAXPATH); else if (sieq(key, "StandardOut")) scopy(j->stdout_path, val, MAXPATH);
else if (sieq(key, "StandardError")) scopy(j->stderr_path, val, MAXPATH); else if (sieq(key, "StandardError")) scopy(j->stderr_path, val, MAXPATH);
@ -379,7 +411,7 @@ start_job(struct job *j)
// exec.c enables the Linux ABI automatically for musl binaries. Do NOT // exec.c enables the Linux ABI automatically for musl binaries. Do NOT
// call enable_linux_abi() here: it would renumber the syscalls this // call enable_linux_abi() here: it would renumber the syscalls this
// child still has to make. // child still has to make.
exec(j->program, j->argv); execve(j->program, j->argv, j->envn ? j->envp : 0);
out("schinit: cannot exec "); out(j->program); out("schinit: cannot exec "); out(j->program);
out(" for "); out(j->label); out("\n"); out(" for "); out(j->label); out("\n");

View file

@ -904,3 +904,130 @@ separate and deliberately does not follow it: musl answers
are scaled at the syscall boundary the way Linux does it. Before, `sys_times` are scaled at the syscall boundary the way Linux does it. Before, `sys_times`
returned raw ticks -- correct only while `HZ` happened to be 100, and silently returned raw ticks -- correct only while `HZ` happened to be 100, and silently
wrong the day anyone changed it. wrong the day anyone changed it.
## 14. A shell that would not start, and four bugs underneath it
Porting zsh was supposed to be a port. It found four defects in the kernel and
one in the filesystem, none of which had anything to do with zsh, and every one
of which had been there the whole time.
### The environment did not exist
`kernel/fs/exec.c` built the initial stack for every new program and wrote this:
```c
stack_slots[slot++] = 0; // end of envp (no environment)
```
Not "no environment yet" — no environment, ever, for any process. musl calls
`execve(path, argv, envp)`, which is Linux syscall 59, which SchOS answers; the
third argument was simply never read. So `$TERM` was unset, `$PATH` was whatever
each program invented for itself, and a variable exported in a shell did not
survive the exec that was supposed to carry it.
That is fatal for zsh specifically. A line editor with no `TERM` cannot ask
terminfo how to move a cursor, so ZLE is exactly as useless as it would be with
no terminal library linked at all.
`exec()` takes an `envp` now, `sys_exec` reads the third argument, and schinit
manifests grew an `Environment` key:
```
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
```
The userland `exec(path, argv)` spelling stays, as a wrapper in `lib/glue.c`
that passes a null environment explicitly. The raw stub is `execve` now —
otherwise a two-argument caller would leave `%rdx` holding whatever happened to
be in it, and the kernel would read that as a pointer.
### Which immediately overflowed the kernel stack
The three pointer arrays exec needs came to about 1.8 KB, and `KSTACKSIZE` is
4096, under a call chain that already runs exec → loaduvm → readi → SchFS →
btree. The first exec after the change took pid 1 down.
The canary caught it cleanly, named the process, and printed a stop code, which
is the whole reason it exists. The arrays live in one `kalloc`'d page now.
### `wait` said "operation not permitted"
```
compdef:80: wait failed: operation not permitted
```
`waitpid` returned a bare `-1` for two different failures: no children to wait
for, and interrupted by a signal. musl reads any return in [-4095, -1] as
`-errno`, so `-1` arrived in userspace as `EPERM`.
zsh saw it, printed that message, and abandoned the rest of `/etc/zsh/zshrc` —
so the shell came up with its default prompt and no completion, which reads as
a broken config file rather than as a `wait()` that could not say what happened.
It returns `-ECHILD` and `-EINTR` now.
This is the third time a bare `-1` has cost real time (see §12, `sys_chdir`).
**EPERM on SchOS should be read as "a syscall returned -1 and meant something
else."** There is no permission model here for it to legitimately come from.
### mmap ran out of slots and blamed memory
```
compdef:80: fatal error: out of memory
```
On a machine with 1000 MB free, of which zsh had used 1.5. `NVMA` was 64 — the
per-process mmap table — and musl's allocator takes a fresh mapping per size
class and does not release them eagerly. compinit reads twelve hundred
completion functions, ran out of slots, and got `-1` from `sys_mmap`, which is
`EPERM` again, which `malloc` turns into NULL, which zsh reports as out of
memory. Three translations from the truth.
`NVMA` is 256 and the failure returns `-ENOMEM`.
### And underneath all of it: a small write costs 58 ms
With everything above fixed, `compinit` still appeared to hang. It was not
hanging. Measured on this machine, 200 writes:
| | time | per write |
|---|---|---|
| to one open file on SchFS | 11.75 s | **58 ms** |
| open + write + close each time | 21.39 s | **107 ms** |
| to `/dev/null` | 0.14 s | 0.7 ms |
`compdump` writes its digest with a few thousand separate `print` calls. At
58 ms each that is minutes, and it looks exactly like a hang. `compinit -D`,
which skips the dump, finishes in 3.6 seconds — the scan was never the problem.
The cause is not a bug; it is the design, stated plainly at the top of
`kernel/fs/schfs/kmount.c`:
> Every entry point below that changes anything is one transaction group: it
> takes the volume lock, does its work, and commits before returning. That makes
> an operation durable the moment it returns.
A commit is two cache flushes and an uberblock write, and a flush is a barrier.
`schfs_ktx_open` already lets one `write(2)` of 1 MB be a single group — 267
block writes instead of 1410 — but nothing brackets *across* syscalls, so two
thousand small writes are two thousand barriers.
`cp` does not notice, because it uses large buffers. Anything that writes a line
at a time notices enormously.
**This is the open question, and it is a design decision rather than a fix.**
Deferring commits would mean giving up durable-on-return, which is a property
SchFS chose deliberately. The options are a writeback window like every other
filesystem has, committing on `fsync`/`close`/timer, or leaving it and accepting
that line-at-a-time writers are slow. Until it is decided, `/etc/zsh/zshrc`
leaves `compinit` switched off and says why.
### What zsh actually got
It runs, as `/bin/zsh`, and it is the console shell. ZLE works: arrow keys
recall history through terminfo, `$COLUMNS` reads 160 because `TIOCGWINSZ` now
answers with the real console geometry (§13), prompt expansion and colour work,
and startup is 0.14 s. mksh keeps `/bin/sh`, so schinit's no-jobs fallback is a
different program from the one that might have failed.
Completion is the piece that waits on the filesystem.

View file

@ -1,11 +1,23 @@
# The interactive shell on the system console. # The interactive shell on the system console.
Label = os.schos.console-shell Label = os.schos.console-shell
Program = /bin/sh # zsh, not /bin/sh. mksh stays as /bin/sh and is what schinit falls back to
Args = sh # when no manifest loads -- so if zsh ever fails to start, the recovery shell
# is a different program rather than the same broken one. See ports/zsh.
Program = /bin/zsh
Args = -zsh
RunAtLoad = yes RunAtLoad = yes
KeepAlive = yes KeepAlive = yes
# The environment this job's shell starts with. Until schinit grew an
# Environment key, every process on SchOS started with an empty environment --
# the kernel built one and exec threw it away. $TERM is the one that matters
# most: a line editor with no TERM cannot ask terminfo how to move a cursor.
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
# This job reads the console, so it has to own it: schinit puts every job in # This job reads the console, so it has to own it: schinit puts every job in
# its own session, and a session with no controlling terminal is not delivered # its own session, and a session with no controlling terminal is not delivered
# terminal input at all. Without this the shell echoes and executes nothing. # terminal input at all. Without this the shell echoes and executes nothing.

14
etc/zsh/zshenv Normal file
View file

@ -0,0 +1,14 @@
# /etc/zsh/zshenv — read by every zsh, interactive or not, before anything else.
#
# Only things that must be true for a non-interactive shell as well. schinit
# sets the console shell's environment from its job manifest, but a zsh started
# any other way (a script, an ssh session one day) gets no such help.
: ${TERM:=linux}
: ${HOME:=/root}
: ${PATH:=/bin:/usr/bin}
export TERM HOME PATH
# Where zsh's own functions live. Set here rather than compiled in so a second
# version can be installed beside this one without either moving.
fpath=(/usr/share/zsh/5.9/functions $fpath)

113
etc/zsh/zshrc Normal file
View file

@ -0,0 +1,113 @@
# /etc/zsh/zshrc — the system-wide interactive zsh configuration for SchOS.
#
# Deliberately short. This sets up the things an interactive shell is useless
# without -- history, completion, and the keys a console sends -- and stops.
# Anything beyond that belongs in a user's own ~/.zshrc, which is the file zsh
# reads after this one.
# ---------------------------------------------------------------------------
# History
# ---------------------------------------------------------------------------
HISTFILE=${HOME:-/root}/.zsh_history
HISTSIZE=2000
SAVEHIST=2000
setopt APPEND_HISTORY INC_APPEND_HISTORY SHARE_HISTORY
setopt HIST_IGNORE_DUPS HIST_IGNORE_SPACE HIST_REDUCE_BLANKS
# ---------------------------------------------------------------------------
# Behaviour
# ---------------------------------------------------------------------------
setopt AUTO_CD # `cd /tmp` is implied by typing `/tmp`
setopt EXTENDED_GLOB
setopt INTERACTIVE_COMMENTS # a # in an interactive line is a comment
setopt NO_BEEP # SchOS has no audio, but it would also scroll
unsetopt FLOW_CONTROL # ^S/^Q are worth more as keys than as flow control
# ---------------------------------------------------------------------------
# Keys
# ---------------------------------------------------------------------------
# Emacs bindings, because that is what every other line editor on the system
# does -- mksh's default and the kernel's own canonical-mode editing both.
bindkey -e
# Home, End, Delete and the arrows, taken from terminfo rather than written
# out as escape sequences. The sequences differ between the SchOS console and
# a serial terminal; the terminal's own description does not.
typeset -A _sch_keys=(
"${terminfo[khome]}" beginning-of-line
"${terminfo[kend]}" end-of-line
"${terminfo[kdch1]}" delete-char
"${terminfo[kich1]}" overwrite-mode
"${terminfo[kcuu1]}" up-line-or-search
"${terminfo[kcud1]}" down-line-or-search
"${terminfo[kcub1]}" backward-char
"${terminfo[kcuf1]}" forward-char
)
for _k _w in "${(@kv)_sch_keys}"; do
[[ -n $_k ]] && bindkey -- "$_k" "$_w"
done
unset _sch_keys _k _w
# A terminal that has just been resized, or that zsh has just taken over, may
# not have told terminfo yet. This puts the keypad in the mode the escape
# sequences above were described for, and takes it back out on exit.
if (( ${+terminfo[smkx]} && ${+terminfo[rmkx]} )); then
autoload -Uz add-zle-hook-widget 2>/dev/null
function _sch_zle_init { echoti smkx }
function _sch_zle_line_finish { echoti rmkx }
zle -N zle-line-init _sch_zle_init
zle -N zle-line-finish _sch_zle_line_finish
fi
# ---------------------------------------------------------------------------
# Completion
# ---------------------------------------------------------------------------
# Completion is NOT initialised here yet, and the reason is a filesystem
# measurement rather than anything to do with zsh.
#
# A small write to SchFS costs about 58 ms, because every operation is its own
# transaction group and a commit is two cache flushes and an uberblock write --
# see the comment at the top of kernel/fs/schfs/kmount.c, which describes the
# trade-off exactly. Measured on this machine, 200 writes:
#
# to one open file on SchFS 11.75 s (58 ms each)
# open + write + close each 21.39 s (107 ms each)
# to /dev/null 0.14 s (0.7 ms each)
#
# compdump writes the digest with a few thousand separate `print` calls, so
# compinit -d takes minutes and looks exactly like a hang. compinit itself is
# fine: with -D, which skips the dump, it finishes in 3.6 seconds.
#
# So this waits for deferred commits. When small writes stop costing a disk
# barrier each, delete this comment and uncomment the three lines below.
#
# autoload -Uz compinit
# compinit -u -d ${HOME:-/root}/.zcompdump
#
# Until then, completion is available on demand in a shell you do not mind
# waiting three seconds for:
#
# autoload -Uz compinit && compinit -D -u
#
# -u is not optional whenever this is turned back on. Without it compinit runs
# compaudit, decides the function directories are "insecure" because SchOS has
# no ownership model to check, and *prompts* for an answer on a shell that is
# still starting up -- which is a real hang, unlike the one above.
# These take effect as soon as compinit is turned on above; a zstyle with no
# completion system to read it is inert, not an error.
zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' # case-insensitive
zstyle ':completion:*' list-colors ''
zstyle ':completion:*:descriptions' format '%F{244}%d%f'
# ---------------------------------------------------------------------------
# Prompt
# ---------------------------------------------------------------------------
# The accent violet from include/theme.h, which is 99 in the 256-colour cube
# and falls back to magenta on the 8-colour console. The last-status marker is
# the only thing here that ever turns red -- same rule as the rest of the
# system: normal states are calm, only failure is loud.
setopt PROMPT_SUBST
PROMPT='%F{magenta}%B%n@schos%b%f %F{cyan}%~%f %(?.%F{green}❯.%F{red}❯)%f '
RPROMPT=''

View file

@ -79,7 +79,7 @@ void consoleintr(int(*)(void));
void panic(char*) __attribute__((noreturn)); void panic(char*) __attribute__((noreturn));
// exec.c // exec.c
int exec(char*, char**); int exec(char*, char**, char**);
// file.c // file.c
struct file* filealloc(void); struct file* filealloc(void);

View file

@ -43,6 +43,7 @@
#define ROOTDEV 1 // device number of file system root disk #define ROOTDEV 1 // device number of file system root disk
#endif #endif
#define MAXARG 32 // max exec arguments #define MAXARG 32 // max exec arguments
#define MAXENV 64 // max environment variables passed through exec
#define MAXPATH 128 // max path #define MAXPATH 128 // max path
#define MAXOPBLOCKS 30 // max # of blocks any FS op writes #define MAXOPBLOCKS 30 // max # of blocks any FS op writes
#define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log #define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log

View file

@ -3,7 +3,18 @@
#include "types.h" #include "types.h"
#include "traps.h" #include "traps.h"
#define NVMA 64 // Simultaneous mmap() regions per process.
//
// 64 was enough for every first-party program and for toybox, and not enough
// for zsh: musl's allocator takes a fresh mapping per size class and does not
// release them eagerly, so compinit -- which reads twelve hundred completion
// functions -- ran out of slots and reported "fatal error: out of memory" on
// a machine with a gigabyte free. The slots were the limit, not the memory.
//
// The table is inline in struct proc, so this is NPROC * NVMA * sizeof(struct
// vma) of kernel memory, always resident. At 256 that is about 1 MB total,
// which is worth it to stop a shell dying on a machine that has 1000 MB spare.
#define NVMA 256
struct vma { struct vma {
uint64 addr; uint64 addr;

View file

@ -18,7 +18,8 @@ int write(int, void*, int);
int read(int, void*, int); int read(int, void*, int);
int close(int); int close(int);
int kill(int); int kill(int);
int exec(char*, char**); int exec(char*, char**); /* execve(path, argv, 0) */
int execve(char*, char**, char**);
int open(char*, int); int open(char*, int);
int mknod(char*, short, short); int mknod(char*, short, short);
int unlink(char*); int unlink(char*);

View file

@ -18,20 +18,39 @@
extern int linux_syscall_map[]; extern int linux_syscall_map[];
extern int linux_syscall_map_size; extern int linux_syscall_map_size;
// envp may be null, which means "no environment" -- the same thing this
// function used to do unconditionally.
int int
exec(char *path, char **argv) exec(char *path, char **argv, char **envp)
{ {
uint64 _t0 = rdtsc(); uint64 _t0 = rdtsc();
fsstat.exec_calls++; fsstat.exec_calls++;
char *s, *last; char *s, *last;
int i, off; int i, off;
uint64 argc, sz, sp, ustack[MAXARG+1]; uint64 argc, envc, sz, sp;
struct elfhdr elf; struct elfhdr elf;
struct inode *ip; struct inode *ip;
struct proghdr ph; struct proghdr ph;
pml4e_t *pgdir, *oldpgdir; pml4e_t *pgdir, *oldpgdir;
uint64 stack_slots[MAXARG + 32];
int slot; int slot;
// The three pointer arrays live on the heap, not on the kernel stack.
//
// Together they are about 1.8 KB, and KSTACKSIZE is 4096 -- with the call
// chain under this function running exec -> loaduvm -> readi -> SchFS ->
// btree, there is not 1.8 KB to spare. Putting them here overflowed the
// stack on the first exec after the environment was added, and the canary
// in param.h caught it on pid 1 before it could corrupt anything.
//
// One page holds all three with room over.
uint64 *scratch = (uint64*)kalloc();
if(!scratch)
return -1;
uint64 *ustack = scratch; /* MAXARG+1 */
uint64 *estack = ustack + MAXARG + 1; /* MAXENV+1 */
uint64 *stack_slots = estack + MAXENV + 1; /* the rest */
_Static_assert((MAXARG + 1 + MAXENV + 1 + MAXARG + MAXENV + 32) * 8 <= 4096,
"exec scratch does not fit in one page");
uint64 max_sz = 0; uint64 max_sz = 0;
uint64 rand_va = 0; uint64 rand_va = 0;
@ -97,6 +116,19 @@ exec(char *path, char **argv)
ustack[argc] = sp; ustack[argc] = sp;
} }
// The environment, copied the same way and for the same reason: these
// strings live in the *old* address space, which is still mapped, and will
// not be once the new pgdir is installed.
for(envc = 0; envp && envp[envc]; envc++) {
if(envc >= MAXENV)
goto bad;
sp -= strlen(envp[envc]) + 1;
sp &= ~7;
if(copyout(pgdir, sp, envp[envc], strlen(envp[envc]) + 1) < 0)
goto bad;
estack[envc] = sp;
}
// Push 16 pseudo-random bytes for AT_RANDOM. // Push 16 pseudo-random bytes for AT_RANDOM.
{ {
uint8 randbuf[16]; uint8 randbuf[16];
@ -117,13 +149,20 @@ exec(char *path, char **argv)
} }
stack_slots[slot++] = 0; // end of argv stack_slots[slot++] = 0; // end of argv
stack_slots[slot++] = 0; // end of envp (no environment) // The environment used to be a single zero here, with the comment "(no
// environment)". Nothing on SchOS had one: not a shell, not a musl program
// that had just called execve with a perfectly good envp, not anything. So
// $TERM was unset, $PATH was whatever each program invented, and a variable
// exported in a shell did not survive the exec that was supposed to carry it.
for(i = 0; i < envc; i++)
stack_slots[slot++] = estack[i];
stack_slots[slot++] = 0; // end of envp
// Auxiliary vector — musl reads these for page size, clock, etc. // Auxiliary vector — musl reads these for page size, clock, etc.
stack_slots[slot++] = 6; // AT_PAGESZ stack_slots[slot++] = 6; // AT_PAGESZ
stack_slots[slot++] = 4096; stack_slots[slot++] = 4096;
stack_slots[slot++] = 17; // AT_CLKTCK stack_slots[slot++] = 17; // AT_CLKTCK
stack_slots[slot++] = 100; stack_slots[slot++] = USER_HZ; // what times() is scaled to, not a literal
stack_slots[slot++] = 23; // AT_SECURE = 0 (not setuid) stack_slots[slot++] = 23; // AT_SECURE = 0 (not setuid)
stack_slots[slot++] = 0; stack_slots[slot++] = 0;
stack_slots[slot++] = 11; // AT_UID stack_slots[slot++] = 11; // AT_UID
@ -232,10 +271,12 @@ exec(char *path, char **argv)
switchuvm(myproc()); switchuvm(myproc());
freevm(oldpgdir); freevm(oldpgdir);
kfree((char*)scratch);
fsstat.exec_cycles += rdtsc() - _t0; fsstat.exec_cycles += rdtsc() - _t0;
return 0; return 0;
bad: bad:
kfree((char*)scratch);
if(pgdir) if(pgdir)
freevm(pgdir); freevm(pgdir);
if(ip){ if(ip){

View file

@ -383,9 +383,20 @@ waitpid(int pid, int *status, int options)
} }
} }
if(!havekids || myproc()->killed){ // Two different failures, and they used to share one bare -1.
//
// musl reads a return in [-4095, -1] as -errno, so -1 arrived in
// userspace as EPERM: "operation not permitted". zsh's compinit calls
// wait, got that, printed "wait failed: operation not permitted", and
// abandoned the rest of /etc/zsh/zshrc -- which looked like a broken
// config file rather than a wait() that could not say what went wrong.
if(!havekids){
release(&ptable.lock); release(&ptable.lock);
return -1; return -ECHILD; // nothing to wait for, which is not an error
}
if(myproc()->killed){
release(&ptable.lock);
return -EINTR; // a signal arrived while waiting
} }
if (options & 1) { // WNOHANG if (options & 1) { // WNOHANG

View file

@ -726,8 +726,8 @@ bad_unlink: iunlockput(dp); end_op(); return -1;
addr_t sys_exec(void) addr_t sys_exec(void)
{ {
char *path, *argv[MAXARG]; char *path, *argv[MAXARG], *envp[MAXENV];
addr_t uargv, uarg; addr_t uargv, uenvp, uarg;
if (argstr(0, &path) < 0 || argaddr(1, &uargv) < 0) return -EFAULT; if (argstr(0, &path) < 0 || argaddr(1, &uargv) < 0) return -EFAULT;
memset(argv, 0, sizeof(argv)); memset(argv, 0, sizeof(argv));
for (int i = 0; ; i++) { for (int i = 0; ; i++) {
@ -736,7 +736,24 @@ addr_t sys_exec(void)
if (!uarg) { argv[i] = 0; break; } if (!uarg) { argv[i] = 0; break; }
if (fetchstr(uarg, &argv[i]) < 0) return -EFAULT; if (fetchstr(uarg, &argv[i]) < 0) return -EFAULT;
} }
return exec(path, argv);
// The third argument. This is Linux's execve(2), so musl has been passing
// an environment here all along -- it was simply never read, and every
// program on the system started with an empty one.
//
// A null envp is legal and means exactly that. It is also what the
// first-party exec() wrapper passes, so old callers keep their old
// behaviour without knowing about this.
memset(envp, 0, sizeof(envp));
if (argaddr(2, &uenvp) == 0 && uenvp) {
for (int i = 0; ; i++) {
if (i >= NELEM(envp)) return -E2BIG;
if (fetchaddr(uenvp + sizeof(addr_t) * i, &uarg) < 0) return -EFAULT;
if (!uarg) { envp[i] = 0; break; }
if (fetchstr(uarg, &envp[i]) < 0) return -EFAULT;
}
}
return exec(path, argv, envp);
} }
// ftruncate/truncate both grow and shrink. Growing used to be quietly dropped // ftruncate/truncate both grow and shrink. Growing used to be quietly dropped

View file

@ -95,7 +95,11 @@ sys_mmap(void)
for (int i = 0; i < NVMA; i++) { for (int i = 0; i < NVMA; i++) {
if (!myproc()->vma[i].used) { v = &myproc()->vma[i]; break; } if (!myproc()->vma[i].used) { v = &myproc()->vma[i]; break; }
} }
if (!v) return -1; // -ENOMEM, not -1. A bare -1 reaches musl as -EPERM, so a process that
// had simply used all NVMA slots was told "operation not permitted" --
// which sends you looking for a permission model this system does not
// have. malloc() turns either into NULL, but only one of them is a clue.
if (!v) return -ENOMEM;
// Ignore non-zero hint addresses (not MAP_FIXED); always pick a fresh VA. // Ignore non-zero hint addresses (not MAP_FIXED); always pick a fresh VA.
// Bump-allocated: each mapping gets its own disjoint, exactly-length-sized // Bump-allocated: each mapping gets its own disjoint, exactly-length-sized

View file

@ -22,3 +22,15 @@ __stack_chk_fail(void)
_xv6_exit(); _xv6_exit();
for (;;); for (;;);
} }
// exec() without an environment.
//
// The syscall is execve, three arguments, because that is what Linux numbers
// 59 and what musl calls. The two-argument spelling stays because most of
// cmd/ has no environment to pass and should not have to say so -- but it
// says so *here*, explicitly, rather than leaving %rdx to chance.
int
exec(char *path, char **argv)
{
return execve(path, argv, 0);
}

View file

@ -22,7 +22,15 @@ SYSCALL(read)
SYSCALL(write) SYSCALL(write)
SYSCALL(close) SYSCALL(close)
SYSCALL(kill) SYSCALL(kill)
SYSCALL(exec) /* The raw stub is execve: three arguments, the Linux shape. exec() is a C
wrapper in glue.c that passes a null environment, so the 2-argument callers
in cmd/ cannot leave %rdx holding whatever happened to be there. */
.global execve
execve:
mov $SYS_exec, %rax
mov %rcx, %r10
syscall
ret
SYSCALL(open) SYSCALL(open)
SYSCALL(mknod) SYSCALL(mknod)
SYSCALL(unlink) SYSCALL(unlink)

View file

@ -6,6 +6,7 @@
# ports/mksh/ MirBSD Korn Shell — installed as sysroot/bin/sh # ports/mksh/ MirBSD Korn Shell — installed as sysroot/bin/sh
# ports/toybox/ core utilities — installed as sysroot/bin/<applet> # ports/toybox/ core utilities — installed as sysroot/bin/<applet>
# ports/ncurses/ terminfo + screen library — link-time only, ships nothing # ports/ncurses/ terminfo + screen library — link-time only, ships nothing
# ports/zsh/ the Z shell — installed as sysroot/bin/zsh, beside /bin/sh
# #
# A port directory holds only a Makefile and optional patches/. Everything # A port directory holds only a Makefile and optional patches/. Everything
# generated lives in build/ports/<name>/ — see mk/port.mk for the contract. # generated lives in build/ports/<name>/ — see mk/port.mk for the contract.
@ -29,6 +30,7 @@ mksh: musl
toybox: musl toybox: musl
fastfetch: musl fastfetch: musl
ncurses: musl ncurses: musl
zsh: musl ncurses
$(PORTS): $(PORTS):
$(MAKE) -C $@ $(MAKE) -C $@

View file

@ -53,8 +53,10 @@ download: $(TARBALL)
$(SRCDIR): $(TARBALL) $(SRCDIR): $(TARBALL)
mkdir -p $(PORTDIR) mkdir -p $(PORTDIR)
rm -rf $@ $(PORTDIR)/fastfetch-$(FF_VERSION)
tar -xzf $< -C $(PORTDIR) tar -xzf $< -C $(PORTDIR)
mv $(PORTDIR)/fastfetch-$(FF_VERSION) $@ mv $(PORTDIR)/fastfetch-$(FF_VERSION) $@
touch $@
extract: $(SRCDIR) extract: $(SRCDIR)

View file

@ -20,8 +20,10 @@ download: $(TARBALL)
$(SRCDIR): $(TARBALL) $(SRCDIR): $(TARBALL)
mkdir -p $(PORTDIR) mkdir -p $(PORTDIR)
rm -rf $@ $(PORTDIR)/mksh
tar -xzf $< -C $(PORTDIR) tar -xzf $< -C $(PORTDIR)
mv $(PORTDIR)/mksh $@ mv $(PORTDIR)/mksh $@
touch $@
# Order-only (|) on $(SRCDIR): the extracted tarball never changes, so a # Order-only (|) on $(SRCDIR): the extracted tarball never changes, so a
# directory mtime should not trigger a rebuild of an already-built shell. # directory mtime should not trigger a rebuild of an already-built shell.

View file

@ -82,8 +82,10 @@ download: $(TARBALL)
$(SRCDIR): $(TARBALL) $(SRCDIR): $(TARBALL)
mkdir -p $(PORTDIR) mkdir -p $(PORTDIR)
rm -rf $@ $(PORTDIR)/ncurses-$(NC_VERSION)
tar -xzf $< -C $(PORTDIR) tar -xzf $< -C $(PORTDIR)
mv $(PORTDIR)/ncurses-$(NC_VERSION) $@ mv $(PORTDIR)/ncurses-$(NC_VERSION) $@
touch $@
extract: $(SRCDIR) extract: $(SRCDIR)
@ -106,7 +108,18 @@ $(PREFIX)/lib/libncursesw.a: $(BUILDDIR)/config.status
$(MAKE) -C $(BUILDDIR) $(MAKE) -C $(BUILDDIR)
$(MAKE) -C $(BUILDDIR) install $(MAKE) -C $(BUILDDIR) install
build install: $(PREFIX)/lib/libncursesw.a build: $(PREFIX)/lib/libncursesw.a
# The acceptance test. It is the one thing this port puts in sysroot/, and it
# earns the space: the build proving the library compiles says nothing about
# whether terminfo works on a SchOS console, which is a different question
# with a different answer. sysroot/bin already carries socktest, nettest and
# shmtest for the same reason.
$(SYSROOT)/bin/tinfotest: tinfotest.c $(PREFIX)/lib/libncursesw.a | $(SYSROOT)/bin
$(HOSTCC_NC) $(NC_CPPFLAGS) -isystem $(PREFIX)/include $(NC_CFLAGS) $(NC_LDFLAGS) -L$(PREFIX)/lib -o $@ $< -lncursesw $(NC_LIBS)
chmod 755 $@
install: $(PREFIX)/lib/libncursesw.a $(SYSROOT)/bin/tinfotest
clean: clean:
rm -rf $(PORTDIR) rm -rf $(PORTDIR)

209
ports/zsh/Makefile Normal file
View file

@ -0,0 +1,209 @@
# 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.
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=no \
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"
$(SYSROOT)/bin/zsh: $(BUILDDIR)/Src/zsh licence-check | $(SYSROOT)/bin
$(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)