Skip to content

Commit aab97bc

Browse files
authored
0.9.0 --- a build can state which version of this library it holds (#21)
* 0.9.0 --- a build can state which version of this library it holds Two rounds of mcpplibs/openkal-linux#13 were answered against the wrong version, and the software gave no way to settle the question. The only version-shaped thing a program could read was `uname`'s `release` field, which was the string literal "0.5.0" through every release after 0.5.0. It was therefore not a missing answer but a WRONG one: a consumer who checked it was misled rather than left uncertain. --- what changes -------------------------------------------------------------- `build.mcpp` reads the version from `mcpp.toml` and defines it. THE NUMBER IS STATED IN ONE PLACE: a `defines` entry in the manifest would state it a second time, four lines below where it already is, and the two would agree until one of them was edited. A manifest the program cannot read yields no definition, and the field then reports "unknown" -- a true statement, unlike the constant it replaces. `uname`'s release field is that version. `OPENKAL_MUSL_TRACE=enosys` names it before the program runs, and WHETHER OR NOT ANYTHING IS MISSING. That last part is the substance rather than a detail. A run in which nothing was refused printed nothing at all, so three situations were one reading: the version is right and no operation is absent; the variable did not take effect; this is not the binary the reader thinks it is. The report that began this round could not distinguish them, and neither could we. --- the quoting, because it failed first -------------------------------------- `-DOKM_VERSION="0.9.0"` reaches a build file that a shell executes, so the compiler received `-DOKM_VERSION=0.9.0`: <command-line>: error: too many decimal points in number naming a line in build.mcpp that was correct. The escape is what the shell removes now, and the quote survives. --- criteria ------------------------------------------------------------------ Three, and the third is the one that protects everybody who is NOT debugging: ok silent unless asked ok the banner names 0.9.0 ok uname reports 0.9.0 Without the third this could become noise in every ordinary run and nothing would say so. Measured: with the variable unset the probe writes zero bytes to the error stream. Confirmed by putting the previous behaviour back: banner: FAIL got=[] uname: FAIL got=[0.5.0] `examples/posix` asserts that the field is neither empty nor the placeholder, and deliberately does NOT assert a particular number -- one that named a version would have to be edited by every release rather than checked by it. The workflow is what compares the number against `mcpp.toml`. --- what a program above this observes ---------------------------------------- ⚠️ The release field MOVES AT EVERY RELEASE. Nothing here or in musl reads it --- `gethostname` and `getdomainname` are musl's only consumers of `uname` and both read `nodename` --- but a program that compares the field against a fixed string will see it change. `sysname` is "openkal" and not "Linux", so nothing can have been reading it as a kernel version. Recorded in README.md's divergence table. Nothing else changes: 49 observations, 0 failures. * ci: the count comparison is arithmetic, because this step runs on BSD too The macOS row reported ##[error]the program wrote 0 line(s) to the error stream without being asked on a program that wrote nothing. BSD `wc' pads its count with spaces where GNU `wc' does not, so `[ "$quiet" = 0 ]' compared " 0" against "0". The step above this one does the same thing and never met it: it carries `if: runner.os == 'Linux'`. This one deliberately runs on every row, because the property it asserts --- that the library says nothing unless asked --- is one every system's consumers depend on.
1 parent 4e7f0ac commit aab97bc

9 files changed

Lines changed: 272 additions & 12 deletions

File tree

.agents/docs/2026-08-30-issue13-round-four-timeout-truncate-and-provenance.md

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -404,16 +404,44 @@ read.
404404
So `uname -r` is a *lying* oracle, which is worse than none: a consumer who
405405
checks it is told a version, and the version is wrong.
406406

407-
Two changes, neither large:
408-
409-
- Derive the `release` field from the package version at build time, so that
410-
`uname -r` answers.
411-
- Have `OPENKAL_MUSL_TRACE` print one line naming the version before anything
412-
else, so that a trace pasted into an issue carries its own provenance.
407+
**Done in 0.9.0**, both halves:
408+
409+
- The `release` field is derived from the package version. `build.mcpp` reads
410+
`mcpp.toml` and defines it, so the number is stated in one place; a manifest
411+
it cannot read yields no definition and the field reports `unknown`, which is
412+
a true statement rather than a false one.
413+
- `OPENKAL_MUSL_TRACE=enosys` names the version **before the program runs**, and
414+
whether or not anything is missing. That last part is the substance: a run in
415+
which nothing was refused printed nothing at all, so "the version is right and
416+
nothing is absent", "the variable did not take effect" and "this is not the
417+
binary I think it is" were one reading.
413418

414419
The second matters more, because the trace is what a reporter is already asked
415-
to paste. A report that carries its version is a report that cannot be answered
416-
against the wrong one, which is the whole of what went wrong this round.
420+
to paste. A report that carries its version cannot be answered against the wrong
421+
one, which is the whole of what went wrong this round.
422+
423+
**measured**, and the third observation is the one that protects everybody who
424+
is not debugging:
425+
426+
```
427+
ok silent unless asked
428+
ok the banner names 0.9.0
429+
ok uname reports 0.9.0
430+
```
431+
432+
and against the previous behaviour, with the constant put back:
433+
434+
```
435+
banner: FAIL got=[]
436+
uname: FAIL got=[0.5.0]
437+
```
438+
439+
⚠️ The release field now MOVES AT EVERY RELEASE. Nothing here or in musl reads
440+
it — `gethostname` and `getdomainname` are musl's only consumers of `uname` and
441+
both read `nodename` — but a program above that compares it against a fixed
442+
string will see it change. Recorded in `README.md`'s divergence table. The
443+
trade was taken because the previous value was not merely uninformative: it was
444+
wrong, and a consumer who checked it was misled rather than left uncertain.
417445

418446
---
419447

.github/workflows/ci.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,69 @@ jobs:
522522
MCPP_TARGET: ${{ matrix.target }}
523523
run: bash tools/run-probe.sh examples/posix posix
524524

525+
# A BUILD STATES WHICH VERSION OF THIS LIBRARY IT HOLDS, AND THE STATEMENT
526+
# IS COMPARED WITH THE MANIFEST RATHER THAN READ.
527+
#
528+
# This step exists because of what its absence cost. Two rounds of
529+
# mcpplibs/openkal-linux#13 were answered against the wrong version: the
530+
# only version-shaped thing a program could read was `uname`'s release
531+
# field, which was the literal "0.5.0" through every release after 0.5.0,
532+
# so a consumer who checked it was told a version and the version was
533+
# wrong. The report that followed named versions its author believed in
534+
# and could not verify, and neither could we.
535+
#
536+
# Three observations, and the third is the one that keeps this honest for
537+
# everybody who is NOT debugging:
538+
#
539+
# 1. the banner names exactly what mcpp.toml says
540+
# 2. `uname` reports the same string
541+
# 3. WITHOUT the variable, the program writes nothing to the error
542+
# stream at all
543+
#
544+
# Without (3) this feature would be free to become noise in every
545+
# ordinary run, and nothing would say so.
546+
- name: A build states its own version, and says nothing when not asked
547+
env:
548+
MCPP_TARGET: ${{ matrix.target }}
549+
run: |
550+
set -euo pipefail
551+
want="$(sed -n 's/^version *= *"\(.*\)"/\1/p' mcpp.toml | head -1)"
552+
[ -n "$want" ] || { echo "::error::no version in mcpp.toml"; exit 1; }
553+
echo " the manifest says $want"
554+
555+
cd examples/posix
556+
binary="$(bash "$GITHUB_WORKSPACE/tools/one-artifact.sh" posix)"
557+
558+
# (3) first, because it is the property an ordinary run depends on.
559+
#
560+
# `-eq' AND NOT `='. This step runs on every row, and BSD `wc' pads its
561+
# count with spaces where GNU `wc' does not, so a string comparison
562+
# reads " 0" and fails on a program that wrote nothing:
563+
#
564+
# ##[error]the program wrote 0 line(s) to the error stream
565+
#
566+
# The step above that does the same thing is `if: runner.os == Linux',
567+
# which is why it never met this.
568+
quiet="$("./$binary" 2>&1 >/dev/null | wc -l)"
569+
[ "$quiet" -eq 0 ] || {
570+
echo "::error::the program wrote $quiet line(s) to the error stream without being asked"
571+
"./$binary" 2>&1 >/dev/null | sed 's/^/ /'; exit 1; }
572+
echo " ok silent unless asked"
573+
574+
# (1) the banner. Each process announces itself once, so a program
575+
# that starts another produces more than one line; they must agree.
576+
banners="$(OPENKAL_MUSL_TRACE=enosys "./$binary" 2>&1 >/dev/null | grep '^openkal-musl [0-9]' | sort -u)"
577+
[ "$banners" = "openkal-musl $want" ] || {
578+
echo "::error::expected exactly 'openkal-musl $want'; got:"
579+
printf '%s\n' "$banners" | sed 's/^/ /'; exit 1; }
580+
echo " ok the banner names $want"
581+
582+
# (2) and the field a program reads without setting anything.
583+
got="$("./$binary" 2>/dev/null | sed -n 's/^note: release=//p' | head -1)"
584+
[ "$got" = "$want" ] || {
585+
echo "::error::uname reports '$got' and the manifest says '$want'"; exit 1; }
586+
echo " ok uname reports $want"
587+
525588
# ⭐ THE NAMES THIS LIBRARY MUST NOT TAKE FROM A PROGRAM ABOVE IT.
526589
#
527590
# Reported as openkal-musl#13. This one is a COMPILE-TIME criterion: the

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ the claim can be checked rather than repeated.
88

99
```toml
1010
[dependencies]
11-
openkal-musl = "0.8.0"
11+
openkal-musl = "0.9.0"
1212
```
1313

1414
It names no implementation and no platform: a C library is the one consumer that
@@ -118,6 +118,7 @@ answer that leaves a program wrong without telling it.
118118
| an immediate answer about a started program | `waitpid(…, WNOHANG)` returns without the program having finished, but may wait up to one polling interval of the implementation beneath (one millisecond on Linux) | `kal_timeout_wait_process` takes a bound and openkal spells "no bound" as zero, so a caller that does not want to wait asks for the smallest bound there is. An environment rounds a bound up to what its clock can distinguish; a bound shorter than the clock is a promise no environment can keep. |
119119
| closing a standard stream in a program being started | `posix_spawn_file_actions_addclose(&fa, 0…2)` makes the spawn report `ENOSYS`; above position two it is performed, because nothing there is inherited | openkal has no value meaning "no stream", and the value that looks like one — zero — means the opposite: the stream the caller has. Accepting the action and not performing it would hand a program the standard input its caller had just taken away. |
120120
| starting a program upon a stream whose handle is zero | a caller that redirects its **output** onto its own standard input and then starts a program gets `ENOSYS` | `kal_spawn_streams` reserves zero for inheritance and `kal_stream` reserves nothing, so an implementation whose streams are the environment's own descriptors hands out zero for standard input. The two agree at position zero and cannot be told apart anywhere else. Reported upstream; refused here rather than answered wrongly. |
121+
| ~~a version a program can read~~ | **answered since 0.9.0.** `uname`'s `release` field is this package's version, and `OPENKAL_MUSL_TRACE=enosys` names it on the error stream once per process before the program runs | It was the string literal `0.5.0` through every release after 0.5.0, so a program that asked was not left without an answer -- it was given a false one. ⚠️ It therefore MOVES AT EVERY RELEASE: nothing here or in musl reads it (`gethostname` and `getdomainname` are musl's only consumers of `uname` and both read `nodename`), but a program above it that compares the field against a fixed string will see it change. `sysname` is `openkal` and not `Linux`, so nothing can have been reading it as a kernel version. |
121122
| **setting** the modification time of a directory | `utimensat` on a directory is refused, so `std::filesystem::last_write_time(dir, t)` throws. **Reading** it is unaffected and correct. The value differs by implementation and is the implementation's to give: `EISDIR` on Linux and macOS, `EACCES` on Windows — both measured | `kal_fs_set_modified` takes a `kal_file` and openkal has neither a `kal_dir` form of it nor a form that takes a name, so this port opens the name as a file, which a directory refuses. What a backend says about that is its own: one distinguishes a directory and one does not. Note that libc++ gives both overloads of `last_write_time` the same name in the message it throws, so the text does not say which of the two failed: the reading overload is `stat` and works on a directory. A caller using a lock directory's timestamp reads it to decide staleness and writes it to refresh the lock, and only the second fails. |
122123

123124
**⭐ What carries confinement here, since a mode word does not.** A program that
@@ -181,9 +182,22 @@ spent on exactly that question.
181182
Each operation the dispatcher has no case for is then named on the standard
182183
error stream, **once**, whatever the number of attempts:
183184

185+
openkal-musl 0.9.0
184186
openkal-musl: no operation for system call 266
185187

186-
Three properties, each of them asserted in continuous integration because each
188+
**The first line is the version, and it is printed whether or not anything is
189+
missing.** That is the whole reason it exists. Before 0.9.0 a run in which
190+
nothing was refused printed nothing at all, and three situations were then
191+
indistinguishable: the version is right and no operation is absent, the variable
192+
did not take effect, or this is not the binary the reader thinks it is. Two
193+
rounds of [openkal-linux#13](https://github.com/mcpplibs/openkal-linux/issues/13)
194+
were answered against the wrong version because of it.
195+
196+
A report pasted into an issue therefore carries its own provenance, and one
197+
process contributes one such line — a program that starts another produces one
198+
for each, and they must agree.
199+
200+
Four properties, each of them asserted in continuous integration because each
187201
of the corresponding failures is quiet:
188202

189203
- **Nothing is reported unless the variable is set.** A diagnostic that appears
@@ -195,6 +209,10 @@ of the corresponding failures is quiet:
195209
answer `ENOSYS` from cases of their own, each a decision with a reason
196210
recorded beside it. Reporting those would name a facility as missing that this
197211
port deliberately does not have, which is a different sentence.
212+
- **The version named is the one in `mcpp.toml`.** It is read from the manifest
213+
by `build.mcpp` rather than written out a second time, and the workflow
214+
compares the printed line against the manifest. A version stated in two places
215+
agrees until one of them is edited.
198216

199217
The report is written to the stream directly rather than through this library's
200218
own output, because what failed may be the operation that output was about to

build.mcpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,70 @@ std::string env_or_empty(const char* name) {
6868
return v ? v : "";
6969
}
7070

71+
// THE VERSION IS READ FROM THE MANIFEST RATHER THAN WRITTEN OUT A SECOND TIME.
72+
//
73+
// A program built on this library could not state which version of it it held.
74+
// The only version-shaped thing it could read was `uname`'s release field,
75+
// which was the string literal "0.5.0" and had never moved -- so a consumer who
76+
// checked it was told a version, and the version was wrong. Two rounds of
77+
// mcpplibs/openkal-linux#13 turned on a question the software gave no way to
78+
// settle.
79+
//
80+
// A `defines` entry in the manifest would state the number a second time, four
81+
// lines below where it is already stated, and the two would agree until one of
82+
// them was edited. So it is read from the one place that has it. An unreadable
83+
// manifest yields no definition at all rather than a wrong one, and the header
84+
// that consumes this reports "unknown" -- which is a true statement, unlike
85+
// the constant it replaces.
86+
std::string package_version() {
87+
const std::string dir = env_or_empty("MCPP_MANIFEST_DIR");
88+
if (dir.empty()) return "";
89+
std::ifstream in(dir + "/mcpp.toml");
90+
if (!in) return "";
91+
92+
// The first `version = "..."` under [package]. Sections after it declare
93+
// versions of other things -- dependencies name theirs the same way -- so
94+
// reading past the first table would answer about one of those.
95+
std::string line;
96+
bool in_package = false;
97+
while (std::getline(in, line)) {
98+
const auto first = line.find_first_not_of(" \t");
99+
if (first == std::string::npos || line[first] == '#') continue;
100+
if (line[first] == '[') { in_package = line.compare(first, 9, "[package]") == 0; continue; }
101+
if (!in_package) continue;
102+
if (line.compare(first, 7, "version") != 0) continue;
103+
const auto open = line.find('"');
104+
if (open == std::string::npos) continue;
105+
const auto close = line.find('"', open + 1);
106+
if (close == std::string::npos) continue;
107+
return line.substr(open + 1, close - open - 1);
108+
}
109+
return "";
110+
}
111+
71112
int main() {
72113
const std::string os = env_or_empty("MCPP_TARGET_OS");
73114
const std::string fam = compiler_family();
74115

116+
// BEFORE THE EARLY RETURN BELOW, AND THAT IS THE WHOLE OF WHY IT IS HERE.
117+
// The compiler family decides whether a runtime library is named; it does
118+
// not decide whether this library knows its own version. Emitting this
119+
// after that return would define the version under one compiler and leave
120+
// it undefined under the other, and the sources would then disagree about
121+
// what they report while compiling cleanly under both.
122+
if (const std::string v = package_version(); !v.empty()) {
123+
mcpp::rerun_if_changed((env_or_empty("MCPP_MANIFEST_DIR") + "/mcpp.toml").c_str());
124+
// THE QUOTES HAVE TO SURVIVE A SHELL. The build file this reaches is
125+
// executed by one, so `-DOKM_VERSION="0.9.0"` arrives at the compiler
126+
// as `-DOKM_VERSION=0.9.0` and 0.9.0 is not an expression:
127+
//
128+
// <command-line>: error: too many decimal points in number
129+
//
130+
// naming a line in this file that is correct. Escaped here so that
131+
// what the shell removes is the escape rather than the quote.
132+
mcpp::cflag(("-DOKM_VERSION=\\\"" + v + "\\\"").c_str());
133+
}
134+
75135
if (fam != "gcc") return 0;
76136

77137
// ⚠️ ELF NEEDS IT TOO, AND FOR THE SAME REASON THE PE BLOCK DID. The Linux

examples/posix/src/main.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <pthread.h>
1111
#include <time.h>
1212
#include <sys/stat.h>
13+
#include <sys/utsname.h>
1314
#include <sys/wait.h>
1415
#include <spawn.h>
1516

@@ -387,6 +388,30 @@ int main(int argc, char **argv, char **envp) {
387388
check(whole, "several pages are obtained in one allocation and every byte of it holds");
388389
}
389390

391+
/* WHAT VERSION OF THE C LIBRARY THIS PROGRAM HOLDS.
392+
*
393+
* The release field was the string literal "0.5.0" through every release
394+
* after 0.5.0, so a program that asked was given a false answer rather than
395+
* no answer. It now comes from the manifest. This asserts that it is neither
396+
* absent nor the placeholder a build that could not read the manifest would
397+
* leave -- it deliberately does NOT assert a particular number, because the
398+
* number moves at every release and an observation naming one would have to
399+
* be edited by every release rather than checked by it.
400+
*
401+
* The build that DOES check the number is the workflow, which compares this
402+
* line against mcpp.toml. Reported in openkal-linux#13, where two rounds were
403+
* spent establishing which version a consumer had actually built. */
404+
{
405+
struct utsname un;
406+
const int ok = uname(&un) == 0;
407+
check(ok, "the system reports its identity");
408+
if (ok) {
409+
printf("note: release=%s\n", un.release);
410+
check(un.release[0] != '\0' && strcmp(un.release, "unknown") != 0,
411+
"the C library reports a version rather than a placeholder");
412+
}
413+
}
414+
390415
printf("-- failures: %d --\n", failures);
391416
return failures ? 1 : 0;
392417
}

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
namespace = "mcpplibs"
33
name = "openkal-musl"
4-
version = "0.8.0"
4+
version = "0.9.0"
55
description = "musl 1.2.5 redirected onto openkal: one C library, ported once, above every implementation of the specification rather than above one kernel."
66
license = "Apache-2.0"
77

port/src/okm.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ int okm_fd_cloexec(int fd, int on);
169169
int okm_fd_get_cloexec(int fd);
170170
void okm_table_init(void);
171171

172+
/* Announces this library's version on the standard error stream, and only
173+
* when OPENKAL_MUSL_TRACE asks for it. Called once from startup rather than
174+
* from the first refused operation: a run in which nothing is refused would
175+
* otherwise print nothing, and a consumer could not tell that from a run in
176+
* which the variable never took effect. okm_syscall.c states the rest. */
177+
void okm_trace_banner(void);
178+
extern const char okm_version[];
179+
172180
/* The stream descriptor `fd' named when the program began, for fd in [0,3), and
173181
* zero otherwise. What a spawn needs in order to tell a descriptor that has been
174182
* redirected from one that has not; okm_fd.c records why the question is about

port/src/okm_start.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,14 @@ void __okm_libc_init(void)
326326
__init_tls(0);
327327
__init_ssp(g_random);
328328
okm_table_init();
329+
330+
/* AFTER THE ENVIRONMENT IS READABLE AND BEFORE THE PROGRAM RUNS. The
331+
* banner asks openkal for a named value, so it cannot precede
332+
* __okm_init_env; and it must precede main, because the question it
333+
* answers -- which version is this -- is asked by a reader of the
334+
* program's output, who cannot be sure the variable took effect if the
335+
* only evidence is a report that may never be produced. */
336+
okm_trace_banner();
329337
}
330338

331339
int __libc_start_main(int (*main_fn)(int, char**, char**), int argc, char** argv,

0 commit comments

Comments
 (0)