Skip to content

Commit 1da7696

Browse files
committed
ci: apply the branch substitution and the engine under review to every job
The host-dimension job resolved this ecosystem from the index and installed the released engine. Both were wrong for a change that spans these repositories, and the first announced itself: E_NOT_FOUND: package 'compat.openkal-musl@0.4.0' not found in the synced index, synced 0 seconds ago The second would not have. A manifest key the engine does not recognise is accepted rather than refused, so that job would have built manifests written for an unreleased engine using a released one, and the semantics the new key asks for would simply be absent from the result. The remedy is not a second copy of the procedure. Two copies that must agree are two copies that will not, which is how one job came to have neither. Both are now scripts that each job calls: tools/install-mcpp.sh the engine this run is judged by tools/branch-graph.sh the working trees of this ecosystem, in place of the versions the manifests name Three consequences follow from the extraction. The substituted paths are now relative. An absolute path names a directory of one machine; a manifest carrying one has been committed in this ecosystem and published, and every consumer resolving it was handed a path that exists nowhere. Relative paths cannot express that mistake. They are also the only form that works unchanged on all three hosts, since `pwd` under MSYS reports `/d/a/...`. Measured before adoption: a dependency named by a relative path resolves against the manifest that contains it rather than against the working directory, at three levels of nesting, and the substituted graph of six repositories resolves with no absolute path anywhere in it. In-place editing no longer uses `sed -i`. BSD sed, which is macOS's, reads the argument after -i as a backup suffix, so the command that edits a file on Linux consumes the next expression on macOS --- and this script now runs on macOS. The engine on PATH is compared against the engine that was built. GITHUB_PATH governs the steps that follow, so the step that appends cannot observe its own effect, and whether the appended spelling is one the runner accepts is a property of the runner. Unasserted, a cross-validation run on a host that ignored it would build with the released engine and report the change under review as tested.
1 parent c21b352 commit 1da7696

3 files changed

Lines changed: 276 additions & 150 deletions

File tree

.github/workflows/ci.yml

