|
31 | 31 | default: "" |
32 | 32 | env: |
33 | 33 | MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }} |
34 | | - MCPP_VERSION: 2026.8.26.2 |
| 34 | + MCPP_VERSION: 2026.8.27.1 |
35 | 35 | XLINGS_VERSION: v2026.8.17.2 |
36 | 36 | XLINGS_NON_INTERACTIVE: '1' |
37 | 37 |
|
|
54 | 54 | steps: |
55 | 55 | - uses: actions/checkout@v4 |
56 | 56 |
|
| 57 | + # WHAT IS COMMITTED NAMES NO DIRECTORY OF ANYBODY'S MACHINE. |
| 58 | + # |
| 59 | + # tools/working-trees.sh rewrites this manifest to name working trees and |
| 60 | + # deliberately does not restore it: the rewrite must survive for the rest |
| 61 | + # of the job. A run by hand therefore leaves the manifest naming absolute |
| 62 | + # paths, and committing that publishes them --- a consumer resolving from |
| 63 | + # the index is handed a manifest pointing at a directory that exists |
| 64 | + # nowhere. |
| 65 | + # |
| 66 | + # ⚠️ THAT HAS HAPPENED. This step runs BEFORE the rewrite, so what it |
| 67 | + # examines is what the commit contains rather than what the job has done |
| 68 | + # to it. |
| 69 | + - name: The committed manifest names no local directory |
| 70 | + run: | |
| 71 | + set -euo pipefail |
| 72 | + bad=$(grep -nE '^[a-z-]+ = \{[^}]*path = "(/|[A-Za-z]:)' mcpp.toml || true) |
| 73 | + if [ -n "$bad" ]; then |
| 74 | + echo "::error::the committed manifest names an absolute path" |
| 75 | + printf '%s\n' "$bad" | sed 's/^/ /' |
| 76 | + echo " tools/working-trees.sh rewrites these and does not restore them;" |
| 77 | + echo " run 'git checkout -- mcpp.toml' after using it by hand." |
| 78 | + exit 1 |
| 79 | + fi |
| 80 | + echo " ok every dependency is named by version or by a relative path" |
| 81 | +
|
57 | 82 | - name: The specification and the implementation for this system |
58 | 83 | run: | |
59 | 84 | bash tools/working-trees.sh '${{ github.head_ref || github.ref_name }}' \ |
@@ -190,6 +215,183 @@ jobs: |
190 | 215 | grep kal_random syms.txt; exit 1; } |
191 | 216 | echo " ok kal_random_fill is weak, kal_time_sleep is strong" |
192 | 217 |
|
| 218 | + # ⭐⭐ ASKING WHETHER A STREAM IS A TERMINAL GETS THE RIGHT ANSWER. |
| 219 | + # |
| 220 | + # musl's `isatty' asks with TIOCGWINSZ; this port answered only TCGETS, |
| 221 | + # so every `isatty' returned 0 — for a real terminal as readily as for a |
| 222 | + # pipe. Nothing failed: `std::print' simply never took its terminal path, |
| 223 | + # and a program deciding on colour or on line buffering decided wrongly |
| 224 | + # and in silence. |
| 225 | + # |
| 226 | + # ⭐ THE CRITERION IS A RELATION, NOT A VALUE. `isatty` under a pipe and |
| 227 | + # under a pseudo-terminal must DIFFER, and must differ the same way the |
| 228 | + # system's own C library does. A test asserting "0 in a pipe" alone would |
| 229 | + # have passed throughout the defect. |
| 230 | + - name: Asking whether a stream is a terminal is answered, not refused |
| 231 | + if: runner.os == 'Linux' && matrix.target == '' |
| 232 | + run: | |
| 233 | + d="$(mktemp -d)"; mkdir -p "$d/src" |
| 234 | + cat > "$d/mcpp.toml" <<TOML |
| 235 | + [package] |
| 236 | + name = "isattyprobe" |
| 237 | + version = "0.1.0" |
| 238 | +
|
| 239 | + [dependencies] |
| 240 | + openkal-musl = { path = "$PWD" } |
| 241 | +
|
| 242 | + [targets.isattyprobe] |
| 243 | + kind = "bin" |
| 244 | + main = "src/main.c" |
| 245 | +
|
| 246 | + [build] |
| 247 | + cxx_runtime = "host-coupled" |
| 248 | + TOML |
| 249 | + sed -i 's/^ //' "$d/mcpp.toml" |
| 250 | + printf '#include <unistd.h>\n#include <stdio.h>\nint main(void){ printf("%%d\\n", isatty(1)); return 0; }\n' > "$d/src/main.c" |
| 251 | + ( cd "$d" && mcpp build --toolchain '${{ matrix.toolchain }}' ) |
| 252 | + bin="$(find "$d/target" -name isattyprobe -type f | head -1)" |
| 253 | + test -n "$bin" || { echo "::error::the probe did not build"; exit 1; } |
| 254 | +
|
| 255 | + # The control: the system's own C library, through the same harness. |
| 256 | + # Without it a `script` that fails to allocate a pty would make the |
| 257 | + # port look broken. |
| 258 | + printf '#include <unistd.h>\n#include <stdio.h>\nint main(void){ printf("%%d\\n", isatty(1)); return 0; }\n' > "$d/ctrl.c" |
| 259 | + cc "$d/ctrl.c" -o "$d/ctrl" |
| 260 | + ctrl_pipe="$("$d/ctrl" | cat | tr -d '\r')" |
| 261 | + ctrl_tty="$(script -qec "$d/ctrl" /dev/null | tr -d '\r' | head -1)" |
| 262 | + [ "$ctrl_pipe" = 0 ] && [ "$ctrl_tty" = 1 ] \ |
| 263 | + || { echo "::error::the harness cannot tell a pty from a pipe (control gave $ctrl_pipe/$ctrl_tty) — this check would prove nothing" |
| 264 | + exit 1; } |
| 265 | +
|
| 266 | + port_pipe="$("$bin" | cat | tr -d '\r')" |
| 267 | + port_tty="$(script -qec "$bin" /dev/null | tr -d '\r' | head -1)" |
| 268 | + echo " control: pipe=$ctrl_pipe tty=$ctrl_tty port: pipe=$port_pipe tty=$port_tty" |
| 269 | + [ "$port_pipe" = "$ctrl_pipe" ] && [ "$port_tty" = "$ctrl_tty" ] \ |
| 270 | + || { echo "::error::isatty over this port disagrees with the system's own C library"; exit 1; } |
| 271 | + echo " ok isatty answers the same as the system's own C library" |
| 272 | +
|
| 273 | + # ⭐⭐ THE INTERNAL OVERLAY STOPS AT THIS PACKAGE'S BOUNDARY. |
| 274 | + # |
| 275 | + # musl reaches its own declarations through `src/include`, whose headers |
| 276 | + # define `hidden`, `weak` and `weak_alias` — names that mean something |
| 277 | + # only to musl's own sources. This package publishes the path it is built |
| 278 | + # from, so every consumer used to see them too, and which consumer broke |
| 279 | + # on which name was found one at a time (openkal-musl#13). |
| 280 | + # |
| 281 | + # `[build] private_include_dirs` (mcpp 2026.8.27.1) says which entries of |
| 282 | + # `include_dirs` stop here. This asserts the DIRECTORY is absent from a |
| 283 | + # consumer's command line — not that one macro no longer collides, which |
| 284 | + # would go green again the moment the package patched that macro while |
| 285 | + # the leak stayed. |
| 286 | + # WHAT IS WITHHELD IS NOT DEFINED BY THIS PACKAGE, AND WHAT IS NOT |
| 287 | + # WITHHELD IS. |
| 288 | + # |
| 289 | + # ⚠️⚠️ THE CRITERION IS THE PACKAGE'S OWN OBJECTS, NOT A PROGRAM'S LINK. |
| 290 | + # |
| 291 | + # It was a program's link, and that measured the wrong thing. Under one |
| 292 | + # toolchain the probe built successfully for symbols this package does not |
| 293 | + # define at all --- something else on the link line supplied them --- so |
| 294 | + # the check reported "did not fail at the link" for a facility that had in |
| 295 | + # fact been withheld. A program's link is closed over this port only when |
| 296 | + # nothing else answers, and that is a property of the toolchain rather than |
| 297 | + # of this package. |
| 298 | + # |
| 299 | + # A definition either is in these objects or is not. That holds under every |
| 300 | + # toolchain and cannot be satisfied by a host C library. |
| 301 | + # |
| 302 | + # ⚠️ AND THE OBJECTS ARE SCOPED TO ONE FINGERPRINT DIRECTORY. `target/` |
| 303 | + # accumulates one per configuration, so a search across all of them reads |
| 304 | + # definitions from a build made before the change --- which is how this |
| 305 | + # check first reported every withheld symbol as still present. |
| 306 | + - name: What is withheld is not defined, and what is not withheld is |
| 307 | + if: runner.os == 'Linux' && matrix.target == '' |
| 308 | + run: | |
| 309 | + set -euo pipefail |
| 310 | + fps=$(ls -d target/*/*/ 2>/dev/null | wc -l) |
| 311 | + [ "$fps" = 1 ] || { echo "::error::expected one fingerprint directory under target/, found $fps" |
| 312 | + ls -d target/*/*/ 2>/dev/null | sed 's/^/ /' |
| 313 | + exit 1; } |
| 314 | + objs=$(find target -name '*.o') |
| 315 | + n=$(printf '%s\n' "$objs" | grep -c . || true) |
| 316 | + echo " examining $n objects in $(ls -d target/*/*/)" |
| 317 | + [ "$n" -gt 100 ] || { echo "::error::only $n objects; nothing was examined"; exit 1; } |
| 318 | +
|
| 319 | + defines() { # symbol -> the number of definitions in these objects |
| 320 | + nm $objs 2>/dev/null | grep -cE "^[0-9a-f]+ [TWi] $1\$" || true |
| 321 | + } |
| 322 | +
|
| 323 | + fail=0 |
| 324 | + for s in epoll_create1 eventfd timerfd_create inotify_init signalfd; do |
| 325 | + d=$(defines "$s") |
| 326 | + if [ "$d" = 0 ]; then echo " withheld, not defined: $s" |
| 327 | + else echo "::error::$s is withheld and yet defined $d time(s)"; fail=1; fi |
| 328 | + done |
| 329 | +
|
| 330 | + # The other half. Without it the check above would pass for a package |
| 331 | + # that had stopped compiling anything at all. `pipe` is here because |
| 332 | + # openkal 0.8 made it expressible; `socket` and `fork` because their |
| 333 | + # closures reach the password functions and the thread implementation, |
| 334 | + # so they are deliberately NOT withheld and the manifest says why. |
| 335 | + for s in printf malloc open pipe faccessat chmod socket fork; do |
| 336 | + d=$(defines "$s") |
| 337 | + if [ "$d" -ge 1 ]; then echo " defined here: $s" |
| 338 | + else echo "::error::$s is not withheld and yet is not defined"; fail=1; fi |
| 339 | + done |
| 340 | +
|
| 341 | + [ "$fail" = 0 ] || exit 1 |
| 342 | + echo " ok the withheld set is exactly what the manifest names" |
| 343 | +
|
| 344 | + - name: What this package is built from is not what it publishes |
| 345 | + working-directory: examples/cross-hello |
| 346 | + run: | |
| 347 | + extra='' |
| 348 | + [ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}' |
| 349 | + mcpp build --toolchain '${{ matrix.toolchain }}' $extra |
| 350 | + test -s compile_commands.json \ |
| 351 | + || { echo "::error::no compile_commands.json — nothing to check"; exit 1; } |
| 352 | +
|
| 353 | + # ⚠️⚠️ PER ROW, NOT OVER THE FILE. This example depends on the |
| 354 | + # package by path, so THE PROVIDER'S OWN ROWS ARE IN THIS SAME FILE |
| 355 | + # and they carry the overlay legitimately — that is what "private, |
| 356 | + # not unused" means. A `grep` over the whole file cannot tell the two |
| 357 | + # apart and would call the package's own build a leak. |
| 358 | + # |
| 359 | + # ⚠️ Separators are normalised because the Windows runner writes |
| 360 | + # `…\musl\src\include`, and a check that silently stops matching on |
| 361 | + # one platform is a check that platform does not have. |
| 362 | + norm='(.arguments // (.command | split(" "))) | join(" ") | gsub("\\\\"; "/")' |
| 363 | + jq -r ".[] | select((.file | gsub(\"\\\\\\\\\"; \"/\")) | test(\"examples/cross-hello\")) | $norm" \ |
| 364 | + compile_commands.json > consumer.txt |
| 365 | + jq -r ".[] | select((.file | gsub(\"\\\\\\\\\"; \"/\")) | test(\"examples/cross-hello\") | not) | $norm" \ |
| 366 | + compile_commands.json > provider.txt |
| 367 | +
|
| 368 | + # ⚠️ DENOMINATORS ON BOTH SIDES. With no consumer row every absence |
| 369 | + # below is vacuously true; with no provider row the control is. |
| 370 | + cons="$(wc -l < consumer.txt)"; prov="$(wc -l < provider.txt)" |
| 371 | + echo " rows: consumer=$cons provider=$prov" |
| 372 | + [ "$cons" -ge 1 ] && [ "$prov" -ge 1 ] \ |
| 373 | + || { echo "::error::consumer=$cons provider=$prov — nothing was checked"; exit 1; } |
| 374 | +
|
| 375 | + bad=0 |
| 376 | + for d in musl/src/include musl/src/internal musl-generated/internal; do |
| 377 | + if grep -q -- "$d" consumer.txt; then |
| 378 | + echo "::error::the internal overlay '$d' reached a consumer" |
| 379 | + bad=1 |
| 380 | + fi |
| 381 | + # The other half of the same key: private is not the same as |
| 382 | + # dropped. musl's own sources must still reach their declarations. |
| 383 | + grep -q -- "$d" provider.txt \ |
| 384 | + || { echo "::error::'$d' is on nobody's command line — private_include_dirs withheld it from this package too" |
| 385 | + bad=1; } |
| 386 | + done |
| 387 | + # And something PUBLIC must still cross the boundary, or this check |
| 388 | + # would pass for a build that published nothing at all. |
| 389 | + grep -q -- 'port/include' consumer.txt \ |
| 390 | + || { echo "::error::no public include directory reached the consumer — the check above proves nothing" |
| 391 | + exit 1; } |
| 392 | + [ "$bad" = 0 ] || exit 1 |
| 393 | + echo " ok the internal overlay stops here; the public headers do not" |
| 394 | +
|
193 | 395 | # A program above this package names one package. It does not name |
194 | 396 | # openkal, it does not name an implementation, and it says nothing about |
195 | 397 | # the platform. |
|
0 commit comments