From 621627c595f1896649471b7a346d13a8fc2ddc36 Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Sat, 5 Sep 2026 05:37:41 +0200 Subject: [PATCH] Measure on the runner which of 0113's triples it can compile C for (#291) 0243 validates a certificate through `rustls`, every crypto provider `rustls` offers is C, and the targets leg compiles seven triples on one Linux runner. 0291 weighed the cross-toolchain, the dropped client platform and the wait, took the wait, and recorded in its own text that it had never measured which C cross-compilers that image carries: every reading behind the question was taken on a Windows machine outside this tree. #291's body names that measurement as the issue's own first act, and it was still unmade. This is that measurement, taken where it has to be taken. A new leg compiles one C translation unit for every triple in 0113's set, using the invocations named in a register beside it, and reports which of them the image can compile C for. The unit asks for the C library headers on purpose: a driver reaches an unknown platform's triple without a sysroot for as long as nothing needs a header, so a probe that stopped at resolving a name would report every Apple triple served on a Linux image carrying no Apple SDK. The failure it prevents is a survey that cannot be read. A block short of a field, a field spelled wrong, a triple in 0113's set that no block covers, a probe for a triple that set does not name, a loader that stopped matching, and a register naming nothing all exit zero and print a page indistinguishable from a run that examined seven triples and found every one of them served. Each is refused, and each was watched refusing before it shipped. It refuses nothing about what it finds. A triple with no compiler on the image is the state 0291 already decided to wait through, and a red tick for it would redden every pull request for a decision that is already taken and written down. The set of triples is not read twice. `.github/targets/targets.sh` gains a `triples` verb, so the survey consumes the reader the targets leg already proves with its own fixtures instead of carrying a second copy that would agree with it until the day it did not. #291 stays open on its second condition, which asks for a green target leg on a tree carrying the dependency, and no tree carries it. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- .github/cross-toolchain/cross-toolchain.sh | 560 +++++++++++++++++++++ .github/cross-toolchain/probes | 86 ++++ .github/targets/targets.sh | 16 +- .github/workflows/cross-toolchain.yml | 78 +++ CONTRIBUTING.md | 33 +- 5 files changed, 766 insertions(+), 7 deletions(-) create mode 100644 .github/cross-toolchain/cross-toolchain.sh create mode 100644 .github/cross-toolchain/probes create mode 100644 .github/workflows/cross-toolchain.yml diff --git a/.github/cross-toolchain/cross-toolchain.sh b/.github/cross-toolchain/cross-toolchain.sh new file mode 100644 index 0000000..516d0d5 --- /dev/null +++ b/.github/cross-toolchain/cross-toolchain.sh @@ -0,0 +1,560 @@ +#!/usr/bin/env bash +# Which of 0113's target triples this runner can compile C for (#291). +# +# WHAT THIS IS FOR. 0243 decides that a certificate is validated through +# `rustls`. Every crypto provider `rustls` offers is C. The `targets` leg beside +# this one compiles the library for seven triples on one Linux runner with no C +# cross-toolchain, so the first compile after that manifest entry arrives goes +# red on every triple whose C compiler the runner does not carry. 0291 weighed +# three ways out of that, took the wait, and named a measurement that would +# reverse it: the runner image carrying a working C cross-toolchain for every +# triple in that set, taken on the runner rather than on a contributor's +# machine. +# +# THAT MEASUREMENT HAD NEVER BEEN TAKEN. #291's body names it as the issue's own +# first act; 0291 says in its own text that it is unmade, that every +# cross-compile reading behind the question was taken on a Windows machine +# outside this tree, and that taking it needs a run on the runner rather than a +# claim about the image. This leg is that run, and it is the only thing in this +# gate whose subject is the image rather than the tree. +# +# IT REPORTS AND REFUSES NOTHING ABOUT WHAT IT FOUND. A triple this runner +# cannot compile C for is the finding rather than a failure: it is the state +# 0291 already decided to wait through, and a leg reddening on every pull +# request for a decision already taken teaches people that red means nothing. +# Two runs in this gate already report without refusing and say so; this is the +# third and it says so here. +# +# WHAT IT DOES REFUSE is a survey that cannot be read. A block missing a field, +# a block naming a field this loader does not know, a triple in 0113's set that +# no block covers, a block naming a triple that set does not carry, a derived +# count that disagrees with the blocks it came from, and a register naming +# nothing. Each of those exits zero and prints a page indistinguishable from a +# run that examined seven triples and found them all served, which is the one +# reading this leg must never produce. +# +# THE SET IS NOT READ TWICE. The triples come from `.github/targets/targets` +# through the `triples` verb of `.github/targets/targets.sh`, which is the same +# reader the `targets` leg applies and the one whose fixtures prove it. A second +# reader of that register here would agree with it until the day it did not. +# +# A PROBE COMPILES A REAL TRANSLATION UNIT and never merely resolves a name. A +# compiler driver targets any triple it knows without a sysroot for as long as +# nothing asks for a header, so a probe that stopped at `command -v` would +# report every Apple triple served on a Linux image carrying no Apple SDK. The +# unit below includes the C library headers for that reason, which is the same +# thing a crypto provider's generated sources fail on when they fail. +# +# Verbs: +# selftest run every fixture and prove each rule bites +# check read the register, then compile the unit for every triple in it +# +# No POSIX character classes and no interval expressions in any pattern below, +# for the reason `.github/targets/targets.sh` gives: the awk on the runner is +# mawk and the awk on a contributor's machine is frequently gawk, and a rule +# that matches on one and not the other is a verdict that depends on who ran it. + +set -euo pipefail + +REGISTER=".github/cross-toolchain/probes" +TARGETS=".github/targets/targets.sh" +TAB="$(printf '\t')" + +# -------------------------------------------------------------------------- +# Rules. Each reads its subject on stdin and writes records to stdout, one per +# line. +# +# A block is a run of field lines. A blank line ends one and so does a comment, +# so the register can argue for itself between blocks without a comment being +# read into the block below it. +# +# awk rather than grep: grep exits 1 when it selects nothing, and "this register +# holds no incomplete block" is a legitimate answer this script has to tell +# apart from a scanner that broke. +# -------------------------------------------------------------------------- + +# The triple and the probe of every block carrying all three fields, tab +# separated, in the order the register writes them. +declared_probes() { + awk ' + function flush() { + if (fields > 0 && triple != "" && probe != "" && reason != "") { + printf "%s\t%s\n", triple, probe + } + triple = ""; probe = ""; reason = ""; fields = 0 + } + { + line = $0 + sub(/\r$/, "", line) + gsub(/^[ \t]+|[ \t]+$/, "", line) + if (line == "" || substr(line, 1, 1) == "#") { flush(); next } + + sep = index(line, ":") + if (sep == 0) { fields = fields + 1; next } + name = substr(line, 1, sep - 1) + value = substr(line, sep + 1) + gsub(/^[ \t]+|[ \t]+$/, "", name) + gsub(/^[ \t]+|[ \t]+$/, "", value) + fields = fields + 1 + if (name == "triple") triple = value + else if (name == "probe") probe = value + else if (name == "reason") reason = value + } + END { flush() } + ' +} + +# One record per block that is missing one of the three fields, as the triple it +# names followed by the fields it lacks. A block naming no triple is reported by +# its position, because there is nothing else to call it by. +blocks_missing_a_field() { + awk ' + function flush() { + if (fields > 0) { + block = block + 1 + missing = "" + if (triple == "") missing = "triple" + if (probe == "") missing = (missing == "" ? "probe" : missing ",probe") + if (reason == "") missing = (missing == "" ? "reason" : missing ",reason") + if (missing != "") { + printf "%s\t%s\n", (triple == "" ? "block " block : triple), missing + } + } + triple = ""; probe = ""; reason = ""; fields = 0 + } + { + line = $0 + sub(/\r$/, "", line) + gsub(/^[ \t]+|[ \t]+$/, "", line) + if (line == "" || substr(line, 1, 1) == "#") { flush(); next } + + sep = index(line, ":") + if (sep == 0) { fields = fields + 1; next } + name = substr(line, 1, sep - 1) + value = substr(line, sep + 1) + gsub(/^[ \t]+|[ \t]+$/, "", name) + gsub(/^[ \t]+|[ \t]+$/, "", value) + fields = fields + 1 + if (name == "triple") triple = value + else if (name == "probe") probe = value + else if (name == "reason") reason = value + } + END { flush() } + ' +} + +# The name of every field this loader does not know, one per line. A field +# spelled slightly wrong leaves its block incomplete, which the rule above +# reports; this one says which word was wrong rather than which value is absent. +unknown_fields() { + awk ' + { + line = $0 + sub(/\r$/, "", line) + gsub(/^[ \t]+|[ \t]+$/, "", line) + if (line == "" || substr(line, 1, 1) == "#") next + + sep = index(line, ":") + if (sep == 0) { print line; next } + name = substr(line, 1, sep - 1) + gsub(/^[ \t]+|[ \t]+$/, "", name) + if (name != "triple" && name != "probe" && name != "reason") print name + } + ' +} + +# How many blocks the register offers at all, read so that a loader which +# stopped matching cannot report an empty set and pass. +raw_blocks() { + awk ' + function flush() { if (fields > 0) n = n + 1; fields = 0 } + { + line = $0 + sub(/\r$/, "", line) + gsub(/^[ \t]+|[ \t]+$/, "", line) + if (line == "" || substr(line, 1, 1) == "#") { flush(); next } + fields = fields + 1 + } + END { flush(); printf "%d\n", n } + ' +} + +# -------------------------------------------------------------------------- +# selftest +# +# Every fixture judges its own text rather than the register in this tree. A +# fixture that judged the real one would prove the state of the tree on the day +# it ran, not the rule. +# -------------------------------------------------------------------------- + +selftest_failures=0 + +assert_out() { + local what="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then + printf 'ok %s\n' "$what" + else + printf 'FAIL %s\n expected: %s\n actual: %s\n' \ + "$what" "$(printf '%s' "$expected" | tr '\n\t' '|>')" "$(printf '%s' "$actual" | tr '\n\t' '|>')" + selftest_failures=$((selftest_failures + 1)) + fi +} + +judge_declared() { printf '%s' "$1" | declared_probes; } +judge_missing() { printf '%s' "$1" | blocks_missing_a_field; } +judge_unknown() { printf '%s' "$1" | unknown_fields; } +judge_raw() { printf '%s' "$1" | raw_blocks; } + +selftest() { + echo "== a block the survey runs ==" + assert_out "reads: a block carrying all three fields" \ + "$(printf 'aarch64-apple-ios\tclang --target=arm64-apple-ios')" \ + "$(judge_declared 'triple: aarch64-apple-ios +probe: clang --target=arm64-apple-ios +reason: The invocation a C build for that triple reaches for. +')" + assert_out "reads: two blocks separated by a blank line, in the order written" \ + "$(printf 'aarch64-apple-ios\tclang --target=arm64-apple-ios\nx86_64-unknown-linux-gnu\tcc')" \ + "$(judge_declared 'triple: aarch64-apple-ios +probe: clang --target=arm64-apple-ios +reason: The phone. + +triple: x86_64-unknown-linux-gnu +probe: cc +reason: The host. +')" + assert_out "reads: two blocks naming one triple, because a triple is served where any probe compiles" \ + "$(printf 'aarch64-linux-android\tclang --target=aarch64-linux-android21\naarch64-linux-android\taarch64-linux-android21-clang')" \ + "$(judge_declared 'triple: aarch64-linux-android +probe: clang --target=aarch64-linux-android21 +reason: The driver form. + +triple: aarch64-linux-android +probe: aarch64-linux-android21-clang +reason: The wrapper the NDK installs. +')" + assert_out "reads: a probe carrying a colon of its own, which stays in the value" \ + "$(printf 'x86_64-unknown-linux-gnu\tcc -DA=b:c')" \ + "$(judge_declared 'triple: x86_64-unknown-linux-gnu +probe: cc -DA=b:c +reason: A definition with a colon in it is a value and not a second field. +')" + assert_out "reads: a block indented by somebody tidying the file" \ + "$(printf 'x86_64-unknown-linux-gnu\tcc')" \ + "$(judge_declared ' triple: x86_64-unknown-linux-gnu + probe: cc + reason: The host. +')" + + echo "== what is not one ==" + assert_out "passes over: a comment, which is where the register argues for itself" \ + "" \ + "$(judge_declared '# triple: aarch64-apple-ios is not a block when it is prose. +# probe: clang +# reason: none. +')" + assert_out "ends a block: a comment between two of them, so the prose is not read into the second" \ + "$(printf 'x86_64-unknown-linux-gnu\tcc')" \ + "$(judge_declared 'triple: aarch64-apple-ios +probe: clang --target=arm64-apple-ios +# the reason for the block above went missing here +triple: x86_64-unknown-linux-gnu +probe: cc +reason: The host. +')" + assert_out "does not read as declared: a block whose reason is whitespace" \ + "" \ + "$(judge_declared "$(printf 'triple: x86_64-unknown-linux-gnu\nprobe: cc\nreason: \t \n')")" + + echo "== the rule that refuses a block nobody finished ==" + assert_out "refuses: a block with no reason, naming the triple and the field" \ + "$(printf 'x86_64-unknown-linux-gnu\treason')" \ + "$(judge_missing 'triple: x86_64-unknown-linux-gnu +probe: cc +')" + assert_out "refuses: a block with no probe" \ + "$(printf 'aarch64-apple-tvos\tprobe')" \ + "$(judge_missing 'triple: aarch64-apple-tvos +reason: The television. +')" + assert_out "refuses: a block with no triple, by its position, since there is nothing to call it by" \ + "$(printf 'block 1\ttriple')" \ + "$(judge_missing 'probe: cc +reason: The host. +')" + assert_out "refuses: the unfinished block and not the finished neighbour one line away" \ + "$(printf 'aarch64-apple-tvos\treason')" \ + "$(judge_missing 'triple: x86_64-unknown-linux-gnu +probe: cc +reason: The host. + +triple: aarch64-apple-tvos +probe: clang --target=arm64-apple-tvos +')" + assert_out "passes over: a block carrying all three" \ + "" \ + "$(judge_missing 'triple: x86_64-unknown-linux-gnu +probe: cc +reason: The host. +')" + + echo "== the rule that names the word that was spelled wrong ==" + assert_out "refuses: a field this loader does not know" \ + "reasons" \ + "$(judge_unknown 'triple: x86_64-unknown-linux-gnu +probe: cc +reasons: The plural is the mistake somebody makes. +')" + assert_out "refuses: a line carrying no colon at all" \ + "probe cc" \ + "$(judge_unknown 'triple: x86_64-unknown-linux-gnu +probe cc +reason: The host. +')" + assert_out "passes over: the three fields it knows" \ + "" \ + "$(judge_unknown 'triple: x86_64-unknown-linux-gnu +probe: cc +reason: The host. +')" + assert_out "passes over: a comment naming a field that does not exist" \ + "" \ + "$(judge_unknown '# reasons: a comment is not a field line +')" + + echo "== the count that catches a loader which stopped matching ==" + assert_out "counts: every run of field lines the register offers" \ + "2" \ + "$(judge_raw '# the register argues for itself here +triple: x86_64-unknown-linux-gnu +probe: cc +reason: The host. + +triple: aarch64-apple-ios +probe: clang --target=arm64-apple-ios +reason: The phone. +')" + assert_out "counts: an unfinished block, which is a block and a refused one" \ + "1" \ + "$(judge_raw 'triple: x86_64-unknown-linux-gnu +')" + assert_out "counts: nothing in a register that is all prose" \ + "0" \ + "$(judge_raw '# nothing here is a block +# and neither is this +')" + + echo + if [ "$selftest_failures" -ne 0 ]; then + echo "::error::$selftest_failures cross-toolchain fixture(s) did not hold. The rules below are not the rules that were proven, so this run measures nothing." + return 1 + fi + echo "Every fixture held. The rules the survey applies are the rules these fixtures ran." +} + +# -------------------------------------------------------------------------- +# check +# -------------------------------------------------------------------------- + +say() { + echo "$1" + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + echo "$1" >> "$GITHUB_STEP_SUMMARY" + fi +} + +# The translation unit every probe compiles. It asks for the C library headers +# rather than only for the ones a freestanding compiler supplies itself, because +# a driver reaches an unknown platform's triple without a sysroot for as long as +# nothing needs a header, and a unit that needed none would report a +# cross-toolchain wherever a driver exists. +write_the_unit() { + cat > "$1" <<'THE_UNIT' +/* Compiled by .github/cross-toolchain/cross-toolchain.sh for one triple at a + time. It asks for the platform's own C library headers on purpose: that is + what a crypto provider's generated sources ask for, and it is what a + compiler without that platform's sysroot cannot answer. */ +#include +#include +#include +#include + +size_t the_length_a_provider_would_copy(const uint8_t *bytes, size_t length) { + void *copy = malloc(length); + if (copy == NULL) { + return 0; + } + memcpy(copy, bytes, length); + free(copy); + return length; +} +THE_UNIT +} + +check() { + local declared missing unknown raw counted refused wanted triple probe name + local fields binary output unserved served=0 total=0 failures=0 workspace unit + local this_one_is_served + local argv=() + + if [ ! -f "$REGISTER" ]; then + echo "::error::${REGISTER} is not here. This survey reads what it invokes out of that file, and an absent register is not a register naming no probe." + return 1 + fi + if [ ! -f "$TARGETS" ]; then + echo "::error::${TARGETS} is not here. The set of triples is that script's answer rather than this one's, so without it this run has nothing to survey and nothing to say it left out." + return 1 + fi + + declared="$(declared_probes < "$REGISTER")" + missing="$(blocks_missing_a_field < "$REGISTER")" + unknown="$(unknown_fields < "$REGISTER")" + raw="$(raw_blocks < "$REGISTER")" + counted="$(printf '%s' "$declared" | grep -c . || true)" + refused="$(printf '%s' "$missing" | grep -c . || true)" + + echo "-- the register, read with its reasons" + echo " ${REGISTER}" + sed 's/^/ /' "$REGISTER" + echo + + if [ -n "$unknown" ]; then + while IFS= read -r name; do + [ -n "$name" ] || continue + echo "::error::${REGISTER} carries a line this loader does not read as a field: ${name}. A field spelled wrong leaves its block short of a value, and the block is then a probe nobody argued for." + done < /dev/null 2>&1; then + echo " ABSENT ${probe}" + echo " ${binary} is not on this image's PATH." + continue + fi + echo " RUN ${probe} -c .c -o .o" + if "${argv[@]}" -c "$unit" -o "$output" > "${workspace}/out" 2>&1; then + echo " COMPILED ${probe}" + this_one_is_served=1 + else + echo " FAILED ${probe}" + head -8 "${workspace}/out" | sed 's/^/ /' + fi + done < ${triple}: a C compiler on this image compiles for it." + else + unserved="${unserved}${triple} " + echo " => ${triple}: no probe on this image compiled for it." + fi + echo + done <&2; exit 2 ;; +esac diff --git a/.github/cross-toolchain/probes b/.github/cross-toolchain/probes new file mode 100644 index 0000000..90ceacf --- /dev/null +++ b/.github/cross-toolchain/probes @@ -0,0 +1,86 @@ +# The C compiler a build for each triple in 0113's set would reach for (#291). +# +# 0243 decides that a certificate is validated through `rustls`, every crypto +# provider `rustls` offers is C, and the target leg compiles the library for +# seven triples on one Linux runner. So the first compile after that manifest +# entry arrives goes red on every triple whose C compiler the runner does not +# carry. 0291 took the wait rather than the cross-toolchain, and named the +# reading below as one of the things that would reverse it: +# +# The runner image is measured carrying a working C cross-toolchain for +# every triple in that set, on the runner rather than on a contributor's +# machine. +# +# That measurement had never been taken. #291's body names it as the issue's own +# first act, and 0291 says in its own text that it is unmade and that taking it +# needs a run on the runner rather than a claim about the image. This register +# and the script beside it are that run. +# +# WHAT IS MEASURED IS A COMPILE AND NOT A PATH. A probe is invoked on a real +# translation unit that includes the C library headers, because a compiler +# driver targets any triple it knows without a sysroot as long as nothing asks +# for a header. A probe that only asked whether a binary is on PATH would report +# every Apple triple served on a Linux runner carrying no Apple SDK, which is +# the false positive this whole question exists to avoid. +# +# NOTHING HERE REFUSES A TRIPLE THE RUNNER CANNOT SERVE. That is the finding, it +# is what 0291 waits on, and a leg that reddened for it would redden on every +# pull request for a decision that is already taken and written down. What is +# refused is a survey that cannot be read: a block missing a field, a triple in +# 0113's set that no block covers, a block naming a triple that set does not +# carry, and a run that examined nothing. +# +# The fields, and the loader refuses a block missing any of them: +# +# triple the triple from `.github/targets/targets` this probe is for. Every +# triple in that register is covered by at least one block and no +# block names a triple outside it, in both directions. +# probe the compiler invocation, minus the input and the output, which the +# script completes with `-c .c -o .o`. Several blocks may +# name one triple; the triple is served where any of them compiles. +# reason why this is the invocation a C build for that triple would reach +# for. A probe nobody argued for measures whatever it happens to run. +# +# A BLOCK IS NOT A CLAIM THAT THE COMPILER IS THERE. It is a claim about what +# would be reached for if it were, and the run is what says which of them +# answered. + +triple: aarch64-linux-android +probe: clang --target=aarch64-linux-android21 +reason: The driver form the Android NDK's own wrapper expands to, with the API level the wrapper carries in its name. cc-rs reaches for the wrapper first and for this second, and both fail identically without the NDK sysroot. + +triple: aarch64-linux-android +probe: aarch64-linux-android21-clang +reason: The NDK's own wrapper, which is what is on PATH where the NDK is installed and its toolchain directory exported. It is named separately from the driver form above so that an image carrying the NDK is not reported short because the plain driver found no sysroot. + +triple: armv7-linux-androideabi +probe: clang --target=armv7a-linux-androideabi21 +reason: The 32-bit Android ABI. The C triple is `armv7a-linux-androideabi` where the Rust triple is `armv7-linux-androideabi`, which is the one place in this register the two spellings differ, and a probe written with the Rust spelling would measure a triple no Android compiler knows. + +triple: armv7-linux-androideabi +probe: armv7a-linux-androideabi21-clang +reason: The NDK's wrapper for the same ABI, for the reason the second Android block above gives. + +triple: aarch64-apple-ios +probe: clang --target=arm64-apple-ios +reason: The invocation cc-rs builds for this triple, spelled the way a C driver spells it. An Apple SDK is not on the driver's default search path anywhere but macOS, so this block is the one that says whether the runner carries one. + +triple: aarch64-apple-tvos +probe: clang --target=arm64-apple-tvos +reason: Apple's television platform, and the same question as the block above with a second SDK behind it. It is a separate block rather than an assumption that one Apple answer covers both, because an image may carry one platform SDK and not another. + +triple: aarch64-apple-darwin +probe: clang --target=arm64-apple-darwin +reason: The macOS desktop on Apple silicon. It is the Apple triple most likely to be served by a plain driver, since a macOS header set is the one an Apple toolchain installs first, so measuring it separately is what stops the three Apple entries being read as one verdict. + +triple: x86_64-pc-windows-msvc +probe: clang-cl --target=x86_64-pc-windows-msvc +reason: The MSVC-compatible driver, which is the only one of the two forms that can exist on a Linux image at all. It needs the Windows SDK headers, which is exactly what this run asks about. + +triple: x86_64-pc-windows-msvc +probe: cl +reason: The Microsoft compiler itself, which is what cc-rs reaches for on this triple. It exists on the Windows images and not on the Linux one, and naming it here is what makes the verdict for this triple a reading rather than an assumption about which images carry it. + +triple: x86_64-unknown-linux-gnu +probe: cc +reason: The host triple of the runner the target leg runs on, where the C compiler is the image's own and no cross-toolchain is involved. It is in the register rather than excused from it, because a survey that skipped the case it expects to pass has no control in it. diff --git a/.github/targets/targets.sh b/.github/targets/targets.sh index 006fc19..22f6224 100644 --- a/.github/targets/targets.sh +++ b/.github/targets/targets.sh @@ -42,6 +42,7 @@ # Verbs: # selftest run every fixture and prove each rule bites # check read the register, then compile the library for every triple in it +# triples print the set, for the leg beside this one that surveys it # # No POSIX character classes and no interval expressions in any pattern below. # The awk on the runner is mawk and the awk on a contributor's machine is @@ -338,8 +339,21 @@ TRIPLES echo "Every triple the register names compiles." } +# The set itself, for a leg beside this one that has to survey the same triples. +# It is a verb here rather than a second reader there, because two readers of one +# register agree until the day they do not, and the disagreement then shows up as +# a platform one leg reports on and the other does not. +triples() { + if [ ! -f "$REGISTER" ]; then + echo "::error::${REGISTER} is not here." >&2 + return 1 + fi + declared_triples < "$REGISTER" +} + case "${1:-}" in selftest) selftest ;; check) selftest && echo && check ;; - *) echo "usage: $0 selftest|check" >&2; exit 2 ;; + triples) triples ;; + *) echo "usage: $0 selftest|check|triples" >&2; exit 2 ;; esac diff --git a/.github/workflows/cross-toolchain.yml b/.github/workflows/cross-toolchain.yml new file mode 100644 index 0000000..498fa27 --- /dev/null +++ b/.github/workflows/cross-toolchain.yml @@ -0,0 +1,78 @@ +# Which of 0113's target triples this runner can compile C for (#291). +# +# WHY THIS RUNS AT ALL. 0243 decides a certificate is validated through +# `rustls`, every crypto provider `rustls` offers is C, and `targets` beside +# this compiles the library for seven triples on one Linux runner. 0291 weighed +# the cross-toolchain, the dropped triple and the wait, took the wait, and wrote +# down that it did so without ever measuring which C cross-compilers this image +# carries - every reading behind that question was taken on a contributor's +# Windows machine. This job is that measurement, taken where it has to be taken. +# +# IT REFUSES NOTHING ABOUT WHAT IT FINDS. A triple this image cannot compile C +# for is the state 0291 already decided to wait through, so a red tick for it +# would redden every pull request for a decision that is already taken. What the +# script refuses is a survey that cannot be read, and it says so on every run. +# `External addresses in documents` and `Scorecard analysis` are the two runs +# already in this gate that report without refusing; this is the third. +# +# IT IS THE ONLY CHECK HERE WHOSE SUBJECT IS THE IMAGE. Every other one judges +# the tree, and a tree judged today is the tree tomorrow. An image gains and +# loses a toolchain on somebody else's schedule, so this verdict is this run's +# and nothing about it is carried forward: the answer is re-measured on every +# pull request and on every push to the default branch, and the day it changes +# is the day 0291's second reversal condition is met or lost. +# +# The check-run name is exactly `cross-toolchain`, on both the workflow and the +# job, for the reason `.github/workflows/targets.yml` gives: GitHub takes the +# name from the job's `name:` and falls back to the job id, so a rename here +# would silently detach anything that came to require it. +# +# No toolchain is installed. Nothing here invokes cargo or rustc: the set of +# triples is read out of `.github/targets/targets` through that leg's own +# reader, and everything else is a C compiler this image either carries or does +# not. +name: cross-toolchain + +on: + pull_request: + branches: ["**"] + types: [opened, synchronize, reopened] + push: + branches: [main] + +# Deny at the workflow level and grant per job, so a job added later starts with +# nothing rather than with what this one needs. +permissions: {} + +concurrency: + # Namespaced on the workflow name rather than the bare word, for the reason + # #178 recorded: a group string two workflows share means the run created + # second cancels the other, and the gate that dies that way leaves a green tick + # beside no verdict. + group: cross-toolchain-workflow-${{ github.ref }} + cancel-in-progress: true + +jobs: + cross-toolchain: + name: cross-toolchain + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read # check out the register and the script that reads it + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes, so do not leave the token in .git/config. + persist-credentials: false + + # Printed rather than assumed. The verdict below is about this image, so + # which image it was is part of the reading rather than context for it. + - name: Say which image this reading was taken on + run: | + echo "runner image: ${ImageOS:-unknown} ${ImageVersion:-unknown}" + echo "kernel: $(uname -srm)" + cat /etc/os-release + + - name: Prove the fixtures, then compile one unit for every triple the register names + run: bash .github/cross-toolchain/cross-toolchain.sh check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 18bbbea..b24102a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -282,12 +282,33 @@ fixtures that prove them. It is not a third member of the pair below: those two return zero whatever they found, and this one refuses a run that produced no report. -Two runs report and refuse nothing, which is deliberate rather than an oversight. -**`External addresses in documents`** requests the addresses documents name and -prints what answered; an address outside this repository that is down for an hour -is not a defect here, and a gate that reddens for it teaches people that red means -nothing. **`Scorecard analysis`** scores the repository and writes to the -code-scanning surface. +**`cross-toolchain`** is the one check here whose subject is the runner image +rather than this tree. It compiles one C translation unit for every triple in +0113's set, using the invocations named in `.github/cross-toolchain/probes`, and +reports which of them this image can compile C for. 0243 validates a certificate +through `rustls`, every crypto provider `rustls` offers is C, and 0291 chose to +wait rather than to buy a cross-toolchain or to drop a client platform - and said +in its own text that it made that choice without this measurement, every reading +behind the question having been taken on a contributor's Windows machine. This +leg is that measurement, and it is re-taken on every run because an image gains +and loses a toolchain on somebody else's schedule. The set is not read twice: the +triples come from the `targets` leg's own reader through +`bash .github/targets/targets.sh triples`. +`.github/cross-toolchain/cross-toolchain.sh` holds the rules and the fixtures +that prove them. + +THREE RUNS REPORT AND REFUSE NOTHING, which is deliberate rather than an +oversight, AND THIS PARAGRAPH SAID TWO. **`External addresses in documents`** +requests the addresses documents name and prints what answered; an address +outside this repository that is down for an hour is not a defect here, and a gate +that reddens for it teaches people that red means nothing. **`Scorecard +analysis`** scores the repository and writes to the code-scanning surface. The +third is `cross-toolchain` above, and what it declines to refuse is narrower than +either: it refuses a survey that cannot be read - a register block missing a +field, a triple in 0113's set no block covers, a probe for a triple that set does +not name - and never the finding that a triple has no compiler here, because that +is the state 0291 already decided to wait through and a red tick for it would +redden every pull request for a decision already taken. ## Which of these is a gate, and which is a sentence