Lines changed: 61 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -51,64 +51,31 @@ jobs:
5151
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
5252
5353
- name: Install mcpp
54+
run: bash tools/install-mcpp.sh
55+
56+
# ⭐ THE ENGINE EVERY STEP BELOW WILL USE, COMPARED AGAINST THE ONE BUILT.
57+
#
58+
# The step above appends a directory to GITHUB_PATH, which governs the
59+
# steps that follow it, so that step cannot observe its own effect. Whether
60+
# the appended spelling is one the runner accepts is a property of the
61+
# runner and differs between hosts. Left unasserted, a cross-validation run
62+
# on a host that ignores it builds this ecosystem with the released engine
63+
# and reports the result as though the change under review had been tested.
64+
- name: The engine on PATH is the one under review
5465
run: |
55-
# THE PIN MAY NAME A RELEASE THIS RUN IS VALIDATING, which does not
56-
# exist yet --- that is what MCPP_SOURCE_REF is for. Bootstrap from
57-
# whatever the index has; the block below replaces it with the build
58-
# under review, and the pin is what an ordinary run tests.
59-
if [ -n "${MCPP_SOURCE_REF:-}" ]; then
60-
xlings update > /dev/null 2>&1 || true
61-
xlings install mcpp -y -g
62-
else
63-
for attempt in 1 2 3 4 5 6; do
64-
xlings update > /dev/null 2>&1 || true
65-
if xlings install "mcpp@$MCPP_VERSION" -y -g; then break; fi
66-
if [ "$attempt" = 6 ]; then
67-
echo "::error::mcpp@$MCPP_VERSION never appeared in the index"; exit 1
68-
fi
69-
sleep 60
70-
done
66+
set -euo pipefail
67+
if [ -z "${MCPP_UNDER_REVIEW:-}" ]; then
68+
echo " no source reference: this run tests $(mcpp --version)"
69+
exit 0
7170
fi
72-
mcpp --version
73-
mcpp self config --mirror GLOBAL
74-
75-
# CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE.
76-
#
77-
# Empty in the ordinary run, so this job keeps testing the released
78-
# mcpp the pin above names. Set it --- through the workflow_dispatch
79-
# input or the repository variable --- and the same job runs against
80-
# that source.
81-
#
82-
# THIS REPOSITORY HAD NO SUCH MECHANISM WHILE EVERY OTHER ONE IN THE
83-
# ECOSYSTEM DID, so an engine change could be validated against seven
84-
# repositories and not against the one that carries a C++ standard
85-
# library. That is the repository where a change to how a target side
86-
# is assembled shows first, because a standard library is configured
87-
# for one C library and compiled against its headers.
88-
#
89-
# The released mcpp installed above is the bootstrap that compiles it;
90-
# mcpp builds itself and there is no other compiler for it here.
91-
if [ -n "${MCPP_SOURCE_REF:-}" ]; then
92-
src="$RUNNER_TEMP/mcpp-src"
93-
[ -d "$src" ] || git clone --quiet --depth 1 \
94-
--branch "$MCPP_SOURCE_REF" \
95-
https://github.com/mcpp-community/mcpp.git "$src"
96-
# The clone's own workspace pin must not decide which mcpp builds it.
97-
# `.xlings.json` at mcpp's root pins the mcpp that compiles mcpp and
98-
# does not move when mcpp is released, so a build inside the checkout
99-
# obeys it and tries to install a version the index may no longer
100-
# carry. What is wanted is the source compiled by the mcpp installed
101-
# above, which is what removing the file leaves.
102-
rm -f "$src/.xlings.json"
103-
( cd "$src" && mcpp build --release )
104-
built=$(find "$src/target" -type f \
105-
\( -name mcpp -o -name mcpp.exe \) | head -1)
106-
[ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; }
107-
echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH"
108-
# Reported, because a PATH entry that does not win looks exactly like
109-
# one that does until something is built with the wrong engine.
110-
echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)"
71+
on_path=$(mcpp --version | awk '{print $2}')
72+
if [ "$on_path" != "$MCPP_UNDER_REVIEW" ]; then
73+
echo "::error::PATH resolves mcpp $on_path, and the build under review is $MCPP_UNDER_REVIEW"
74+
echo " the directory appended to GITHUB_PATH did not take effect on this host"
75+
command -v mcpp
76+
exit 1
11177
fi
78+
echo " every step below runs $on_path, built from $MCPP_SOURCE_REF"
11279
11380
- name: Select the toolchain
11481
run: |
@@ -117,91 +84,9 @@ jobs:
11784
mcpp toolchain default "$spec"
11885
11986
# THE C LIBRARY THIS RUNTIME IS CONFIGURED FOR, AS WRITTEN ON THE BRANCH
120-
# UNDER TEST RATHER THAN AS PUBLISHED.
121-
#
122-
# This package declares openkal-musl by version, which is what a published
123-
# manifest must say. A change that spans the two repositories cannot be
124-
# tested that way: the version named here does not exist in the index until
125-
# the other half is released, and the run fails with
126-
#
127-
# E_NOT_FOUND: package 'openkal-musl@<version>' not found in the
128-
# synced index
129-
#
130-
# --- which reads as a mistake in this manifest and is nothing of the kind.
131-
#
132-
# The other repositories in this ecosystem solve it by substituting a
133-
# working tree, and this one now does the same. The substitution is
134-
# asserted rather than assumed: one that matched nothing would leave the
135-
# manifest naming a version, the resolver would fetch a published C
136-
# library, and the run would report on that one while appearing to report
137-
# on this branch.
138-
- name: The C library, as written on this branch
139-
run: |
140-
set -euo pipefail
141-
branch='${{ github.head_ref || github.ref_name }}'
142-
git clone --quiet https://github.com/mcpplibs/openkal-musl.git .musl
143-
if git -C .musl rev-parse --verify --quiet "origin/$branch" > /dev/null; then
144-
git -C .musl checkout --quiet "origin/$branch"
145-
echo "openkal-musl is at $branch $(git -C .musl rev-parse --short HEAD)"
146-
else
147-
echo "openkal-musl has no $branch; its default branch is used" \
148-
"($(git -C .musl rev-parse --short HEAD))"
149-
fi
150-
151-
# openkal-musl reaches the specification by whatever its own manifest
152-
# says. That becomes the same working tree, so that the whole stack
153-
# under test is the one written on this branch.
154-
git clone --quiet https://github.com/mcpplibs/openkal.git .spec
155-
if git -C .spec rev-parse --verify --quiet "origin/$branch" > /dev/null; then
156-
git -C .spec checkout --quiet "origin/$branch"
157-
fi
158-
159-
# ⚠️ EVERY BACKEND THE C LIBRARY NAMES, DISCOVERED RATHER THAN LISTED.
160-
#
161-
# openkal-musl names a backend per target: linux, macos, windows and
162-
# opensbi, each conditional. Their versions all move with a change that
163-
# spans these repositories, so each one left unsubstituted fails the
164-
# same way, one link further down:
165-
#
166-
# E_NOT_FOUND: package 'openkal-linux@0.6.0' (the host build)
167-
# E_NOT_FOUND: package 'openkal-opensbi@0.2.0' (the bare-metal one)
168-
#
169-
# I fixed the first by naming it, and the second appeared. A list
170-
# written by hand is a list that is one entry short, so the set is read
171-
# out of the manifest instead.
172-
for backend in $(grep -oE '^openkal-[a-z]+ = \{ version' .musl/mcpp.toml | cut -d' ' -f1); do
173-
git clone --quiet "https://github.com/mcpplibs/$backend.git" ".$backend"
174-
if git -C ".$backend" rev-parse --verify --quiet "origin/$branch" > /dev/null; then
175-
git -C ".$backend" checkout --quiet "origin/$branch"
176-
fi
177-
here="$(cd ".$backend" && pwd)"
178-
# The backend reaches the specification too, and by whatever form its
179-
# own manifest uses.
180-
sed -i -E "s|^openkal = .*$|openkal = { path = \"$(cd .spec && pwd)\" }|" ".$backend/mcpp.toml"
181-
sed -i -E "s|^$backend = \\{ version = \"[^\"]*\"(.*)$|$backend = { path = \"$here\"\\1|" \
182-
.musl/mcpp.toml
183-
grep -q "path = \"$here\"" .musl/mcpp.toml \
184-
|| { echo "::error::$backend was not substituted"; exit 1; }
185-
echo " $backend -> working tree"
186-
done
187-
188-
spec="$(cd .spec && pwd)"
189-
musl="$(cd .musl && pwd)"
190-
sed -i -E "s|^openkal = .*$|openkal = { path = \"$spec\" }|" .musl/mcpp.toml
191-
sed -i -E "s|^openkal-musl = .*$|openkal-musl = { path = \"$musl\" }|" mcpp.toml
192-
193-
grep -q "path = \"$musl\"" mcpp.toml \
194-
|| { echo "::error::the C library substitution matched nothing"; exit 1; }
195-
grep -q "path = \"$spec\"" .musl/mcpp.toml \
196-
|| { echo "::error::the specification substitution matched nothing"; exit 1; }
197-
198-
# ⚠️ THE LAST CHECK IS THE ONE THAT WOULD HAVE CAUGHT BOTH FAILURES:
199-
# nothing anywhere in the substituted graph still names a version.
200-
if grep -nE '^openkal[a-z-]* = (\"|\{ version)' .musl/mcpp.toml mcpp.toml; then
201-
echo "::error::something in the graph still names a version rather than a tree"
202-
exit 1
203-
fi
204-
echo "the whole stack names working trees, and nothing in it names a version"
87+
# UNDER TEST RATHER THAN AS PUBLISHED. The reasoning is in the script.
88+
- name: The stack, as written on this branch
89+
run: bash tools/branch-graph.sh '${{ github.head_ref || github.ref_name }}'
20590

