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:
parent
b6450d866d
commit
14f0e4d0e2
22 changed files with 684 additions and 24 deletions
12
Makefile
12
Makefile
|
|
@ -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
26
NOTICE
|
|
@ -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
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
14
etc/zsh/zshenv
Normal 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
113
etc/zsh/zshrc
Normal 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=''
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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*);
|
||||||
|
|
|
||||||
|
|
@ -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){
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
12
lib/glue.c
12
lib/glue.c
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
10
lib/usys.S
10
lib/usys.S
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 $@
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
209
ports/zsh/Makefile
Normal 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)
|
||||||
Loading…
Reference in a new issue