-
Notifications
You must be signed in to change notification settings - Fork 0
328 lines (296 loc) · 16.4 KB
/
Copy pathci.yml
File metadata and controls
328 lines (296 loc) · 16.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
name: ci
# What this checks that a plain `mcpp build` does not:
#
# - BOTH toolchains. The llvm leg uses lld and no sysroot, and in this
# ecosystem it has repeatedly caught things gcc structurally cannot see.
# - that upstream/ is the release tarball byte for byte. Everything this fork
# adds lives under mcpp/.
# - that the GENERATED headers say what they are supposed to. Five of them
# are produced by hand-reimplemented generators, and a wrong answer in
# glibconfig.h does not fail to compile — it changes the width of gsize or
# which printf glib calls. The tests assert the behaviour; this step
# asserts the text, because the two fail differently.
on:
push:
branches: [ '**' ]
pull_request:
workflow_dispatch:
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
toolchain: [default, llvm]
name: build (${{ matrix.toolchain }})
steps:
- uses: actions/checkout@v4
# XLINGS_NON_INTERACTIVE: without it the installer takes its `curl | bash`
# branch (`xlings self install < /dev/tty`) and a runner's /dev/tty is
# readable but not connected.
#
# XLINGS_VERSION pins the INSTALLER so a red run can be told apart from an
# ecosystem change. mcpp itself stays unpinned: this package depends on
# the current ecosystem, and a pinned mcpp carries a frozen snapshot.
#
# The second PATH entry is the one that matters: `mcpp` is installed INTO
# THE SUBOS, so without subos/default/bin the next step dies with a bare
# `mcpp: command not found`.
- name: Install xlings + mcpp
env:
XLINGS_NON_INTERACTIVE: "1"
XLINGS_VERSION: "2026.8.27.5"
run: |
curl -fsSL https://d2learn.org/xlings-install.sh | bash
echo "$HOME/.xlings/bin" >> "$GITHUB_PATH"
"$HOME/.xlings/bin/xlings" --version
"$HOME/.xlings/bin/xlings" install mcpp
echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH"
# A clean runner carries the index snapshot of the day its image was
# built, so a package published since then is simply absent. This fork
# needs compat.pcre2, which is new.
- name: refresh the index
run: mcpp index update
# `toolchain default` alone fails with "llvm@ is not installed": the
# runner image carries only the default toolchain.
- name: select toolchain
if: matrix.toolchain == 'llvm'
run: |
mcpp toolchain install llvm
mcpp toolchain default llvm
- name: glib
run: cd mcpp/glib && mcpp test
- name: gobject
run: cd mcpp/gobject && mcpp test
- name: gmodule
run: cd mcpp/gmodule && mcpp test
- name: gio
run: cd mcpp/gio && mcpp test
# Not "did it build" — what the generators actually wrote. Each of these
# would compile and link if it were wrong.
#
# ⚠️ EACH MEMBER HAS ITS OWN include/. That is the fix for a silent
# failure (see mcpp/common/generators.h), and it means the four shared
# outputs are checked in every member's tree rather than in one place —
# which is also how a divergence between them would be caught.
- name: the generated headers say what they should
run: |
cd mcpp
for m in glib gobject gmodule gio; do
echo "--- $m/include ---"
find "$m/include" -type f | sort
# glibconfig.h: the public type model. Every member needs it.
grep -qx '#define GLIB_SIZEOF_VOID_P 8' "$m/include/glibconfig.h" \
|| { echo "::error::$m: glibconfig.h does not describe an LP64 target"; exit 1; }
grep -qx '#define G_BYTE_ORDER G_LITTLE_ENDIAN' "$m/include/glibconfig.h" \
|| { echo "::error::$m: glibconfig.h byte order is wrong for this runner"; exit 1; }
# G_GNUC_INTERNAL lives in glibconfig.h, not gmacros.h. Omitting it is
# how gmodule-deprecated.c failed with `unknown type name`.
grep -q 'G_GNUC_INTERNAL' "$m/include/glibconfig.h" \
|| { echo "::error::$m: glibconfig.h is missing G_GNUC_INTERNAL"; exit 1; }
# gversionmacros.h: one block per even minor up to the current one.
n=$(grep -c '^#define GLIB_VERSION_2_' "$m/include/glib/gversionmacros.h")
[ "$n" -ge 41 ] || { echo "::error::$m: only $n version macros"; exit 1; }
grep -q 'GLIB_VERSION_2_82' "$m/include/glib/gversionmacros.h" \
|| { echo "::error::$m: gversionmacros.h stops before the current version"; exit 1; }
# config.h: two macros with almost the same name, and only one of
# them selects anything.
grep -qx '#define USE_SYSTEM_PRINTF 1' "$m/include/config.h" \
|| { echo "::error::$m: config.h would select glib's bundled gnulib printf"; exit 1; }
# ⚠️ `^#define`, not a bare grep: config.h EXPLAINS in a comment why
# these are absent, and a loose match would find the explanation.
for u in HAVE_ISSETUGID HAVE_LCHMOD HAVE_GETFSSTAT HAVE_SYSCTLBYNAME HAVE_XLOCALE_H; do
grep -qE "^#define +$u" "$m/include/config.h" \
&& { echo "::error::$m: $u is DEFINED; it is #ifdef-tested and must be absent"; exit 1; }
done
done
# ── the two that belong to ONE member each ────────────────────────
# glib-enumtypes: the mkenums reimplementation. Four enums, and the
# count is asserted because a silent drop produces a library missing
# g_unicode_script_get_type and the failure lands in a consumer.
t=$(grep -c '_get_type (void)' gobject/include/gobject/glib-enumtypes.h)
echo "generated enum types: $t"
[ "$t" -eq 4 ] || { echo "::error::$t enum types, expected 4"; exit 1; }
grep -q 'g_unicode_script_get_type' gobject/include/gobject/glib-enumtypes.h \
|| { echo "::error::GUnicodeScript did not survive mkenums"; exit 1; }
# The nick is the fiddly half of the generator: prefix stripped, lower
# cased, underscores to hyphens.
grep -q '"han"' gobject/include/gobject/glib-enumtypes.c \
|| { echo "::error::the enum nicks are not what mkenums derives"; exit 1; }
# gmoduleconf.h: the loader selection. FLAT, because gmodule.c
# includes it by bare name and a subdirectory include_dirs entry that
# does not exist yet is silently dropped.
grep -q 'G_MODULE_IMPL_DL' gmodule/include/gmoduleconf.h \
|| { echo "::error::gmoduleconf.h did not select the dl loader"; exit 1; }
# ── gio: the same generator, twenty times the input ───────────────
# 82 GTypes, and the count is asserted for the same reason gobject's
# 4 is: mkenums drops silently, and a missing GType surfaces in a
# consumer rather than here.
g=$(grep -c '_get_type (void)' gio/include/gio/gioenumtypes.h)
echo "gio enum types: $g"
[ "$g" -eq 82 ] || { echo "::error::$g gio enum types, expected 82"; exit 1; }
# ⭐ THE MACRO NAME, checked separately from the function name.
# gnome.gobject 2.82.5 shipped `G_UNICODE_TYPE_TYPE` where upstream
# has `G_TYPE_UNICODE_TYPE`: mkenums builds @ENUMPREFIX@ from the TYPE
# name, not from the enumerator prefix. Every FUNCTION name was right,
# so it compiled, linked, and passed a test that checked the function
# and the nick. Only the macro was wrong, so only the macro finds it.
grep -qx '#define G_TYPE_FILE_TYPE (g_file_type_get_type ())' \
gio/include/gio/gioenumtypes.h \
|| { echo "::error::gioenumtypes.h macro names are not upstream's"; exit 1; }
# ENUM vs FLAGS is decided ONLY by a `/*< flags >*/` comment, and gio
# has exactly seven. Guessing from the name would make GConverterFlags
# a flags type; upstream's ABI says it is an enum.
f=$(grep -c 'g_flags_register_static' gio/include/gio/gioenumtypes.c)
[ "$f" -eq 7 ] || { echo "::error::$f flags types, expected 7"; exit 1; }
grep -q 'g_enum_register_static (g_intern_static_string ("GConverterFlags")' \
gio/include/gio/gioenumtypes.c \
|| { echo "::error::GConverterFlags has no /*< flags >*/ and must be an ENUM"; exit 1; }
# The seventeen nick overrides. Derived, this one would be "no-flags".
grep -q '"G_CONVERTER_NO_FLAGS", "none"' gio/include/gio/gioenumtypes.c \
|| { echo "::error::the /*< nick=… >*/ overrides were not applied"; exit 1; }
# gio includes its generated headers BOTH ways — 48 times as
# "gioenumtypes.h" and 3 as <gio/gioenumtypes.h>. The flat forwarder
# answers the first spelling.
grep -q '#include <gio/gioenumtypes.h>' gio/include/gioenumtypes.h \
|| { echo "::error::the flat gioenumtypes.h forwarder is missing"; exit 1; }
# ── the MODULE wrappers ───────────────────────────────────────────
# The namespace is a contract in this index: `compat.xxx` means
# headers, an owner namespace like `gnome.xxx` means `import`. These
# counts are floors rather than equalities — upstream adds API — but
# a COLLAPSE is what they are for, and every collapse this generator
# had was silent:
#
# a brace inside a char literal (`= '{'` in GVariantClass) left the
# typedef reader unbalanced and it swallowed the rest of the file
# G_DECLARE_INTERFACE/FINAL_TYPE expand to the typedef and
# `_get_type`, so a text scanner sees neither — GListModel,
# GListStore and g_list_model_get_type all went missing
# glibconfig.h is spelled without a `glib/` prefix, so deriving the
# header set from the umbrella missed gsize and gssize
#
# In every case the module still COMPILED. Only a consumer noticed.
for m in glib:1900 gobject:400 gmodule:12 gio:2400; do
name=${m%%:*}; floor=${m##*:}
n=$(grep -c '^using ::' "$name/src/$name.cppm")
echo "$name module exports: $n (floor $floor)"
[ "$n" -ge "$floor" ] || { echo "::error::$name exports only $n names; the scan collapsed"; exit 1; }
done
# The three names pango is waiting on, by name.
grep -qx 'using ::g_list_model_get_type;' gio/src/gio.cppm \
|| { echo "::error::gio's module is missing g_list_model_get_type"; exit 1; }
grep -qx 'using ::GListStore;' gio/src/gio.cppm \
|| { echo "::error::gio's module is missing GListStore"; exit 1; }
# An ENUMERATOR, which is the check that needed two toolchains: GCC
# makes them visible through an exported typedef and clang does not.
grep -qx 'using ::G_UNICODE_OTHER_LETTER;' glib/src/glib.cppm \
|| { echo "::error::enumerators are not exported; clang will reject consumers"; exit 1; }
# And the re-export chain, which is not optional: glib is a workspace
# PATH dependency, so a consumer cannot name it and MUST receive it
# through whichever member it does name.
grep -qx 'export import gnome.glib;' gio/src/gio.cppm \
|| { echo "::error::gio's module does not re-export gnome.glib"; exit 1; }
echo "all generated headers check out, in all four trees"
# A consumer naming MORE THAN ONE member is the case that caught the
# cross-package source reference: each alone worked, the pair did not.
# mcpp resolved the members' `../glib` path dependencies to ONE package,
# so only one member's include/ was ever written and the other's source
# entry matched nothing — no error, just an absent object.
#
# Three members now, because that is the shape a real consumer has:
# pango names gobject and gio, and gio pulls glib and gmodule behind it.
- name: a consumer naming gobject, gmodule AND gio links
run: |
d=$(mktemp -d); mkdir -p "$d/src"
cat > "$d/mcpp.toml" <<TOML
[package]
name = "trio"
version = "0.1.0"
standard = "c++23"
[target.'cfg(linux)'.dependencies.gnome]
gobject = { path = "$PWD/mcpp/gobject" }
gmodule = { path = "$PWD/mcpp/gmodule" }
gio = { path = "$PWD/mcpp/gio" }
[targets.trio]
kind = "bin"
main = "src/main.cpp"
TOML
cat > "$d/src/main.cpp" <<'CPP'
#include <glib-object.h>
#include <gmodule.h>
#include <gio/gio.h>
#include <gobject/glib-enumtypes.h>
#include <cstdio>
int main() {
// One symbol from each member's OWN generated code — which is
// exactly what a shared include/ would have lost.
const GType script = g_unicode_script_get_type(); // gobject
const GType ftype = G_TYPE_FILE_TYPE; // gio
const GType lmodel = G_TYPE_LIST_MODEL; // gio, the pango gate
std::printf("GUnicodeScript=%lu GFileType=%lu GListModel=%lu gmodule=%d\n",
(unsigned long)script, (unsigned long)ftype,
(unsigned long)lmodel, g_module_supported());
return (script != 0 && ftype != 0 && G_TYPE_IS_INTERFACE(lmodel)
&& g_module_supported()) ? 0 : 1;
}
CPP
cd "$d" && mcpp run
# ⭐ THE ONLY CHECKED-IN GENERATED CODE IN THIS FORK, AND THE ONLY ONE THAT
# NEEDS A JOB OF ITS OWN.
#
# Five of gio's six generators are C++ in build.mcpp, so they cannot go stale:
# they run on every build. The sixth is gdbus-codegen, 8,351 lines of Python,
# and its 15,392 lines of output are committed. Committed output has exactly
# one failure mode a build never sees — it can stop matching its input. Nobody
# notices, because it still compiles.
#
# So: regenerate and require a byte-identical tree. That also pins the claim
# the whole arrangement rests on — that the output is a PURE FUNCTION of the
# XML and the codegen version, with no host, target or locale in it.
codegen:
runs-on: ubuntu-latest
name: mcpp/generated/ still matches the XML it came from
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: regenerate and diff
run: |
mcpp/tools/gengdbus.sh python3
if ! git diff --exit-code --stat -- mcpp/generated; then
echo "::error::mcpp/generated/ is not what gdbus-codegen produces from upstream's XML. Run mcpp/tools/gengdbus.sh and commit the result."
git diff -- mcpp/generated | head -60
exit 1
fi
echo "mcpp/generated/ reproduces exactly"
# The script copies the codegen package out of the tree precisely so this
# holds; without that, config.py and __pycache__/ would both land in
# upstream/ and the job below would fail on a clean checkout.
- name: and upstream/ was only read
run: |
if ! git diff --exit-code --stat -- upstream; then
echo "::error::gengdbus.sh modified upstream/"; exit 1
fi
leaked=$(find upstream -name '__pycache__' -o -name 'config.py')
[ -z "$leaked" ] || { echo "::error::left behind in upstream/: $leaked"; exit 1; }
echo "upstream/ untouched"
upstream:
runs-on: ubuntu-latest
name: upstream/ is the release tarball, unmodified
steps:
- uses: actions/checkout@v4
- name: diff upstream/ against glib 2.82.5
run: |
curl -fsSL -o /tmp/u.tar.xz \
https://download.gnome.org/sources/glib/2.82/glib-2.82.5.tar.xz
mkdir -p /tmp/u && tar xf /tmp/u.tar.xz -C /tmp/u --strip-components=1
if ! diff -r -q /tmp/u upstream >/dev/null 2>&1; then
echo "::error::upstream/ differs from glib 2.82.5. Everything this fork adds belongs under mcpp/."
diff -r -q /tmp/u upstream | head -20
exit 1
fi
echo "upstream/ is 2.82.5, unmodified"