20691
- name: The runtime builds
20792
run: mcpp build
@@ -581,23 +466,49 @@ jobs:
581466
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
582467
583468
- name: Install mcpp
469+
run: bash tools/install-mcpp.sh
470+
471+
# ⭐ THE ENGINE EVERY STEP BELOW WILL USE, COMPARED AGAINST THE ONE BUILT.
472+
#
473+
# The step above appends a directory to GITHUB_PATH, which governs the
474+
# steps that follow it, so that step cannot observe its own effect. Whether
475+
# the appended spelling is one the runner accepts is a property of the
476+
# runner and differs between hosts. Left unasserted, a cross-validation run
477+
# on a host that ignores it builds this ecosystem with the released engine
478+
# and reports the result as though the change under review had been tested.
479+
- name: The engine on PATH is the one under review
584480
run: |
585-
for attempt in 1 2 3 4 5 6; do
586-
xlings update > /dev/null 2>&1 || true
587-
if xlings install "mcpp@$MCPP_VERSION" -y -g; then break; fi
588-
if [ "$attempt" = 6 ]; then
589-
echo "::error::mcpp@$MCPP_VERSION never appeared in the index"
590-
exit 1
591-
fi
592-
sleep 60
593-
done
594-
mcpp self config --mirror GLOBAL
481+
set -euo pipefail
482+
if [ -z "${MCPP_UNDER_REVIEW:-}" ]; then
483+
echo " no source reference: this run tests $(mcpp --version)"
484+
exit 0
485+
fi
486+
on_path=$(mcpp --version | awk '{print $2}')
487+
if [ "$on_path" != "$MCPP_UNDER_REVIEW" ]; then
488+
echo "::error::PATH resolves mcpp $on_path, and the build under review is $MCPP_UNDER_REVIEW"
489+
echo " the directory appended to GITHUB_PATH did not take effect on this host"
490+
command -v mcpp
491+
exit 1
492+
fi
493+
echo " every step below runs $on_path, built from $MCPP_SOURCE_REF"
494+
495+
# ⭐ THE SAME ENGINE AND THE SAME STACK AS THE LINUX JOB, FROM A DIFFERENT
496+
# HOST. This job had neither: it installed the released engine and
497+
# resolved this ecosystem from the index, so a change spanning these
498+
# repositories was validated on one host of three and reported as
499+
# validated everywhere.
500+
- name: Select the toolchain
501+
run: |
502+
set -euo pipefail
595503
# ⚠️ INSTALL, THEN SELECT. `toolchain default` names a toolchain and
596504
# does not fetch one, so selecting an absent payload fails with
597505
# `llvm@22.1.8 is not installed` — measured on both rows of this job.
598506
mcpp toolchain install llvm 22.1.8
599507
mcpp toolchain default 'llvm@22.1.8'
600508
509+
- name: The stack, as written on this branch
510+
run: bash tools/branch-graph.sh '${{ github.head_ref || github.ref_name }}'
511+
601512
- name: Every target, from this host
602513
run: |
603514
set -euo pipefail

