diff --git a/CLAUDE.md b/CLAUDE.md index 9a7ed53d4..398c32168 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1206,6 +1206,32 @@ silent: (NTS)`, and `php-smoke.php` is **44 of 45** on the VM -- PHP_INT_SIZE 8, strlen, max, str_repeat at 100k, preg, json, and `/net/tcp/stats` read as 404 bytes. Notes in `docs/notes/php.md`.** +***AND TWELVE EXTENSIONS ARE BUILT NOW***: the four Phoronix REQUIRES +(libxml, dom, simplexml, zlib -- `system-info` runs) plus eight of the +nine it calls OPTIONAL -- posix, pcntl, bz2, readline, openssl +(LibreSSL), curl, sqlite3, zip. **579 TUs compile and `--undefined` +resolves 1120 symbols** against libap and eight library trees, all of +which come in by `#pragma lib` from their own headers, so `cmd/php` +still has no `LIB=` line. *GD is impossible* (no libgd here; libpng is +not a drawing library) and **`ext/sockets` is deferred to a libap +change**: no `struct cmsghdr`, no `CMSG_*`, no `msg_flags`, `sendmsg` +a stub and **`recvmsg` declared-by-the-header-and-defined-nowhere**. +***THE MEASUREMENT THAT NEARLY SHIPPED WAS AN EMPTY TRANSLATION +UNIT***: five of these wrap their whole body in `#ifdef HAVE_`, so +with the name unanswered they compile to nothing and report 0 errors +-- and *counting `TEXT` symbols does not separate them either*, +because kencc emits php's header `static inline`s that gcc at -O2 +discards, putting the floor at ~360. **The discriminator is whether +the assembly mentions `_module_entry`**: 0 against 10-14, one +grep. Three findings, all upstream's: LibreSSL has no +`sk_X509_new_reserve` (a capacity hint, so `sk_X509_new_null()`); +readline reaches its CLI callbacks through `dlsym`, where APE's +`` would have answered NULL and installed nothing *silently*, +and upstream's own commented-out direct call is the fix; and +`return ;` in a `void` function, which gcc accepts and C11 +6.8.6.4p1 forbids -- fixed in php, **deviation recorded in 0.6 and not +taken**, since `ORETURN` with a left operand means "compute into the +return location" and accepting void is a statement-tree rewrite. ***AND THE SECOND ONE WAS EVERY MULTIPLICATION.*** `zend_multiply.h:135`'s fallback arm -- the one this port takes -- declared the product `long`, which is 64 bits on LP64 and **32 diff --git a/docs/notes/instruments.md b/docs/notes/instruments.md index e07b4a725..3308d7e21 100644 --- a/docs/notes/instruments.md +++ b/docs/notes/instruments.md @@ -1152,9 +1152,34 @@ broken compiler. ## `php-mirror.sh` — php's whole build, compiled on the host `sys/lib/tests/hostkencc/php-mirror.sh`. `--flags` prints the compile -line, `--compile` builds all 514 translation units and **gates**. +line, `--compile` builds all 579 translation units and **gates**. Depends on `hostkencc.sh` having run. +***ITS `--undefined` MODE SAVED EMPTY DIAGNOSTICS FOR A YEAR OF ITS +SHORT LIFE, AND THE REASON IS THE ONE `hostkencc.sh --probe` ALREADY +CARRIES: kencc's `diag()` WRITES TO STDOUT.*** Under `-S` stdout is +where the assembly goes, so a failing unit's errors land at the END OF +THE .s FILE and the `2>` capture holds nothing but cpp's `#warning` +line. A real finding -- LibreSSL's missing `sk_X509_new_reserve` -- +read as **exit 1 with no message**, which is indistinguishable from a +compiler crash and was nearly filed as one. The arm now recompiles a +failing unit to an OBJECT, where stdout is the diagnostics and nothing +else. *The same trap, in the same directory, found twice by two +different people-shaped mistakes.* + +**`--undefined` needs a `--root` for every library an extension +links**, and this is not optional bookkeeping: with only libap known, +241 of 539 names came back `absent` and the real findings were +invisible among them. Nine roots now. **Its `absent` column has three +standing caveats and the script prints all of them**: DATA symbols +(`stdin`, `environ`, `h_errno`, `xmlParserVersion`, ...), readline's 16 +`rl_*` **variables**, and the 10 `BZ2_*`, which are *functions that +are present* -- `bzlib.c` spells every definition +`int BZ_API(BZ2_bzopen)`, so **the name is inside a macro** and no +regex over definition sites can see it. *A sweep that reads source +text cannot see through the preprocessor, and the failure mode is a +confident `absent` naming a function that is right there.* + **What it came from**: eight consecutive rounds, each costing a VM rebuild to learn about one compile error, because the error in the next file is invisible until the first is fixed. Its first run returned all diff --git a/docs/notes/php.md b/docs/notes/php.md index 6b301ab12..9db0a222f 100644 --- a/docs/notes/php.md +++ b/docs/notes/php.md @@ -1220,3 +1220,160 @@ cannot be found by reading the subject.* by `apfunc-sweep.py` while answering php's `config.h`. It is the composition of `localtime_r` and `asctime_r`, both of which were already there, and is now in `time/ctime.c`. + +## The eight optional extensions, and the check that could only pass + +Phoronix's `system-info` lists nine OPTIONAL extensions once the four +required ones are in: OpenSSL, GD, Bzip2, SQLite3, PCNTL, POSIX, CURL, +Sockets, Readline. **Eight are built. GD is impossible and Sockets is +deferred**, and the two reasons are different in kind. + +Eight of the nine already had a library in this tree — `lib/bz2`, +`lib/readline`+`lib/history`, `lib/libressl`, `lib/curl`, `lib/sqlite`, +`lib/zip` — and POSIX and PCNTL need none. **GD needs libgd, which is +not here**; `lib/png` is, and a PNG codec is not a drawing library, so +this is an absent dependency rather than a port. + +### The first measurement was worthless, twice over + +Running the mirror over the candidate files reported **posix 1/1, +sqlite3 1/1, zip 2/2, curl 4/4 — clean**. Four of those were real and +*five other extensions reported clean while compiling to nothing at +all*: every one of these extensions wraps its whole body in +`#ifdef HAVE_`, and none of the `HAVE_` names was in the +`config.h`. `ext/posix/posix.c` with `HAVE_POSIX` unanswered is an +empty translation unit, and an empty translation unit compiles. + +***A check that cannot fail is not a check***, and this file already +carried that rule for `config.h` answers. What makes it worth writing +down again is that the obvious repair **also failed**: counting `TEXT` +symbols in `6c -S` output gave 363, 368, 397, 452, 470, 486, 505 — for +empty and non-empty units alike — because kencc emits the `static +inline` functions out of php's headers that gcc at `-O2` discards. *The +floor is about 360 and the signal is underneath it.* + +**The discriminator that works is one `grep`**: does the assembly +mention `_module_entry`. 0 for the five empty ones, 10-14 for the +real ones, in a single command. *The question was never "did it +compile" but "did it compile the extension".* + +### Three findings, and all three were somebody else's + +**LibreSSL has no `*_new_reserve` family at all.** +`openssl_backend_common.c:714` is +`sk_X509_new_reserve(NULL, sk_X509_INFO_num(sk))`, and neither that nor +the `OPENSSL_sk_new_reserve` underneath it exists in LibreSSL 4.3.2. +The call is a **capacity hint and nothing else** — the stack it makes +is filled by `sk_X509_push` in the loop immediately below, and push +grows on demand — so `#define sk_X509_new_reserve(cmp, n) +sk_X509_new_null()` is the same program with one more realloc in it. +It is in `php_openssl_backend.h` under `#if +defined(LIBRESSL_VERSION_NUMBER)`, which is **upstream's own +convention**: php already carries five such guards, one of them in +that same file. *All six openssl files compile after it.* + +**`ext/readline` reaches `php_cli_get_shell_callbacks` through +`dlsym`,** and upstream's own commented-out alternative three lines +above is the direct call. Upstream's comment says why the dynamic +lookup is there — even the CGI SAPI links against "CLI"-only +extensions, so the symbol may be absent at run time — and *neither half +of that applies to a build with no dynamic linking*: the extension is +static, `sapi/cli/php_cli.c:111` is in the same binary, and +`sapi/cli/cli.h` is already included. + +***The alternative was not "leave it alone".*** APE ships a +`` whose `dlsym` is `return NULL`, so including it compiles, +links, and then installs no callbacks at all — `php -a` without +readline and nothing printed to say so. **A stub that answers "nothing +found" is the trap, not the fix**, which is this tree's oldest rule +arriving through a header nobody added for php. Keyed on +`!defined(HAVE_DLFCN_H)` rather than on Plan 9, so any platform with +dynamic linking keeps upstream's arm untouched. + +**`return ;` in a `void` function.** +`ext/pcntl/pcntl.c:1495` is `return pcntl_signal_dispatch();`, and +kencc answers `incompatible types: "VOID" and "VOID" for op "RETURN"`. +***Both operands being `VOID` is the tell that this is a rule and not +a type mismatch***: C11 6.8.6.4p1 is a constraint and kencc is right, +while gcc and clang accept it silently without `-pedantic`. Fixed as +two statements in php; **the compiler deviation is recorded in 0.6's +inventory and not taken**, with the reason — `ORETURN` with a non-`Z` +left means "compute the value into the return location", so accepting +a void operand is a statement-tree rewrite, not a relaxed check. + +### What the answers were derived from + +Every function question went to `apfunc-sweep.py` with the library's +own tree as a `--root`; every macro and struct question was a compile +against the real APE headers. The results that mattered: + +* libap has **26 of the 29** functions `ext/posix` asks about. Only + `eaccess`, `getsid` and `ttyname_r` are absent. +* `ext/pcntl` gets `fork`, `sigaction`, `sigprocmask`, `sigwaitinfo`, + `sigtimedwait`, `wait3` and `wait4`; not `waitid`, `getpriority`, + `setpriority`, `sched_getcpu`, `unshare`, `forkx` or `getcpuid`. +* **`WIFCONTINUED` exists and `WCONTINUED` does not.** That looks like + a typo and is not — they are separate `#ifdef`s in `pcntl.c` and a + compile answers them separately. +* LibreSSL has neither `RAND_egd` nor `OSSL_set_max_threads`, so no EGD + and no argon2. **The TLS feature macros are not answered in the + `config.h` and must not be**: `xp_ssl.c:70-146` computes + `HAVE_TLS13`, `HAVE_SEC_LEVEL` and the rest from the version macros + itself. +* libzip 1.11.4 has all six functions php version-checks for; + sqlite 3.53.4 has both. **`SQLITE_OMIT_LOAD_EXTENSION` is the Plan 9 + answer and not a preference** — `sqlite3_load_extension` is dlopen, + so the alternative is a link error rather than a missing feature. +* `rl_erase_empty_line` is a **variable**, so the sweep cannot see it; + `readline.c:250` defines it and `readline.h:648` declares it. + +**All six libraries come in by `#pragma lib` from their own headers**, +checked rather than asserted: ``, ``, +``, ``, ``, `` and +the `` set each carry one. `zip.h` carries **six**, which +is libzip's compression backends and its crypto. So there is still no +`LIB=` line in `cmd/php/mkfile`. *`history.h` naming `libreadline.a` +is right rather than a slip*: `lib/readline/mkfile` puts `$HISTOBJ` +into `libreadline.a` as well. + +### The state + +**579 translation units compile, 0 errors**, and the link wants **1166** +symbols from outside php, of which `--undefined` resolves **1120** +against libap and the eight library trees. The remaining 46 — 40 +`absent`, 6 `declared-only` — are all data, assembly or macro-hidden, +and each was opened by hand: the nine known DATA symbols +(`stdin`/`stdout`/`stderr`, `environ`, `h_errno`, `_ctype`, +`_errnoloc`, `xmlParserVersion`, `xmlStringText`), `getcontext`/ +`swapcontext`/`signal`/`xmlMalloc`/`zError`/`zlibVersion` as before, +the 16 readline **variables**, and **the 15 `BZ2_*`, which are +functions and are present** — `bzlib.c` spells every definition +`int BZ_API(BZ2_bzopen)`, so the name is inside a macro and no regex +over definition sites can match it. That caveat is +now printed by the mirror itself, with the list, so the next reader +compares against it instead of re-deriving it. + +### ext/sockets, and why it is the one deferred + +It is not a `config.h` answer. `` has **no `struct +cmsghdr`**, no `CMSG_DATA`/`CMSG_LEN`/`CMSG_SPACE`, no +`MSG_TRUNC`/`MSG_CTRUNC`, and `struct msghdr` has no `msg_flags`; +`multicast.c` additionally `#error`s unless `HAVE_IF_NAMETOINDEX` is +set, which is a free answer since libap has the function. Underneath +that, **`sendmsg` is a stub returning ENOSYS and `recvmsg` is declared +by the header and defined nowhere** — `apfunc-sweep` reports it +`declared`, the middle state that link-fails far from the claim. + +What it would take: the `CMSG_*` macros and `struct cmsghdr` are +boilerplate pointer arithmetic; `msg_flags` is a **public struct +change**, so it needs `mk distclean` and an entry beside `-J` and the +64-bit `ino_t`; and `sendmsg`/`recvmsg` can be real for the +scatter/gather case — iterate the iovec over the socket fd — with +ancillary data the only part that genuinely cannot work, since Plan 9 +has no `SCM_RIGHTS`. **That last part is the one to get right**: an +`fd`-passing call must fail loudly rather than silently drop the +control message. + +Phoronix wants sockets only "when running the Phoromatic Server", so +it is the least valuable of the nine and the only one needing a public +struct to move. *Deferred on cost, not on possibility.* diff --git a/docs/releases/0.6.md b/docs/releases/0.6.md index c165e50f8..27c7c9992 100644 --- a/docs/releases/0.6.md +++ b/docs/releases/0.6.md @@ -1181,6 +1181,33 @@ distinction is the point of the list. ### Compiler +- **`return ;` in a `void` function is REJECTED, and + gcc and clang accept it.** `com.c:224`'s `ORETURN` case runs + `tcompat(n, n->type, l->type, tasign)` whatever the types are, so a + void operand returned from a void function answers + + pcntl.c:1495 incompatible types: "VOID" and "VOID" for op "RETURN" + + ***Both operands being `VOID` is the tell***: this is a rule, not a + type mismatch. **kencc is strictly right** — C11 6.8.6.4p1 is a + constraint, "A return statement with an expression shall not appear + in a function whose return type is void" — but gcc and clang accept + it silently without `-pedantic`, so the construct is common in real + C and this is a portability deviation rather than a correctness one. + **MEASURED**: php 8.5.12's `ext/pcntl/pcntl.c:1495`, which is + `return pcntl_signal_dispatch();` in a `static void` function; + fixed in php as two statements, which is the same program. + **Why it is recorded and not taken**: accepting it can only widen + what compiles — there is no ABI consequence and no codegen change + for anything that already builds — but the fix is not a predicate + edit. `ORETURN` with a non-`Z` left means "compute the value into + the return location", so a void operand has to be restructured into + "evaluate the expression, then return", which is a statement-tree + rewrite inside `tcom`'s expression walk rather than a relaxed check. + *One line of php against a structural change in the shared front + end, on the last day of a port* — the php side was taken for that + reason, and the construct will recur. + - **`mkvar` keys an optimizer variable on its OFFSET, so a type pun deletes the store -- FIXED IN `6c` ONLY, LIVE IN THE OTHER NINE.** `[1-9]c/reg.c`'s `mkvar` keys on (sym, name, **offset**), so @@ -1362,6 +1389,32 @@ distinction is the point of the list. ### libap +- **`recvmsg` is declared by `` and defined NOWHERE, and + `sendmsg` is a stub that returns ENOSYS.** `apfunc-sweep.py` reports + `recvmsg` as `declared` — the middle state the sweep exists for, and + the one that link-fails a long way from the claim, since a + `config.h` or a header can say "yes" to it right up until `6l` runs. + `network/sendmsg.c` is fourteen lines and its own comment says Plan + 9 "does not natively support the complex POSIX sendmsg/recvmsg + interface based on msghdr". + **The header is missing the rest of the interface too**: no `struct + cmsghdr`, no `CMSG_DATA`/`CMSG_LEN`/`CMSG_SPACE`/`CMSG_FIRSTHDR`/ + `CMSG_NXTHDR`, no `MSG_TRUNC`/`MSG_CTRUNC`, and `struct msghdr` has + no `msg_flags`. + **MEASURED** by php 8.5.12's `ext/sockets`, which is the reason the + one extension of nine that Phoronix asks for is not built: three of + its five files fail to compile and the other two would fail to link. + **What it would take, and why it is not free**: the `CMSG_*` macros + and `struct cmsghdr` are boilerplate pointer arithmetic, and + `sendmsg`/`recvmsg` can be *real* for the scatter-gather case by + iterating the iovec over the socket fd — but `msg_flags` is a + **public struct change**, so it joins `-J` and the 64-bit + `ino_t`/`dev_t` in needing `mk distclean`, and **ancillary data + genuinely cannot work**: Plan 9 has no `SCM_RIGHTS`, so an + fd-passing call must fail loudly rather than silently drop the + control message. *That last part is the whole risk* — this list's + most common bug is a stub that answers the wrong kind of nothing. + - **Every architecture's `fenv.s` is inert, so the fenv API is a stub on all of them** — **measured**, and the most consequential thing the 386 cross build has turned up. `as` rendered musl's `.globl name` and diff --git a/sys/lib/tests/hostkencc/php-mirror.sh b/sys/lib/tests/hostkencc/php-mirror.sh index ef0c03500..13b2176bc 100755 --- a/sys/lib/tests/hostkencc/php-mirror.sh +++ b/sys/lib/tests/hostkencc/php-mirror.sh @@ -226,13 +226,36 @@ for s in $list; do # `-S' rather than `-o': the assembly carries the symbol # table this mode needs, and nothing else does without a # reader for Plan 9 object files. + # + # ***BUT kencc's `diag()' WRITES TO STDOUT***, which under + # `-S' is where the assembly goes -- so a failing unit's + # diagnostics land at the END OF THE .s FILE and `t.log' + # comes back holding nothing but cpp's `#warning' line. + # The first version of this arm saved exactly that, and a + # real error (`sk_X509_new_reserve' undeclared, from + # LibreSSL) read as *exit 1 with no message*. **The same + # trap `hostkencc.sh --probe' already carries**, where ten + # backends "passed" because `diag' is not on stderr. + # + # So on failure, recompile to an OBJECT -- where stdout is + # the diagnostics and nothing else -- and report that. if $CC6 $extra $CFL -S "$src" \ > "$OUT/asm/$(echo "$s" | tr / _).s" 2> "$OUT/t.log"; then ok=$((ok+1)) else bad=$((bad+1)) echo "==== $s" >> "$OUT/errors" - cat "$OUT/t.log" >> "$OUT/errors" + # `|| true' on BOTH: this file runs under `set -e', + # the recompile is EXPECTED to fail, and a bare + # failing command kills the script -- which it did, + # silently, so the run printed nothing at all and + # exited 1. *The positive control caught it; the + # first version of this arm was never exercised + # because nothing had failed since it was written.* + $CC6 $extra $CFL -o "$OUT/t.6" "$src" \ + > "$OUT/t.log" 2>&1 || true + grep -v '#warning directive' "$OUT/t.log" \ + >> "$OUT/errors" || true fi continue fi @@ -278,21 +301,38 @@ EOF n=$(wc -l < "$OUT/undefined") echo "php-mirror: $n symbol(s) the link must find outside php" echo "php-mirror: the list is in $OUT/undefined; asking libap --" - # ***THREE LIBRARIES NOW, NOT ONE.*** php pulls libxml2 and libz - # through `#pragma lib' in their headers, so a sweep that knows - # only libap answers `absent' for every `xmlParseFile' and - # `deflateInit_' -- 241 of the 539 the first run with the dom, - # simplexml and zlib extensions reported. `--root' adds their - # vendored sources; add one here for every library a new - # extension links. + # ***NINE LIBRARIES NOW, NOT ONE.*** php pulls each of them + # through `#pragma lib' in its own header, so a sweep that knows + # only libap answers `absent' for every `xmlParseFile', + # `deflateInit_', `SSL_new' and `curl_easy_init' -- 241 of the + # 539 the first run with the dom, simplexml and zlib extensions + # reported. `--root' adds their vendored sources; **add one here + # for every library a new extension links**, or the sweep's + # `absent' column fills with names that are present and the real + # findings are lost in it. python3 "$ROOT/sys/lib/tests/apfunc-sweep.py" \ --root sys/src/external/libxml2 \ --root sys/src/external/zlib \ + --root sys/src/external/libressl \ + --root sys/src/external/curl \ + --root sys/src/external/sqlite \ + --root sys/src/external/libzip \ + --root sys/src/external/readline \ + --root sys/src/external/bzip2 \ --file "$OUT/undefined" | grep -v ' *defined ' || true echo "php-mirror: ^ anything here is READ, not a failure -- the" echo "php-mirror: sweep sees C function definitions only, so" echo "php-mirror: assembly reads as 'declared' and DATA as 'absent'." + echo "php-mirror: The known-innocent 'absent' names, all checked" + echo "php-mirror: by hand: stdin/stdout/stderr, environ, h_errno," + echo "php-mirror: _ctype, _errnoloc, xmlParserVersion," + echo "php-mirror: xmlStringText (DATA); the 16 rl_* (readline" + echo "php-mirror: VARIABLES); and the 15 BZ2_* -- bzlib.c spells" + echo "php-mirror: every definition 'int BZ_API(BZ2_bzopen)', so" + echo "php-mirror: the NAME IS INSIDE A MACRO and no regex over" + echo "php-mirror: definition sites can see it. **A name here" + echo "php-mirror: that is not on that list is the finding.**" exit 0 fi echo "php-mirror: compiled $ok, errors $bad, missing $missing" diff --git a/sys/lib/tests/php-smoke.php b/sys/lib/tests/php-smoke.php index 2cc6042a7..e7f8676f0 100644 --- a/sys/lib/tests/php-smoke.php +++ b/sys/lib/tests/php-smoke.php @@ -51,10 +51,25 @@ $fails = 0; +/* + * Long values are ELIDED IN THE MIDDLE, not truncated at the end. + * Section 8 round-trips a few thousand bytes through libbz2 and + * libz, and printing those in full buries every other line in the + * section -- but *a value cut at the front only is the one that + * hides a difference in the tail*, which is exactly where a + * compression round trip goes wrong. Both ends are kept. + */ +function brief($v) { + $s = var_export($v, true); + if (strlen($s) <= 72) return $s; + return substr($s, 0, 36) . " ...[" . strlen($s) . " chars]... " + . substr($s, -24); +} + function eq($what, $got, $want) { global $fails; - $g = var_export($got, true); - $w = var_export($want, true); + $g = brief($got); + $w = brief($want); if ($got === $want) { printf(" PASS %-44s %s\n", $what, $g); } else { @@ -255,5 +270,151 @@ function eq($what, $got, $want) { eq("PHP_OS is a string", is_string(PHP_OS), true); echo " INFO PHP_OS = " . PHP_OS . ", PHP_VERSION = " . PHP_VERSION . "\n"; +/* ---------------------------------------------------------------- */ +echo "\nSection 8: the twelve extensions, and each one DOING something\n"; +/* + * ***LOADED IS NOT WORKING, AND THAT IS THE WHOLE POINT OF THIS + * SECTION.*** `extension_loaded()' asks whether a module entry was + * registered, which is a fact about `internal_functions_cli.c' and + * not about the library underneath it. Every one of these + * extensions was first seen "compiling clean" while its body was + * `#ifdef'd out to nothing -- so a check that only counts modules + * would have passed on that build too. + * + * So each line below calls through to the LIBRARY: bz2 round-trips + * bytes through libbz2, zip and sqlite3 and curl and openssl each + * ask their own library its version or do one real operation. + */ +$want = ['core', 'date', 'pcre', 'hash', 'json', 'random', 'Reflection', + 'SPL', 'standard', 'libxml', 'dom', 'SimpleXML', 'zlib', + 'posix', 'pcntl', 'bz2', 'readline', 'openssl', 'curl', + 'sqlite3', 'zip']; +$missing = []; +foreach ($want as $e) { + if (!extension_loaded($e)) $missing[] = $e; +} +echo " INFO loaded: " . implode(', ', get_loaded_extensions()) . "\n"; +/* + * ***ASSERTED ON Plan9 ONLY, and this is Section 7's rule rather than + * a softening.*** The list above is a statement about THIS PORT's + * `internal_functions_cli.c', so a build host that happens not to + * ship `ext/readline' would fail it while being perfectly correct -- + * and *a test that fails on the reference cannot tell you whether the + * test or the tree is wrong*, which is the whole reason this file is + * run against host php first. + * + * `PHP_OS' is `"Plan9"' here, from `php_config.h:594'. **Section 7 + * prints PHP_OS**, so a build whose PHP_OS changed shows up as this + * check silently not running rather than as a mystery pass -- which + * is the failure mode to watch for, not the one being accepted. + */ +if (PHP_OS === 'Plan9') { + eq("every expected extension is registered", $missing, []); +} elseif ($missing) { + echo " INFO not on Plan9; absent here: " . implode(', ', $missing) . "\n"; +} + +/* bz2 -- a real round trip through libbz2, not a version string. */ +if (extension_loaded('bz2')) { + $plain = str_repeat("APExp bzip2 round trip. ", 200); + $packed = bzcompress($plain, 9); + eq("bzcompress returns a string", is_string($packed), true); + eq("bzcompress actually compresses", strlen($packed) < strlen($plain), true); + eq("bzdecompress round trip", bzdecompress($packed), $plain); +} + +/* zlib -- the same question, since ext/zlib is what answers "MISSING ZIP". */ +if (extension_loaded('zlib')) { + $plain = str_repeat("APExp zlib round trip. ", 200); + eq("gzdeflate/gzinflate round trip", gzinflate(gzdeflate($plain, 9)), $plain); +} + +/* dom + libxml -- parse and read a value back out. */ +if (extension_loaded('dom')) { + $d = new DOMDocument(); + $d->loadXML('hello'); + eq("DOM parses and reads text", $d->getElementsByTagName('a')->item(0)->textContent, "hello"); + eq("DOM reads an attribute", $d->getElementsByTagName('a')->item(0)->getAttribute('id'), "x"); +} +if (extension_loaded('SimpleXML')) { + $x = simplexml_load_string('7'); + eq("SimpleXML reads an element", (string)$x->a, "7"); +} + +/* sqlite3 -- create, insert, select, in memory, so no file is left. */ +if (extension_loaded('sqlite3')) { + echo " INFO SQLite3 library version: " . SQLite3::version()['versionString'] . "\n"; + $db = new SQLite3(':memory:'); + $db->exec('CREATE TABLE t (a INTEGER, b TEXT)'); + $db->exec("INSERT INTO t VALUES (42, 'forty-two')"); + $row = $db->querySingle('SELECT b FROM t WHERE a = 42'); + eq("sqlite3 round trip", $row, "forty-two"); + $db->close(); +} + +/* openssl -- a digest and a random, both computed by libressl. */ +if (extension_loaded('openssl')) { + echo " INFO OpenSSL: " . OPENSSL_VERSION_TEXT . "\n"; + eq("openssl_digest sha256 of the empty string", + openssl_digest("", "sha256"), + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + eq("openssl_random_pseudo_bytes length", + strlen(openssl_random_pseudo_bytes(16)), 16); + eq("a cipher list is non-empty", count(openssl_get_cipher_methods()) > 0, true); +} + +/* curl -- built, and able to report its own library's version. */ +if (extension_loaded('curl')) { + $v = curl_version(); + echo " INFO libcurl " . $v['version'] . ", ssl " . ($v['ssl_version'] ?? 'none') . "\n"; + eq("curl_init returns a handle", curl_init() !== false, true); +} + +/* zip -- write an archive to a temp file, read it back, delete it. */ +if (extension_loaded('zip')) { + echo " INFO libzip " . (function_exists('zip_libzip_version') ? zip_libzip_version() : ZipArchive::LIBZIP_VERSION) . "\n"; + $tmp = tempnam(sys_get_temp_dir(), 'apexpzip'); + $z = new ZipArchive(); + eq("ZipArchive::open for writing", $z->open($tmp, ZipArchive::OVERWRITE) === true, true); + $z->addFromString('hello.txt', 'zip round trip'); + $z->close(); + $z2 = new ZipArchive(); + eq("ZipArchive::open for reading", $z2->open($tmp) === true, true); + eq("zip round trip", $z2->getFromName('hello.txt'), "zip round trip"); + $z2->close(); + @unlink($tmp); +} + +/* posix -- ids that must agree with the ones standard php reports. */ +if (extension_loaded('posix')) { + eq("posix_getpid agrees with getmypid", posix_getpid(), getmypid()); + eq("posix_getuid returns an int", is_int(posix_getuid()), true); + $u = posix_getpwuid(posix_getuid()); + eq("posix_getpwuid returns a name", is_array($u) && isset($u['name']), true); + if (is_array($u)) echo " INFO posix user: " . $u['name'] . "\n"; + eq("posix_uname returns a sysname", is_array(posix_uname()), true); +} + +/* pcntl -- the parts that need no fork, so the test cannot hang. */ +if (extension_loaded('pcntl')) { + /* + * ***NO fork() HERE, DELIBERATELY.*** A forked child that does + * not exit turns this file into the one test in the tree that + * can hang, and the convention is that such a case gets its own + * file and its own SIGALRM. What is asked here is only that + * the extension registered and its signal table is readable. + */ + eq("pcntl_signal_dispatch runs", (function () { pcntl_signal_dispatch(); return true; })(), true); + eq("pcntl_strerror is a string", is_string(pcntl_strerror(1)), true); +} + +/* readline -- registered, and its library version readable. */ +if (extension_loaded('readline')) { + $i = readline_info(); + eq("readline_info returns an array", is_array($i), true); + if (is_array($i) && isset($i['library_version'])) + echo " INFO readline " . $i['library_version'] . "\n"; +} + printf("\nphp-smoke: %d failures\n", $fails); exit($fails); diff --git a/sys/src/ape/cmd/php/cfg/main/php_config.h b/sys/src/ape/cmd/php/cfg/main/php_config.h index 122463d17..80d5d7c57 100644 --- a/sys/src/ape/cmd/php/cfg/main/php_config.h +++ b/sys/src/ape/cmd/php/cfg/main/php_config.h @@ -376,6 +376,179 @@ #define HAVE_DOM 1 #define HAVE_SIMPLEXML 1 #define HAVE_ZLIB 1 + +/* + * ***THE EIGHT OPTIONAL EXTENSIONS, AND WHY EVERY ONE OF THESE LINES + * HAD TO BE WRITTEN BEFORE ANYTHING COULD BE MEASURED.*** + * + * Phoronix's `system-info' lists nine OPTIONAL extensions once the + * four required ones are in. Eight of them have a library in this + * tree already -- `lib/bz2', `lib/readline'+`lib/history', + * `lib/libressl', `lib/curl', `lib/sqlite', `lib/zip' -- and two need + * no library at all. **GD is the one that is impossible**: there is + * no libgd here, only libpng, and libpng is not a drawing library. + * + * ***FIVE OF THE EIGHT "COMPILED CLEAN" BEFORE A SINGLE LINE BELOW + * EXISTED, AND THAT WAS AN EMPTY TRANSLATION UNIT EACH TIME.*** Every + * one of these extensions wraps its whole body in `#ifdef HAVE_', + * so the mirror compiled `ext/posix/posix.c', `ext/bz2/bz2.c', + * `ext/sockets/sockets.c', `ext/openssl/openssl.c' and + * `ext/readline/readline.c' to *nothing at all* and reported 0 errors + * for each. *A check that cannot fail is not a check*, and this one + * could only pass. **The discriminator that works is the module + * entry**: `6c -S' on the extension's main file either mentions + * `_module_entry' or it does not, and that told the five empty + * ones from the four real ones in a single command. + * ***AND THE FIRST DISCRIMINATOR TRIED WAS NO BETTER***: counting + * `TEXT' symbols gave 363-505 for everything, empty units included, + * because kencc emits the `static inline's out of php's headers that + * gcc at -O2 discards -- so the floor is ~360 and the signal is lost + * in it. + * + * Every function answer below is `apfunc-sweep.py' with the library's + * own tree as a `--root', not recall; every macro and struct answer is + * a compile against the real APE headers. The two that are neither -- + * `HAVE_ERASE_EMPTY_LINE' and `HAVE_STRUCT_SIGINFO_T' -- say so. + */ + +/* ---- ext/posix. `getsid' and `eaccess' are the only two absent. -- */ +#define HAVE_POSIX 1 +#define HAVE_CTERMID 1 +#define HAVE_FPATHCONF 1 +#define HAVE_PATHCONF 1 +#define HAVE_GETGROUPS 1 +#define HAVE_GETLOGIN 1 +#define HAVE_GETPGID 1 +#define HAVE_GETRLIMIT 1 +#define HAVE_INITGROUPS 1 +#define HAVE_MKFIFO 1 +#define HAVE_MKNOD 1 +#define HAVE_SETEGID 1 +#define HAVE_SETEUID 1 +#define HAVE_SETRLIMIT 1 +#define HAVE_SETSID 1 +/* `makedev' comes from musl's ; compiled, not grepped. */ +#define HAVE_MAKEDEV 1 +/* #undef HAVE_EACCESS */ +/* #undef HAVE_GETSID */ +/* #undef HAVE_TTYNAME_R */ + +/* + * ---- ext/pcntl. `fork', `sigaction', `sigprocmask', `sigwaitinfo', + * `sigtimedwait', `wait3' and `wait4' are all in libap; `waitid', + * `getpriority', `setpriority', `sched_getcpu', `unshare', `forkx' and + * `getcpuid' are not. + * + * **`WIFCONTINUED' exists and `WCONTINUED' does not**, which looks + * like a typo and is not: they are separate `#ifdef's in `pcntl.c' and + * a compile against `' answers them separately. + * `HAVE_STRUCT_SIGINFO_T' is a TYPE question, so it is a compile of + * `siginfo_t si;' rather than anything the sweep can see. + */ +#define HAVE_SIGWAITINFO 1 +#define HAVE_STRUCT_SIGINFO_T 1 +#define HAVE_WAIT3 1 +#define HAVE_WAIT4 1 +#define HAVE_WIFCONTINUED 1 +/* #undef HAVE_WCONTINUED */ +/* #undef HAVE_DECL_WCONTINUED */ +/* #undef HAVE_WAITID */ +/* #undef HAVE_WAIT6 */ +/* #undef HAVE_GETPRIORITY */ +/* #undef HAVE_SETPRIORITY */ +/* #undef HAVE_SCHED_GETCPU */ +/* #undef HAVE_SCHED_SETAFFINITY */ +/* #undef HAVE_UNSHARE */ +/* #undef HAVE_FORKX */ +/* #undef HAVE_GETCPUID */ +/* #undef HAVE_RFORK */ +/* #undef HAVE_PTHREAD_SET_QOS_CLASS_SELF_NP */ +/* #undef HAVE_LINUX_RAW_SYSCALL_WAITID */ +/* #undef HAVE_LINUX_RAW_SYSCALL_PIDFD_OPEN */ + +/* ---- ext/bz2, against `lib/bz2'. ---------------------------------- */ +#define HAVE_BZ2 1 + +/* + * ---- ext/readline, against `lib/readline' (GNU readline 8.3) and + * `lib/history'. **Not libedit**, which this tree also has: the two + * are mutually exclusive in `readline.c' and GNU readline is the one + * built. `rl_erase_empty_line' is a VARIABLE, so the sweep cannot see + * it -- `readline.c:250' defines it and `readline.h:648' declares it. + */ +#define HAVE_LIBREADLINE 1 +#define HAVE_HISTORY_LIST 1 +#define HAVE_RL_CALLBACK_READ_CHAR 1 +#define HAVE_RL_ON_NEW_LINE 1 +#define HAVE_RL_COMPLETION_MATCHES 1 +#define HAVE_ERASE_EMPTY_LINE 1 +/* #undef HAVE_LIBEDIT */ + +/* + * ---- ext/openssl, against `lib/libressl' (LibreSSL 4.3.2). + * + * **LibreSSL announces `OPENSSL_VERSION_NUMBER 0x20000000L'** + * (`opensslv.h:13'), so php takes its *v1* backend; all six files + * compile. The TLS feature macros -- `HAVE_TLS13', `HAVE_SEC_LEVEL', + * `HAVE_EVP_PKEY_EC' and the rest -- are NOT answered here and must + * not be: `xp_ssl.c:70-146' and `php_openssl_backend.h:136' compute + * them from the version macros themselves. + * + * `RAND_egd' and `OSSL_set_max_threads' are both absent from LibreSSL, + * which is the sweep's answer and is also what the two `#undef's below + * mean: no EGD, and no argon2 (that one wants OpenSSL 3.2). + */ +#define HAVE_OPENSSL_EXT 1 +/* #undef HAVE_RAND_EGD */ +/* #undef HAVE_OPENSSL_ARGON2 */ +/* #undef USE_OPENSSL_SYSTEM_CIPHERS */ +/* #undef LOAD_OPENSSL_LEGACY_PROVIDER */ + +/* ---- ext/curl, against `lib/curl'. -------------------------------- */ +#define HAVE_CURL 1 + +/* + * ---- ext/sqlite3, against `lib/sqlite' (3.53.4). + * + * **`SQLITE_OMIT_LOAD_EXTENSION' is the Plan 9 answer and not a + * preference**: `sqlite3_load_extension' is dlopen, which does not + * exist here, so the alternative is a link error rather than a + * missing feature. + */ +#define HAVE_SQLITE3 1 +#define HAVE_SQLITE3_ERRSTR 1 +#define HAVE_SQLITE3_EXPANDED_SQL 1 +#define SQLITE_OMIT_LOAD_EXTENSION 1 + +/* + * ---- ext/zip, against `lib/zip' (libzip 1.11.4). Each of the five + * feature names is a `PHP_CHECK_LIBRARY' on one libzip function, and + * 1.11.4 is newer than all five, which the sweep confirms by naming + * the file each is defined in. + */ +#define HAVE_ZIP 1 +#define HAVE_LIBZIP_VERSION 1 +#define HAVE_ENCRYPTION 1 +#define HAVE_SET_MTIME 1 +#define HAVE_PROGRESS_CALLBACK 1 +#define HAVE_CANCEL_CALLBACK 1 +#define HAVE_METHOD_SUPPORTED 1 + +/* + * ---- ext/sockets is NOT built, and the reason is in libap rather + * than in php. `' has no `struct cmsghdr', no + * `CMSG_DATA'/`CMSG_LEN'/`CMSG_SPACE', no `MSG_TRUNC'/`MSG_CTRUNC', + * and `struct msghdr' has no `msg_flags'; `sendmsg' is a stub + * returning ENOSYS and **`recvmsg' is declared by the header and + * defined nowhere**, which `apfunc-sweep' reports as `declared' -- the + * middle state that link-fails far from the claim. + * + * Phoronix wants sockets only "when running the Phoromatic Server", + * so it is the least valuable of the nine and the only one needing a + * public struct to change. `docs/notes/php.md' has what it would + * take. + */ +/* #undef HAVE_SOCKETS */ /* #undef HAVE_GETIFADDRS */ /* #undef HAVE_GETLOADAVG */ /* #undef HAVE_GETTID */ diff --git a/sys/src/ape/cmd/php/internal_functions_cli.c b/sys/src/ape/cmd/php/internal_functions_cli.c index ba9bc1c17..2424de21e 100644 --- a/sys/src/ape/cmd/php/internal_functions_cli.c +++ b/sys/src/ape/cmd/php/internal_functions_cli.c @@ -62,6 +62,14 @@ #include "ext/spl/php_spl.h" #include "ext/standard/php_standard.h" #include "ext/uri/php_uri.h" +#include "ext/posix/php_posix.h" +#include "ext/pcntl/php_pcntl.h" +#include "ext/bz2/php_bz2.h" +#include "ext/readline/php_readline.h" +#include "ext/openssl/php_openssl.h" +#include "ext/curl/php_curl.h" +#include "ext/sqlite3/php_sqlite3.h" +#include "ext/zip/php_zip.h" static zend_module_entry * const php_builtin_extensions[] = { @@ -86,6 +94,24 @@ static zend_module_entry * const php_builtin_extensions[] = { phpext_standard_ptr, phpext_spl_ptr, phpext_uri_ptr, + /* + * The eight optional extensions Phoronix asks for. Only one of + * the eight declares a dependency -- `PHP_ADD_EXTENSION_DEP(zip, + * pcre)' -- and pcre is the second entry in this array, so the + * order below is free and is the order the mkfile lists them in. + * + * GD is the ninth and is absent because there is no libgd in + * this tree; ext/sockets is the tenth and is deferred to a libap + * change. Both are explained in `php_config.h'. + */ + phpext_posix_ptr, + phpext_pcntl_ptr, + phpext_bz2_ptr, + phpext_readline_ptr, + phpext_openssl_ptr, + phpext_curl_ptr, + phpext_sqlite3_ptr, + phpext_zip_ptr, }; diff --git a/sys/src/ape/cmd/php/mkfile b/sys/src/ape/cmd/php/mkfile index 9effa473f..682e5f694 100644 --- a/sys/src/ape/cmd/php/mkfile +++ b/sys/src/ape/cmd/php/mkfile @@ -512,6 +512,50 @@ OFILES=\ ext/zlib/zlib.$O\ ext/zlib/zlib_fopen_wrapper.$O\ ext/zlib/zlib_filter.$O\ +# ***THE EIGHT OPTIONAL EXTENSIONS PHORONIX ASKS FOR.*** Nine are\ +# listed; **GD is the one left out and it is not a choice** -- there\ +# is no libgd in this tree, only libpng, and libpng is not a drawing\ +# library. `ext/sockets' is the ninth and is deferred: it needs\ +# `struct cmsghdr', the `CMSG_*' macros, `msg_flags' and a real\ +# `recvmsg' in libap, which is a public-struct change. The\ +# `php_config.h' entry has the detail.\ +#\ +# Each file list is the extension's own `config.m4'\ +# `PHP_NEW_EXTENSION' line, read rather than recalled, after that\ +# mistake cost three rounds on `ext/dom' above.\ +#\ +# **The libraries all come in by `#pragma lib' from their own\ +# headers** -- `', `', `',\ +# `', `', `' -- which is why there is\ +# still no `LIB=' line in this mkfile.\ +# ext/posix\ + ext/posix/posix.$O\ +# ext/pcntl\ + ext/pcntl/pcntl.$O\ + ext/pcntl/php_signal.$O\ +# ext/bz2\ + ext/bz2/bz2.$O\ + ext/bz2/bz2_filter.$O\ +# ext/readline\ + ext/readline/readline.$O\ + ext/readline/readline_cli.$O\ +# ext/openssl\ + ext/openssl/openssl.$O\ + ext/openssl/openssl_pwhash.$O\ + ext/openssl/openssl_backend_common.$O\ + ext/openssl/openssl_backend_v1.$O\ + ext/openssl/openssl_backend_v3.$O\ + ext/openssl/xp_ssl.$O\ +# ext/curl\ + ext/curl/interface.$O\ + ext/curl/multi.$O\ + ext/curl/share.$O\ + ext/curl/curl_file.$O\ +# ext/sqlite3\ + ext/sqlite3/sqlite3.$O\ +# ext/zip\ + ext/zip/php_zip.$O\ + ext/zip/zip_stream.$O\ # ext/opcache\ ext/opcache/ZendAccelerator.$O\ ext/opcache/shared_alloc_mmap.$O\ @@ -816,6 +860,8 @@ OBJDIRS=\ ext/lexbor/lexbor/unicode ext/lexbor/lexbor/url ext/opcache\ ext/libxml ext/dom ext/dom/parentnode\ ext/dom/lexbor/selectors-adapted ext/simplexml ext/zlib\ + ext/posix ext/pcntl ext/bz2 ext/readline ext/openssl\ + ext/curl ext/sqlite3 ext/zip\ ext/pcre ext/pcre/pcre2lib ext/random ext/reflection ext/spl\ ext/standard ext/standard/libavifinfo ext/uri\ ext/uri/uriparser/src main main/streams sapi/cli diff --git a/sys/src/external/php/ext/openssl/php_openssl_backend.h b/sys/src/external/php/ext/openssl/php_openssl_backend.h index 00da5e74f..8035e0c12 100644 --- a/sys/src/external/php/ext/openssl/php_openssl_backend.h +++ b/sys/src/external/php/ext/openssl/php_openssl_backend.h @@ -290,6 +290,26 @@ X509_REQ *php_openssl_csr_from_param( #define PHP_OPENSSL_ASN1_INTEGER_set ASN1_INTEGER_set #endif +#if defined(LIBRESSL_VERSION_NUMBER) +/* + * APExp: LibreSSL has no `*_new_reserve' family at all -- not + * `sk_X509_new_reserve', not the `OPENSSL_sk_new_reserve' underneath + * it -- so `openssl_backend_common.c:714' does not compile against it. + * + * The call is a CAPACITY HINT and nothing else: the stack it makes is + * filled by `sk_X509_push' in the loop immediately below, and push + * grows the stack on demand. So `sk_X509_new_null()' is the same + * program with one more realloc in it, and the second argument -- + * `sk_X509_INFO_num(sk)' -- is discarded rather than worked around. + * + * ***NOT a stub that answers failure, and not a change of meaning***: + * both forms return a new empty stack or NULL. The comparison cost + * is the only thing that differs, and `NULL' is already being passed + * as the comparison function at this one site. + */ +#define sk_X509_new_reserve(cmp, n) sk_X509_new_null() +#endif + EVP_PKEY *php_openssl_extract_public_key(EVP_PKEY *priv_key); struct php_openssl_pem_password { diff --git a/sys/src/external/php/ext/pcntl/pcntl.c b/sys/src/external/php/ext/pcntl/pcntl.c index e2443bb93..336700e6f 100644 --- a/sys/src/external/php/ext/pcntl/pcntl.c +++ b/sys/src/external/php/ext/pcntl/pcntl.c @@ -1492,7 +1492,19 @@ void pcntl_signal_dispatch(void) static void pcntl_signal_dispatch_tick_function(int dummy_int, void *dummy_pointer) { - return pcntl_signal_dispatch(); + /* + * APExp: was `return pcntl_signal_dispatch();'. Returning a void + * EXPRESSION from a void function is a constraint violation -- + * C11 6.8.6.4p1, "A return statement with an expression shall not + * appear in a function whose return type is void" -- which gcc + * and clang accept as an extension and kencc rejects: + * + * pcntl.c:1495 incompatible types: "VOID" and "VOID" for op "RETURN" + * + * *Both operands void is the tell that this is the rule and not a + * type mismatch.* The two statements below are the same program. + */ + pcntl_signal_dispatch(); } /* {{{ Enable/disable asynchronous signal handling and return the old setting. */ diff --git a/sys/src/external/php/ext/readline/readline_cli.c b/sys/src/external/php/ext/readline/readline_cli.c index bf508a79a..55f00838c 100644 --- a/sys/src/external/php/ext/readline/readline_cli.c +++ b/sys/src/external/php/ext/readline/readline_cli.c @@ -763,6 +763,33 @@ this extension sharedto offer compatibility. #endif*/ #endif +#if !defined(COMPILE_DL_READLINE) && !defined(HAVE_DLFCN_H) +/* + * APExp: upstream's own alternative, three lines above this one, + * restored for the one case it was written for. + * + * The `dlsym' arm is deliberate and upstream's comment says why: even + * the CGI SAPI links against "CLI"-only extensions, so the symbol may + * genuinely be absent at run time and the lookup has to be dynamic. + * **Neither half of that applies to a build with no dynamic linking at + * all**: this extension is static, `php_cli_get_shell_callbacks' is + * `sapi/cli/php_cli.c:111' in the same binary, and `sapi/cli/cli.h' is + * already included above, so the direct call is resolved by the linker. + * + * ***AND THE ALTERNATIVE HERE IS NOT "LEAVE IT ALONE".*** APE does + * ship a `', whose `dlsym' is `return NULL' -- so including + * it compiles, links, and then installs no callbacks at all, leaving + * `php -a' without readline and with nothing printed to say so. *A + * stub that answers "nothing found" is the trap, not the fix.* + * + * Keyed on `HAVE_DLFCN_H' rather than on Plan 9: the condition is that + * there is no dynamic linking, and any platform that has it keeps + * upstream's arm untouched. + */ +#undef GET_SHELL_CB +#define GET_SHELL_CB(cb) (cb) = php_cli_get_shell_callbacks() +#endif + PHP_MINIT_FUNCTION(cli_readline) { cli_shell_callbacks_t *cb;