tools/branch-graph.sh

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env bash
2+
#
3+
# THE ECOSYSTEM AS WRITTEN ON A BRANCH, RATHER THAN AS PUBLISHED.
4+
#
5+
# This package declares openkal-musl by version, which is what a published
6+
# manifest must say. A change that spans the two repositories cannot be tested
7+
# that way: the version named here does not exist in the index until the other
8+
# half is released, and the run fails with
9+
#
10+
# E_NOT_FOUND: package 'openkal-musl@<version>' not found in the synced index
11+
#
12+
# --- which reads as a mistake in this manifest and is nothing of the kind. The
13+
# remedy is to put the working trees in place of the versions, for the whole
14+
# graph rather than for its first edge.
15+
#
16+
# ⚠️ WHY THIS IS A SCRIPT AND NOT A STEP. It was a step, in one job of two, and
17+
# the other job resolved from the index and failed exactly as above the moment
18+
# the versions moved. Two copies of a procedure that must agree are two copies
19+
# that will not; one file called twice cannot drift.
20+
#
21+
# ⚠️ THE SUBSTITUTED PATHS ARE RELATIVE, AND DELIBERATELY SO.
22+
#
23+
# An absolute path names a directory of one machine. A manifest carrying one has
24+
# been committed in this ecosystem and published, and every consumer resolving
25+
# it was handed a path that exists nowhere. Relative paths cannot express that
26+
# mistake. They are also the only form that works unchanged on all three hosts:
27+
# `pwd` under MSYS reports `/d/a/...`, which is not a path the engine resolves,
28+
# so an absolute form would need a Windows-only conversion here.
29+
#
30+
# ⚠️ `sed -i` IS NOT PORTABLE. BSD sed, which is macOS's, reads the argument
31+
# after -i as a backup suffix; the same command that edits a file on Linux
32+
# consumes the next expression on macOS. In-place editing goes through a
33+
# temporary file below for that reason.
34+
set -euo pipefail
35+
36+
branch="${1:-}"
37+
[ -n "$branch" ] || { echo "usage: ${0##*/} <branch>" >&2; exit 2; }
38+
39+
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
40+
cd "$root"
41+
42+
# Clones a repository of this ecosystem, preferring the branch under test when
43+
# that repository has one of the same name.
44+
fetch() {
45+
local repo="$1" dir="$2"
46+
rm -rf "$dir"
47+
git clone --quiet "https://github.com/mcpplibs/$repo.git" "$dir"
48+
if git -C "$dir" rev-parse --verify --quiet "origin/$branch" > /dev/null; then
49+
git -C "$dir" checkout --quiet "origin/$branch"
50+
printf ' %-22s %s %s\n' "$repo" "$branch" "$(git -C "$dir" rev-parse --short HEAD)"
51+
else
52+
printf ' %-22s default branch %s (it has no %s)\n' \
53+
"$repo" "$(git -C "$dir" rev-parse --short HEAD)" "$branch"
54+
fi
55+
}
56+
57+
# In-place editing that both GNU and BSD sed perform identically.
58+
edit() {
59+
local file="$1" expr="$2"
60+
sed -E "$expr" "$file" > "$file.substituted"
61+
mv "$file.substituted" "$file"
62+
}
63+
64+
echo "the stack under test:"
65+
fetch openkal-musl .musl
66+
fetch openkal .spec
67+
68+
manifests=(mcpp.toml .musl/mcpp.toml)
69+
70+
# ⚠️ EVERY BACKEND THE C LIBRARY NAMES, DISCOVERED RATHER THAN LISTED.
71+
#
72+
# openkal-musl names a backend per target --- linux, macos, windows, opensbi ---
73+
# each conditional on the target it serves. Their versions all move with a change
74+
# that spans these repositories, so each one left unsubstituted fails the same
75+
# way, one link further down:
76+
#
77+
# E_NOT_FOUND: package 'openkal-linux@0.6.0' (the host build)
78+
# E_NOT_FOUND: package 'openkal-opensbi@0.2.0' (the bare-metal one)
79+
#
80+
# The first was fixed by naming it, and the second appeared. A list written by
81+
# hand is a list that is one entry short, so the set is read out of the manifest.
82+
for backend in $(grep -oE '^openkal-[a-z]+ = \{ version' .musl/mcpp.toml | cut -d' ' -f1); do
83+
fetch "$backend" ".$backend"
84+
85+
# The backend reaches the specification too, by whatever form its own
86+
# manifest uses. From <root>/.<backend>/ the specification is ../.spec.
87+
edit ".$backend/mcpp.toml" 's|^openkal = .*$|openkal = { path = "../.spec" }|'
88+
89+
edit .musl/mcpp.toml \
90+
"s|^$backend = \\{ version = \"[^\"]*\"(.*)\$|$backend = { path = \"../.$backend\"\\1|"
91+
grep -q "path = \"../.$backend\"" .musl/mcpp.toml \
92+
|| { echo "::error::$backend was not substituted"; exit 1; }
93+
94+
manifests+=(".$backend/mcpp.toml")
95+
done
96+
97+
edit .musl/mcpp.toml 's|^openkal = .*$|openkal = { path = "../.spec" }|'
98+
edit mcpp.toml 's|^openkal-musl = .*$|openkal-musl = { path = "./.musl" }|'
99+
100+
# The substitution is asserted rather than assumed. One that matched nothing
101+
# would leave the manifest naming a version, the resolver would fetch a
102+
# published C library, and the run would report on that one while appearing to
103+
# report on this branch.
104+
grep -q 'path = "./.musl"' mcpp.toml \
105+
|| { echo "::error::the C library substitution matched nothing"; exit 1; }
106+
grep -q 'path = "../.spec"' .musl/mcpp.toml \
107+
|| { echo "::error::the specification substitution matched nothing"; exit 1; }
108+
109+
# ⭐ THE CHECK THAT WOULD HAVE CAUGHT EVERY FAILURE ABOVE AT ITS FIRST OCCURRENCE:
110+
# nothing anywhere in the substituted graph still names a version. The three
111+
# defects this file records were each found by a build failing one link further
112+
# down than the last; this asks the whole graph at once.
113+
if grep -nE '^openkal[a-z-]* = ("|\{ version)' "${manifests[@]}"; then
114+
echo "::error::something in the graph still names a version rather than a tree"
115+
exit 1
116+
fi
117+
118+
echo "the whole stack names working trees; nothing in it names a version"

0 commit comments

Comments
 (0)