-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexport-kernel-tree.sh
More file actions
executable file
·1370 lines (1205 loc) · 69.6 KB
/
Copy pathexport-kernel-tree.sh
File metadata and controls
executable file
·1370 lines (1205 loc) · 69.6 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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
#
# export-kernel-tree.sh — render the carried kernel series as a Linux-Kernel_MiSTer-style
# git tree: a pristine upstream tarball as one base commit, then one commit per patch.
#
# WHY
# ---
# This repo keeps the MiSTer kernel as {pinned upstream version + hash} + an ordered
# patch series. MiSTer-devel/Linux-Kernel_MiSTer keeps it as a materialized git tree:
# a squashed tarball commit (`v5.15.1`) with MiSTer commits replayed on top. Those are
# the SAME MODEL — tarball base plus ordered series — differing only in whether the
# base is stored as a hash or as 283MB of blobs. This script renders one into the other.
#
# It exists so the rendered tree is a BUILD OUTPUT, not a second source of truth. Edits
# belong in the patch series; this regenerates from it. Given the same inputs it emits
# byte-identical commits (see DETERMINISM), so re-running after no change is a no-op
# rather than a force-push of fresh SHAs.
#
# TWO SERIES: WHAT WE SHIP vs WHAT THIS TREE CARRIES
# --------------------------------------------------
# For most of this script's life those were the same set, and EXPORT.md said so: the
# exported tree WAS the shipped kernel, patch for patch. That is no longer true, and the
# difference is deliberate rather than drift.
#
# Linux-Kernel_MiSTer is upstream's kernel for every MiSTer, not just ours. Some of what
# it must carry, our image specifically does not want. The motivating case is the fork's
# `loop=` boot parameter (fork commit 3d95de58f, "Support for init loop device."), which
# patches init/do_mounts.c so the KERNEL itself mounts /media/fat and loop-mounts
# linux/linux.img as the root filesystem. That is upstream's boot mechanism — every stock
# MiSTer boots through it, and a 6.18 branch that dropped it would not boot on any of
# them. Our image replaced it with a real initramfs /init, so applying it here would add
# an unreachable second boot path to the kernel we ship (recorded as carried-upstream-only
# in docs/kernel-recon/reconciliation.md — carried for this tree, not for our image).
#
# Deleting it from the export to keep the two trees identical would be the wrong trade:
# it would break upstream's boot to preserve a documentation claim. Applying it to our
# image would be the other wrong trade. So there are two series:
#
# board/mister/de10nano/linux-patches/ carried — applied by BOTH Buildroot
# (BR2_LINUX_KERNEL_PATCH) and this
# script. The kernel we ship.
# board/mister/de10nano/linux-patches-upstream/ upstream-only — applied ONLY here.
# Buildroot never sees this directory.
#
# The second directory's path is DERIVED from the first ("${patch_dir}-upstream"), so a
# defconfig change moves both together and this script needs no edit. Numbering there
# starts at 0100 so a filename alone says which namespace it is in.
#
# The cost of the split is that EXPORT.md can no longer say "this tree is the shipped
# kernel". It must say what it now is: the shipped kernel PLUS exactly these N patches,
# each named, each with the reason it is not in our image. That is generated below from
# the files actually present — never hardcoded — and the export FAILS CLOSED if a patch
# in that directory has no stated reason, because a table row with a blank reason is
# worse than no table: it reads as reviewed when nothing reviewed it.
#
# WHAT YOU GET
# ------------
# <output>/ a fresh git repo, branch MiSTer-v<major.minor>, containing:
# - one base commit "Linux <ver>" — pristine upstream, hash-verified
# - one commit per carried patch, original authorship preserved
# - one commit per upstream-only patch, likewise (see TWO SERIES above)
# - arch/arm/configs/MiSTer_defconfig — so the tree builds standalone:
# make ARCH=arm MiSTer_defconfig && make ARCH=arm zImage
# which is the thing `make linux` inside Buildroot cannot hand someone.
# - EXPORT.md — states it is generated, names the source of truth, and records
# the fork commit we last reconciled against.
# - tag mister-<ver>
#
# WHERE THE BRANCH HANGS (--parent-repo/--parent)
# -----------------------------------------------
# Linux-Kernel_MiSTer is not one chain. Its tarball commits form a SPINE —
#
# e12ed6c19 v5.13.12 -> 137491a75 v5.14 -> b6f2ca1c4 v5.14.5 -> aba1ef4c1 v5.15.1
#
# — and each MiSTer-vX.Y branch hangs off a spine point with the MiSTer series replayed
# on top (MiSTer-v5.15 = aba1ef4c1 + 112 commits). Every spine commit is a PRISTINE
# tarball with no MiSTer code in it.
#
# So the right shape for a new kernel is to extend the spine the same way, parenting the
# base commit on the newest spine point (aba1ef4c1) rather than on a branch tip:
#
# aba1ef4c1 v5.15.1 --+-- [112 MiSTer commits] --> MiSTer-v5.15 (theirs, untouched)
# |
# +-- v6.18.38 -- [our commits] -> MiSTer-v6.18
#
# That buys three things at once:
# - shared ancestry with MiSTer-v5.15, so GitHub can compare and a PR is possible at
# all (across unrelated histories the compare API 404s: "No common ancestor");
# - a log with NO MiSTer-5.15 commits in it — they are siblings, not ancestors — so
# nothing lists a change that is absent from the tree. Parenting on the branch TIP
# instead would list ~112 commits whose changes this tree discards, and a reader
# would see "xone: update driver" and conclude xone is present when it is a
# Buildroot package now;
# - a base commit whose diff against its parent is PURE upstream 5.15.1 -> 6.18.38,
# with zero MiSTer noise, because both trees are pristine.
#
# Their branch is never touched: it becomes a sibling, exactly as MiSTer-v5.14 already
# is. What each of its commits became — carried, superseded upstream, or dropped — is
# recorded in MISTER-KERNEL-PATCH-RECON.md, which cites the superseding vanilla commit.
# No git command can answer that: across this much context drift `git patch-id` matches
# nothing, so "is this commit in 6.18?" is semantic, not mechanical.
#
# Without --parent-repo the base commit is a root commit and the branch is an orphan —
# fine for a standalone tree, but it cannot be PR'd anywhere.
#
# This script NEVER touches a fork or a remote. To publish, fetch the orphan branch
# into a fork and push from there (see EXPORT.md, which spells out the two commands).
#
# DETERMINISM
# -----------
# Reproducibility comes from two choices:
# - `git am --committer-date-is-author-date`, so committer dates come from the
# patches rather than from the clock;
# - the base commit's date is the extracted Makefile's mtime. kernel.org tarballs are
# produced with `git archive`, so every file carries the tag's commit time — stable
# across machines and meaningful, unlike download time. Override with
# SOURCE_DATE_EPOCH.
#
# Usage: scripts/export-kernel-tree.sh --output DIR [--parent-repo R --parent C]
# [--onto COMMIT] [--fork-sync SHA] [--tarball FILE]
# [--upstream-patches DIR] [--no-upstream-patches]
#
# --output DIR where to build the tree (must not already exist)
# --parent-repo R clone R and work inside it, rather than starting a fresh root
# --parent C spine commit to extend; a base commit is created on top of it
# from the pinned tarball (requires --parent-repo)
# --onto COMMIT replay onto COMMIT, which must ALREADY BE the pinned kernel
# version -- no base commit is created and the tarball is not
# used for the kernel. Use when upstream has published its own
# vanilla base to PR against; the result fast-forwards onto it.
# Mutually exclusive with --parent (requires --parent-repo).
# --fork-sync SHA fork commit this export was reconciled against; recorded in
# EXPORT.md as the backport-queue starting point
# --tarball FILE use this tarball instead of the dl/ cache or a download
# --upstream-patches DIR
# the upstream-only series to replay after the carried one, instead
# of the derived default "<BR2_LINUX_KERNEL_PATCH>-upstream". These
# patches are NOT in the shipped image (see TWO SERIES above). An
# explicitly named directory must exist and be non-empty; the DERIVED
# one may be absent or empty, which simply means there are none.
# --no-upstream-patches
# skip the upstream-only series entirely. The result is exactly the
# kernel the image ships — useful for diffing this tree against a
# Buildroot build, where the extra patches are the only expected
# difference and so make the comparison useless. Not what you want
# for a tree you intend to publish. Mutually exclusive with
# --upstream-patches.
#
# Exit: 0 = tree built and verified; non-zero = anything failed (fails closed).
set -o errexit
set -o nounset
set -o pipefail
# Assigned then marked readonly separately: `readonly X="$(cmd)"` masks cmd's exit status
# (shellcheck SC2155), and the rest of scripts/ avoids that pattern.
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly REPO_ROOT
readonly DEFCONFIG="$REPO_ROOT/configs/fragments/de10nano.fragment"
readonly HASH_FILE="$REPO_ROOT/board/mister/de10nano/patches/linux/linux.hash"
# Committer identity for the generated commits. Patch AUTHORS are preserved by `git am`;
# this only says who mechanically produced the tree, and it must be explicit so the
# script works on a runner with no git config.
readonly EXPORT_NAME="${EXPORT_COMMITTER_NAME:-MiSTer Buildroot export}"
readonly EXPORT_EMAIL="${EXPORT_COMMITTER_EMAIL:-export@mister-devel.invalid}"
die() { printf 'export-kernel-tree: %s\n' "$*" >&2; exit 1; }
say() { printf '\n=== %s\n' "$*"; }
# Only set when we download rather than use the dl/ cache. Cleaned on exit: it holds a
# ~150MB kernel tarball, so leaking it on every run is not a rounding error. --output is
# deliberately NOT touched here -- it is the deliverable, and it must survive a failure
# for the failure to be diagnosable.
#
# cleanup() uses `if` rather than `[[ ... ]] && rm`: as an EXIT trap, the function's own
# return status becomes the script's exit status, and a bare `[[ -n $download_dir ]]`
# returns 1 whenever nothing was downloaded -- making every successful cache-hit run exit
# 1 despite printing PASS.
# scratch_dir holds `git mailinfo` output while we read the upstream-only patches'
# Subject: lines; it is tiny but there is no reason to leak one per run.
download_dir=''
scratch_dir=''
cleanup() {
if [[ -n $download_dir ]]; then
rm -rf "$download_dir"
fi
if [[ -n $scratch_dir ]]; then
rm -rf "$scratch_dir"
fi
}
trap cleanup EXIT
output=''
fork_sync=''
tarball_override=''
parent_repo=''
parent=''
onto=''
upstream_patch_dir=''
skip_upstream=false
while (($#)); do
case "$1" in
--output) output="${2:-}"; shift 2 ;;
--parent-repo) parent_repo="${2:-}"; shift 2 ;;
--parent) parent="${2:-}"; shift 2 ;;
--onto) onto="${2:-}"; shift 2 ;;
--fork-sync) fork_sync="${2:-}"; shift 2 ;;
--tarball) tarball_override="${2:-}"; shift 2 ;;
--upstream-patches) upstream_patch_dir="${2:-}"; shift 2 ;;
--no-upstream-patches) skip_upstream=true; shift ;;
# `q` on the closing line, not a bare range. A sed range RE-ARMS after it closes, and
# this file contains a SECOND "# Usage:" -- the one in the build-mister-modules.sh
# heredoc emitted in section 6c. Without the quit, the range reopened there, found no
# second "# Exit:", and ran to end of file: --help printed ~300 lines of this script's
# own source after the banner. Quitting at the first "# Exit:" prints the banner and
# only the banner, regardless of what later sections contain.
-h | --help)
sed -n '/^# Usage:/,/^# Exit:/{p;/^# Exit:/q;}' "${BASH_SOURCE[0]}" |
sed 's/^# \?//'
exit 0
;;
*) die "unknown argument: $1 (try --help)" ;;
esac
done
[[ -n $output ]] || die 'missing --output DIR (try --help)'
[[ ! -e $output ]] || die "--output already exists: $output"
# A parent is meaningless without the repo it lives in, and cloning a repo without saying
# where to hang the branch would silently fall back to an orphan.
[[ -n $parent_repo && -z $parent && -z $onto ]] && die '--parent-repo requires --parent or --onto'
[[ -n $parent && -z $parent_repo ]] && die '--parent requires --parent-repo'
[[ -n $onto && -z $parent_repo ]] && die '--onto requires --parent-repo'
[[ -n $parent && -n $onto ]] && die '--parent and --onto are mutually exclusive:
--parent extends a spine and CREATES a base commit from the tarball; --onto replays onto
a base that already exists. Pick one.'
# Naming a directory and then asking for it to be skipped is not a resolvable intent, and
# guessing either way would silently produce a tree the caller did not ask for -- one of
# which (the skipped one) is missing upstream's boot path.
[[ -n $upstream_patch_dir ]] && $skip_upstream &&
die '--upstream-patches and --no-upstream-patches are mutually exclusive.'
# --- 1. Read the pinned inputs out of the defconfig -----------------------------------
# The defconfig is the single source of truth for what we build; nothing here is
# hardcoded, so a version bump is a one-line defconfig edit and this script follows.
defconfig_value() {
# Values look like: BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.18.38"
#
# Do NOT anchor on the closing quote. This defconfig carries trailing comments on
# some lines, and anchoring silently yields an empty value rather than failing --
# which for an optional setting (a config fragment) would mean quietly dropping it.
sed -n "s/^$1=\"\([^\"]*\)\".*$/\1/p" "$DEFCONFIG" | tail -1
}
# Buildroot spells the external tree's own path as a make variable inside the defconfig;
# resolve it the way Buildroot would.
resolve_br_path() {
printf '%s' "${1//\$(BR2_EXTERNAL_MISTER_PATH)/$REPO_ROOT}"
}
[[ -f $DEFCONFIG ]] || die "no defconfig at $DEFCONFIG"
version="$(defconfig_value BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE)"
[[ -n $version ]] || die 'BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE not set in defconfig'
patch_dir="$(resolve_br_path "$(defconfig_value BR2_LINUX_KERNEL_PATCH)")"
config_file="$(resolve_br_path "$(defconfig_value BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE)")"
fragments="$(resolve_br_path "$(defconfig_value BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES)")"
[[ -d $patch_dir ]] || die "patch dir not found: $patch_dir"
[[ -f $config_file ]] || die "kernel config not found: $config_file"
# nullglob, or an empty patch dir yields an array holding the literal "*.patch" pattern
# -- length 1, so the guard below passes -- and `git am` then fails on a path that does
# not exist, blaming the patch rather than the empty directory.
shopt -s nullglob
series=("$patch_dir"/*.patch)
shopt -u nullglob
((${#series[@]})) || die "no patches in $patch_dir"
# --- 1b. The upstream-only series ------------------------------------------------------
# Patches this tree carries that the shipped image deliberately does not. See TWO SERIES
# at the top of this file for why that divergence exists and why it is not drift.
#
# The default path is DERIVED from the carried series rather than written out, for the
# same reason nothing else here is hardcoded: BR2_LINUX_KERNEL_PATCH is the one place
# that says where kernel patches live, and a second hardcoded copy of that path would go
# stale the first time the defconfig moved -- silently, by finding no directory and
# exporting a tree with upstream's boot mechanism quietly missing from it.
upstream_explicit=false
if [[ -n $upstream_patch_dir ]]; then
upstream_explicit=true
# Absolutize NOW, before anything globs it. The glob below runs in the invocation
# cwd, but `git am` runs after the `cd "$output"` in section 4 -- so a RELATIVE
# --upstream-patches yields relative paths that are unopenable by the time they are
# applied, and git am's failure is reported as patch rot. That sends the operator off
# to rebase a patch that was never broken. The carried series is immune only by
# accident: it goes through resolve_br_path(), which substitutes an absolute
# $REPO_ROOT. Left `-d`-guarded so a missing directory still hits the typo-vs-empty
# die below rather than failing here with a worse message.
if [[ -d $upstream_patch_dir ]]; then
upstream_patch_dir="$(cd "$upstream_patch_dir" && pwd)" ||
die "--upstream-patches: cannot resolve directory: $upstream_patch_dir"
fi
else
upstream_patch_dir="${patch_dir}-upstream"
fi
upstream_series=()
if $skip_upstream; then
say 'Skipping the upstream-only series (--no-upstream-patches)'
upstream_patch_dir=''
elif [[ -d $upstream_patch_dir ]]; then
shopt -s nullglob
upstream_series=("$upstream_patch_dir"/*.patch)
shopt -u nullglob
elif $upstream_explicit; then
# An explicitly named directory that is not there is a typo, not an empty series.
# Treating it as empty would export a tree missing exactly the patches the caller
# went out of their way to ask for, and report PASS.
die "--upstream-patches: no such directory: $upstream_patch_dir"
fi
# Same rule for an explicitly named directory that exists but holds nothing: the caller
# asked for a series, so producing none is a failure. The DERIVED directory is different
# -- absent or empty there legitimately means "there are no upstream-only patches", which
# is the state this repo was in before the loop= patch existed.
$upstream_explicit && ((${#upstream_series[@]} == 0)) &&
die "--upstream-patches: no *.patch files in $upstream_patch_dir"
# Read each upstream-only patch's Subject: and its reason for not being in the image, up
# front, BEFORE the tarball download and the whole series replay. A missing reason is a
# hard failure (see below), and discovering that after ten minutes of work would train
# people to skip the export rather than fix the patch.
#
# `git mailinfo` is used rather than a regex because it is the parser `git am` itself
# uses: it strips the "[PATCH 1/1] " prefix, unfolds continuation lines and decodes
# RFC2047-encoded headers, so the table below shows the same subject the commit will
# actually carry. scripts/lint-kernel-patches.sh checks the same thing in CI.
readonly NOT_IN_IMAGE_FILE='not-in-image'
upstream_subjects=()
upstream_reasons=()
if ((${#upstream_series[@]})); then
scratch_dir="$(mktemp -d "${TMPDIR:-/tmp}/export-kernel-tree-meta.XXXXXX")" ||
die 'could not create a temporary directory'
for up_patch in "${upstream_series[@]}"; do
up_name="$(basename "$up_patch")"
up_info="$(git mailinfo "$scratch_dir/msg" "$scratch_dir/patch" <"$up_patch" 2>/dev/null)" ||
die "git mailinfo could not parse $up_name. Run scripts/lint-kernel-patches.sh."
# The exit status above is NOT the signal for a malformed identity, and relying on
# it would leave the fast gate half-open. `git mailinfo` exits 0 while leaving
# Author/Email EMPTY for a `From:` it cannot parse -- which is why
# scripts/lint-kernel-patches.sh checks the fields rather than the status, and why
# that script exists at all: this repo shipped exactly that defect once
# (0013-hid-flydigi-vader.patch carried `From: Alexey Melnikov` with no <email>).
#
# `git am` hard-fails on it later with "fatal: empty ident name (for <>) not
# allowed" -- but "later" here means after the tarball download, the hash verify,
# the clone, the extract and a 31-patch replay, and the failure arrives wearing the
# generic series-replay error instead of naming the file and the line. Checking all
# three fields the same way the lint does keeps this gate honest: everything `git am`
# needs from the headers is validated before any expensive work starts.
#
# DUPLICATED ON PURPOSE -- KEEP IN SYNC WITH scripts/lint-kernel-patches.sh
# --------------------------------------------------------------------------
# The non-empty Author/Email/Subject criteria below are the same three checks
# lint-kernel-patches.sh makes (see the `problems+=(...)` block there). They are
# duplicated rather than shared because the two scripts have no common library and
# sourcing one from the other would couple a CI-only linter to the export's runtime.
# That is a deliberate trade, not an oversight: the cost is that a change to the
# criteria HERE must be mirrored THERE, or CI and the export start disagreeing about
# what a valid patch header is -- and the failure mode is a green lint followed by a
# failed export, which is the exact confusion this gate exists to prevent.
# If a third caller ever needs these checks, factor all three into a shared helper
# instead of adding another copy.
up_subject="$(sed -n 's/^Subject: //p' <<<"$up_info")"
up_author="$(sed -n 's/^Author: //p' <<<"$up_info")"
up_email="$(sed -n 's/^Email: //p' <<<"$up_info")"
[[ -n $up_subject ]] ||
die "no Subject: in $up_name — it would appear as a blank row in EXPORT.md's
upstream-only table, and 'git am' would have no commit message to write."
[[ -n $up_author && -n $up_email ]] ||
die "unparseable From: in $up_name — 'git am' needs \`Name <email>\` to write a
commit and dies with \"fatal: empty ident name (for <>) not allowed\".
got: $(grep -m1 '^From:' "$up_patch" || echo '(no From: line at all)')
Run scripts/lint-kernel-patches.sh, which checks both series the same way."
# WHY A REASON IS MANDATORY
# -------------------------
# EXPORT.md tells upstream reviewers, in a table, which patches are in this tree
# but not in the MiSTer image and why. A row with an empty reason is worse than
# no table at all: it has the shape of a reviewed decision without being one, and
# it is exactly the kind of claim that goes unchallenged for years. So the reason
# is an input to the export, not prose someone remembers to add afterwards.
#
# Two places it can come from, in this order:
#
# 1. a `Not-in-image:` line in the patch's own commit message — preferred,
# because it travels with the patch through rebases and re-exports;
# 2. a row in the series directory's `not-in-image` file, keyed by filename —
# for patches imported VERBATIM from the fork, where editing the commit
# message would mean rewriting someone else's commit text just to satisfy
# a tool of ours.
#
# Read from the mailinfo-split message body, not the raw file: grepping the raw
# patch would also match a `Not-in-image:` string inside a diff hunk.
up_reason="$(sed -n 's/^Not-in-image:[[:space:]]*//p' "$scratch_dir/msg" | head -1)"
if [[ -z $up_reason && -f "$upstream_patch_dir/$NOT_IN_IMAGE_FILE" ]]; then
# awk with an exact first-field match rather than sed: the key is a
# filename full of '.' and '-', which sed would read as a regex, and a
# near-miss would silently match the wrong row. Exact equality cannot.
# It also skips '#' comment lines for free -- their first field is the
# comment, which is never a patch filename.
up_reason="$(awk -v key="$up_name" \
'$1 == key { $1 = ""; sub(/^[[:space:]]+/, ""); print; exit }' \
"$upstream_patch_dir/$NOT_IN_IMAGE_FILE")"
fi
[[ -n $up_reason ]] || die "no stated reason why $up_name is absent from the MiSTer image.
Every patch in $upstream_patch_dir is carried for the exported
tree ONLY, and EXPORT.md publishes a table naming each one and why the image does not
apply it. Refusing to emit that table with a blank row. Add either:
* a line 'Not-in-image: <one-line reason>' to the patch's commit message, or
* a row '$up_name <one-line reason>' to
$upstream_patch_dir/$NOT_IN_IMAGE_FILE"
upstream_subjects+=("$up_subject")
upstream_reasons+=("$up_reason")
done
fi
branch="MiSTer-v${version%.*}" # 6.18.38 -> MiSTer-v6.18, matching the fork's convention
tag="mister-${version}"
say "Exporting Linux $version + ${#series[@]} carried patches + ${#upstream_series[@]} upstream-only -> $output (branch $branch)"
# --- 2. Get the tarball, and verify it against the signed-manifest hash ----------------
# Fails closed: an unverified kernel tarball is the whole reason linux.hash exists.
tarball="$tarball_override"
if [[ -n $onto ]]; then
# --onto: the base already exists upstream, so the kernel tarball is not needed and
# no base commit is created. The safety property still has to hold, though -- replaying
# a 6.18 series onto, say, a 5.15 base must not be attempted -- so the version is read
# back out of the target commit's own Makefile below rather than trusted.
say "Replaying onto existing base $onto (no base commit created)"
elif [[ -z $tarball ]]; then
cached="$REPO_ROOT/dl/linux/linux-$version.tar.xz"
if [[ -f $cached ]]; then
tarball="$cached"
say "Using cached tarball: $tarball"
else
# Explicit template, matching scripts/ci-tests.sh and scripts/check-linux-img.sh:
# bare `mktemp -d` is a GNU extension and errors out on BSD/macOS mktemp, which
# wants one. (`-t` is not the answer either -- GNU deprecates it and BSD reads
# its argument as a prefix rather than a template.)
download_dir="$(mktemp -d "${TMPDIR:-/tmp}/export-kernel-tree.XXXXXX")" ||
die 'could not create a temporary download directory'
tarball="$download_dir/linux-$version.tar.xz"
url="https://cdn.kernel.org/pub/linux/kernel/v${version%%.*}.x/linux-$version.tar.xz"
say "Downloading $url"
curl --fail --location --silent --show-error --output "$tarball" "$url" ||
die "download failed: $url"
fi
fi
expected=''
if [[ -z $onto ]]; then
[[ -f $tarball ]] || die "no such tarball: $tarball"
expected="$(sed -n "s/^sha256[[:space:]]\+\([0-9a-f]\{64\}\)[[:space:]]\+linux-$version\.tar\.xz$/\1/p" "$HASH_FILE" | tail -1)"
[[ -n $expected ]] || die "no sha256 for linux-$version.tar.xz in $HASH_FILE — bump the hash from kernel.org's signed manifest"
actual="$(sha256sum "$tarball" | cut -d' ' -f1)"
[[ $actual == "$expected" ]] || die "tarball hash mismatch for linux-$version.tar.xz
expected $expected (from $HASH_FILE)
actual $actual"
say "Tarball verified: sha256 $actual"
fi
# --- 3. Extract ------------------------------------------------------------------------
if [[ -n $onto ]]; then
# Resolve the ref in the SOURCE repo and carry the SHA into the clone. Ref names are
# ambiguous across a clone boundary and it is not a theoretical problem: `git clone`
# copies the source's LOCAL branches to origin/*, so `--onto origin/MiSTer-v6.18`
# resolves inside the clone to the source's own local MiSTer-v6.18 -- a different
# commit from the origin/MiSTer-v6.18 the caller meant. That silently replayed a
# series onto a tree that already had it applied, and the version check could not
# catch it because both trees were the same Linux version.
onto="$(git -C "$parent_repo" rev-parse --verify --quiet "$onto^{commit}")" ||
die "--onto is not a commit in $parent_repo"
say "Resolved --onto to $onto in $parent_repo"
say "Cloning $parent_repo"
git clone --quiet --no-checkout "$parent_repo" "$output" || die "clone failed: $parent_repo"
git -C "$output" rev-parse --verify --quiet "$onto^{commit}" >/dev/null ||
die "$onto is not reachable in the clone of $parent_repo"
git -C "$output" checkout --quiet --detach "$onto"
# The base is someone else's, so verify it is the version we are about to patch
# rather than assuming. Read it from the target's own Makefile: replaying a 6.18
# series onto a 5.15 base would otherwise fail deep in `git am` with conflicts that
# look like bad patches instead of a bad base.
onto_version="$(sed -nE 's/^VERSION = //p;s/^PATCHLEVEL = /./p;s/^SUBLEVEL = /./p' \
"$output/Makefile" | head -3 | tr -d '\n')"
[[ $onto_version == "$version" ]] || die "--onto $onto is Linux $onto_version, but this
repo pins $version (BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE). Refusing to replay a $version
patch series onto a $onto_version base."
say "Base verified: $onto is Linux $onto_version"
elif [[ -n $parent_repo ]]; then
say "Cloning $parent_repo to extend its spine at $parent"
git clone --quiet --no-checkout "$parent_repo" "$output" || die "clone failed: $parent_repo"
git -C "$output" rev-parse --verify --quiet "$parent^{commit}" >/dev/null ||
die "--parent $parent is not a commit in $parent_repo"
# Detach at the spine point, then replace the worktree wholesale with the new
# tarball. `git add --all` stages the deletions and the additions together, so the
# resulting commit's tree is the pristine tarball and its parent is the spine.
git -C "$output" checkout --quiet --detach "$parent"
find "$output" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
else
mkdir -p "$output"
fi
if [[ -z $onto ]]; then
say "Extracting"
tar -xf "$tarball" -C "$output" --strip-components=1
fi
# kernel.org tarballs come from `git archive`, so every file's mtime is the tag's commit
# time. That makes this stable across machines, unlike the download time.
if [[ -n ${SOURCE_DATE_EPOCH:-} ]]; then
base_epoch="$SOURCE_DATE_EPOCH"
elif [[ -n $onto ]]; then
# No tarball here, so take the base's own commit date. Still deterministic: it is a
# property of the commit we were pointed at, not of when this script ran.
base_epoch="$(git -C "$output" log --format='%ct' -1 "$onto")"
else
base_epoch="$(stat -c %Y "$output/Makefile")"
fi
base_date="$(date -u -d "@$base_epoch" '+%Y-%m-%dT%H:%M:%S+00:00')"
# --- 4. Base commit: pristine upstream, on its own ---------------------------------------
# Kept as its own commit so `git diff <base> HEAD` is exactly the MiSTer delta and
# nothing else — the review question worth answering.
cd "$output"
[[ -n $parent_repo ]] || git init --quiet --initial-branch="$branch"
git config user.name "$EXPORT_NAME"
git config user.email "$EXPORT_EMAIL"
git config commit.gpgsign false
if [[ -n $onto ]]; then
# The base commit is upstream's; ours would be a duplicate. Branch and go straight to
# the series, so the result fast-forwards onto their branch and the PR is exactly our
# delta -- nothing of theirs restated.
base_commit="$(git rev-parse HEAD)"
# -B, not -b. `git clone` copies the source repo's LOCAL branches, so as soon as the
# parent repo has its own MiSTer-v6.18 checked out -- which it does the moment anyone
# fetches a previous export back into it -- `-b` dies with "a branch named
# 'MiSTer-v6.18' already exists" after the clone and the base verification have already
# succeeded. That made the export's success depend on the parent repo's branch state
# rather than on its commits, so it passed the first time and failed forever after.
# Overwriting is right here and not destructive: $output is a throwaway clone this
# script just created, the ref being replaced is a COPY of the parent's, and the real
# publish step is an explicit fetch out of this directory (see EXPORT.md).
git checkout --quiet -B "$branch"
else
# Subject is bare "v6.18.38" to match the spine's existing convention (v5.13.12, v5.14,
# v5.14.5, v5.15.1) — the branch should read as the next entry, not a foreign import.
#
# --force is load-bearing, not defensive. The kernel ships .gitignore files that match
# paths it also tracks, so a plain `git add` after a tarball extract silently drops
# them. That is not hypothetical: it is exactly why this repo's own v5.15.1 base is NOT
# byte-identical to kernel.org's v5.15.1 — 11 files (Documentation/.yamllint,
# fs/*/.kunitconfig, selftests/bpf/test_progs.c, selftests/arm64/tags/* to the `tags`
# ctags pattern, ...) are simply absent from it. Without --force we would reproduce that
# bug here and lose Documentation/.renames.txt from 6.18.38.
git add --all --force
GIT_AUTHOR_DATE="$base_date" GIT_COMMITTER_DATE="$base_date" \
git commit --quiet --file=- <<EOF
v$version
Pristine upstream kernel $version, unpacked from linux-$version.tar.xz as
published on kernel.org.
sha256 $expected
Verified against the pinned hash in Buildroot_MiSTer, itself transcribed from
kernel.org's PGP-signed release manifest.
No MiSTer change is present in this commit -- it is upstream and nothing else,
exactly like the v5.13.12/v5.14/v5.14.5/v5.15.1 commits it follows. Every MiSTer
delta is a separate commit on top, so a diff from this commit to the tip of
$branch is precisely the MiSTer patch series.
$(if [[ -n $parent_repo ]]; then printf '%s\n' "
Because this commit's parent is a pristine tarball commit too, the diff against
that parent is the pure upstream delta, with no MiSTer code on either side."; fi)
Generated by scripts/export-kernel-tree.sh in Buildroot_MiSTer. Do not edit this
tree directly; see EXPORT.md.
EOF
base_commit="$(git rev-parse HEAD)"
# -B for the same reason as the --onto path above: the clone carries a copy of the parent
# repo's local branches, so -b fails once the parent has a branch of this name.
[[ -n $parent_repo ]] && git checkout --quiet -B "$branch"
fi
# --- 5. Replay the carried series -------------------------------------------------------
# --committer-date-is-author-date keeps this reproducible: dates come from the patches,
# not the clock, so an unchanged series regenerates to identical SHAs.
#
# Author identity comes from each patch's own From:, which scripts/lint-kernel-patches.sh
# guarantees is parseable — `git am` hard-fails the whole series on a malformed one.
say "Replaying ${#series[@]} patches with git am"
if ! git am --committer-date-is-author-date "${series[@]}" >/dev/null 2>&1; then
git am --abort 2>/dev/null || true
die "git am failed. Run scripts/lint-kernel-patches.sh first — a malformed From:
line fails the whole series. If the headers are fine, a patch does not apply to
$version and the series needs rebasing onto it."
fi
applied="$(git rev-list --count "$base_commit"..HEAD)"
((applied == ${#series[@]})) ||
die "expected ${#series[@]} commits, got $applied"
say "Applied $applied/${#series[@]} carried patches cleanly"
# --- 5b. Replay the upstream-only series ------------------------------------------------
# AFTER the carried series and BEFORE the defconfig commit, deliberately. That ordering is
# what makes the tree's history readable as two contiguous blocks: everything from the
# base up to carried_tip is exactly the kernel the image ships, and the block after it is
# exactly what this tree adds for upstream. EXPORT.md publishes both as `git diff` ranges
# computed from that layout, so reordering these steps silently changes what those
# one-liners mean.
#
# Same --committer-date-is-author-date as above: dates come from the patches, not the
# clock, so an unchanged series regenerates to identical SHAs.
carried_tip="$(git rev-parse HEAD)"
upstream_applied=0
if ((${#upstream_series[@]})); then
say "Replaying ${#upstream_series[@]} upstream-only patches with git am"
if ! git am --committer-date-is-author-date "${upstream_series[@]}" >/dev/null 2>&1; then
git am --abort 2>/dev/null || true
die "git am failed on the upstream-only series in $upstream_patch_dir.
These patches are never applied by Buildroot, so unlike the carried series NOTHING ELSE
in this repo exercises them -- an image build stays green while they rot against a new
kernel. Run scripts/lint-kernel-patches.sh for a malformed From:; otherwise a patch no
longer applies to $version and needs rebasing onto it."
fi
upstream_applied="$(git rev-list --count "$carried_tip"..HEAD)"
((upstream_applied == ${#upstream_series[@]})) ||
die "expected ${#upstream_series[@]} upstream-only commits, got $upstream_applied"
say "Applied $upstream_applied/${#upstream_series[@]} upstream-only patches cleanly"
fi
# --- 6. In-tree defconfig, so the tree is usable without Buildroot ------------------------
# This is the step that makes the export worth shipping: `git clone && make` works, which
# is what a materialized tree is FOR and what `make linux` inside Buildroot cannot give.
#
# Buildroot consumes BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE by copying it to .config and
# running olddefconfig; `make MiSTer_defconfig` fills in defaults the same way, so the
# minimized file works unchanged as a defconfig.
say "Generating arch/arm/configs/MiSTer_defconfig"
if [[ -n $fragments ]]; then
# Merge with the kernel's OWN merge_config.sh rather than reimplementing Buildroot's
# merge. -m merges without invoking a compiler; -r keeps later fragments winning.
read -r -a frag_list <<<"$fragments"
KCONFIG_CONFIG=arch/arm/configs/MiSTer_defconfig \
./scripts/kconfig/merge_config.sh -m -r -O arch/arm/configs \
"$config_file" "${frag_list[@]}" >/dev/null 2>&1 ||
die 'merge_config.sh failed merging the config fragments'
mv arch/arm/configs/.config arch/arm/configs/MiSTer_defconfig 2>/dev/null || true
config_note="merged from $(basename "$config_file") + $(printf '%s ' "${frag_list[@]##*/}")"
else
cp "$config_file" arch/arm/configs/MiSTer_defconfig
config_note="copied verbatim from $(basename "$config_file")"
fi
git add arch/arm/configs/MiSTer_defconfig
GIT_AUTHOR_DATE="$base_date" GIT_COMMITTER_DATE="$base_date" \
git commit --quiet --file=- <<EOF
ARM: configs: add MiSTer_defconfig
The kernel configuration this board ships, in the kernel's own minimized
defconfig form, so the tree builds standalone without Buildroot:
make ARCH=arm MiSTer_defconfig
make ARCH=arm zImage
$config_note, which is the exact configuration Buildroot builds
(BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE) — the image and this tree cannot drift.
This is deliberately the minimized form rather than a full expanded .config: an
expanded one bakes in the generating toolchain (CONFIG_CC_VERSION_TEXT) and
every default, which pins a config to one machine and buries the ~500 lines that
are actually a decision under ~4000 that are not.
Generated by scripts/export-kernel-tree.sh in Buildroot_MiSTer.
EOF
# --- 6b. Vendor the out-of-tree kernel modules -------------------------------------------
# Without this the exported tree builds a kernel with no Xbox (xone) and no 11ac WiFi,
# while the 5.15 fork has both vendored in-tree — a silent feature regression for anyone
# who builds this tree expecting what MiSTer ships.
#
# WHY THE SOURCES ARE VENDORED BUT NOT WIRED INTO Kconfig
# -------------------------------------------------------
# The obvious thing is in-tree integration (Kconfig symbol + `obj-$(CONFIG_X) += dir/`),
# the way the fork does it. It is not safe for these packages, and the reason is in the
# Realtek Makefiles, above their own `ifneq ($(KERNELRELEASE),)` guard:
#
# export TopDIR ?= $(shell pwd)
# $(shell cp $(TopDIR)/autoconf_..._linux.h $(TopDIR)/include/autoconf.h)
#
# That is parse-time filesystem mutation keyed off `pwd`. In an in-tree build `pwd` is the
# KERNEL ROOT, not the module directory, so TopDIR points at the wrong tree and the
# driver's generated autoconf.h silently never appears -- `$(shell ...)` swallows the
# error. These 2594-line Makefiles are built on the assumption that they are never
# in-tree, across ~1900 files of driver. Wiring them in-tree would mean inventing hooks no
# upstream tests, then patching upstream Makefiles we would have to maintain forever.
#
# rtl8852cu-morrownr (the newer Realtek "phl" tree) reaches the same conclusion by a
# slightly different route, worth noting so nobody re-tests the old one and declares it
# fixed: it does still `export TopDIR ?= $(shell pwd)` (its Makefile:319) but its
# `$(shell cp ... autoconf.h)` is gated off by `CONFIG_AUTOCFG_CP = n` (:67, guard at
# :465), so THAT specific mutation is inactive. The pwd dependence is not -- `DRV_PATH ?=
# $(TopDIR)` (:323) is what `include $(wildcard $(DRV_PATH)/platform/*.mk)` resolves
# against, and in an in-tree build that misses platform/autodetect.mk entirely, taking
# -DCONFIG_IOCTL_CFG80211 and the rest of the flag set with it. Same verdict: out-of-tree
# only.
#
# So the sources go in at the paths the fork uses (the tree LOOKS like the fork's), and
# they are built through the exact out-of-tree invocation Buildroot already uses -- which
# is upstream's own supported path, and is proven daily by our own image builds. The
# recipe is read from each package's .mk rather than reinvented here, which is also what
# keeps a package bump cheap: change the pin in the .mk, re-run, done.
#
# xone is a partial exception -- 41 files, a clean Kbuild -- but its `obj-m :=` is declared
# UNCONDITIONALLY, never gated on CONFIG_XONE, so an in-tree Kconfig symbol for it would be
# decorative: present, and doing nothing. Two mechanisms in one tree is also harder to
# explain than one. It goes through the same path as the rest.
# Package -> in-tree path. The only hand-maintained mapping here, kept declarative on
# purpose. Every kernel-module package gets an entry, not just the currently-enabled ones,
# so flipping one on in the defconfig needs no edit here.
declare -A MODULE_PATH=(
[xone]='drivers/hid/xone'
[rtl8812au]='drivers/net/wireless/realtek/rtl8812au'
[rtl8814au-morrownr]='drivers/net/wireless/realtek/rtl8814au'
[rtl8821au-morrownr]='drivers/net/wireless/realtek/rtl8821au'
[rtl8821cu-morrownr]='drivers/net/wireless/realtek/rtl8821cu'
# rtl8852cu-morrownr is the one WiFi fork the image currently SHIPS (v10.2,
# ADR 0016 — mainline rtw89 has no rtw8852cu.c), so unlike its neighbours
# this row is on the live path, not a just-in-case entry. Same naming rule
# as the rest: the fork suffix is a Buildroot package-name concern, and the
# in-tree path uses the plain chip name the 5.15 fork would have used.
[rtl8852cu-morrownr]='drivers/net/wireless/realtek/rtl8852cu'
[rtl88x2bu]='drivers/net/wireless/realtek/rtl88x2bu'
[rtl8188eu-aircrack-ng]='drivers/net/wireless/realtek/rtl8188eu'
[rtl8188fu]='drivers/net/wireless/realtek/rtl8188fu'
)
# A package is a kernel module iff its .mk evals Buildroot's kernel-module infra. Detected
# rather than listed, so a new one cannot be missed by forgetting to update a list here.
#
# The `=y` is NOT anchored to end-of-line: this defconfig annotates most package lines
# with a trailing comment ("BR2_PACKAGE_RTL8812AU=y # RTL8812AU 11ac -- ..."), and
# anchoring matched only the one line without one, silently vendoring xone alone and
# dropping all three WiFi drivers.
mapfile -t enabled_kmods < <(
sed -n 's/^\(BR2_PACKAGE_[A-Z0-9_]*\)=y\([[:space:]].*\)\?$/\1/p' "$DEFCONFIG" |
while read -r sym; do
dir="$(tr 'A-Z_' 'a-z-' <<<"${sym#BR2_PACKAGE_}")"
mk="$REPO_ROOT/package/$dir/$dir.mk"
[[ -f $mk ]] || continue
# shellcheck disable=SC2016 # literal Makefile text being matched with
# grep -F, not a shell expression -- must stay single-quoted.
grep -qF '$(eval $(kernel-module))' "$mk" || continue
printf '%s\n' "$dir"
done
)
((${#enabled_kmods[@]})) || die 'detected zero kernel-module packages in the defconfig.
That is almost certainly a parsing bug in this script rather than the truth — the image
ships xone and the Realtek WiFi drivers. Refusing to export a tree missing them.'
say "Vendoring ${#enabled_kmods[@]} out-of-tree kernel modules: ${enabled_kmods[*]}"
module_build_lines=()
module_doc_rows=()
for pkg in "${enabled_kmods[@]}"; do
upper="$(tr 'a-z-' 'A-Z_' <<<"$pkg")"
mk="$REPO_ROOT/package/$pkg/$pkg.mk"
dest="${MODULE_PATH[$pkg]:-}"
# Fail closed. Silently skipping an enabled driver is exactly the regression this
# whole section exists to prevent.
[[ -n $dest ]] || die "no in-tree path mapped for kernel-module package '$pkg'.
Add it to MODULE_PATH in $(basename "${BASH_SOURCE[0]}") — refusing to export a tree
that silently omits a driver the image ships."
pkg_version="$(sed -n "s/^${upper}_VERSION = //p" "$mk" | tail -1)"
[[ -n $pkg_version ]] || die "no ${upper}_VERSION in $mk"
pkg_opts="$(sed -n "s/^${upper}_MODULE_MAKE_OPTS = //p" "$mk" | tail -1)"
# EXPORTED_LINUX_DIR. One option value has to be TRANSLATED rather than copied:
# rtl8852cu-morrownr passes KSRC=$(LINUX_DIR) (its Makefile probes the kernel
# version through KSRC and, left at platform/autodetect.mk's default of
# /lib/modules/$(uname -r)/build, silently drops every ccflag -- see that .mk).
# $(LINUX_DIR) is a Buildroot make variable. Emitted verbatim into
# build-mister-modules.sh it would become bash COMMAND SUBSTITUTION of a command
# named LINUX_DIR -- and, checked rather than assumed, `set -o errexit` does NOT
# catch that. The emitted line is `build_module <dir> CONFIG_RTL8852CU=m
# KSRC=$(LINUX_DIR)`: a simple command with arguments, not an assignment-only
# command, and a failed command substitution inside an ARGUMENT word does not
# become the enclosing command's exit status. Ran the exact shape under
# `set -o errexit; set -o nounset; set -o pipefail` (the generated script's own
# preamble): bash printed "LINUX_DIR: command not found" to stderr, build_module
# still ran with KSRC= EMPTY, the next line executed, and the script exited 0.
# So the silent empty-KSRC build -- every ccflag dropped, including
# -I$(src)/include and -DCONFIG_RTL8852C -- is the DEFAULT outcome, not a
# hypothetical one behind relaxed errexit. That makes this translation
# load-bearing, not belt-and-braces. The exported tree's equivalent is its own
# $KDIR, double-quoted in the replacement so a path with spaces survives; the `$`
# is backslash-escaped below so THIS script does not expand it.
pkg_opts="${pkg_opts//\$(LINUX_DIR)/\"\$KDIR\"}"
# Fail closed on any OTHER $(...) make variable: an option added later that this
# translation does not know about would be emitted verbatim and mis-execute the
# same way. Better to stop the export than to ship a build script that does.
# Written as `if`, not `[[ … ]] && die`, because a false test in an && list is a
# non-zero status and `set -o errexit` (line 151) would abort the export on the
# HAPPY path.
# shellcheck disable=SC2016 # the single quotes are the POINT: this matches a
# LITERAL "$(" substring in $pkg_opts. Expanding it here is exactly the bug
# being detected. (CI runs `shellcheck -x` at default severity, which
# includes info-level findings like SC2016 -- do not assume `-S warning`
# locally is the same gate.)
if [[ $pkg_opts == *'$('* ]]; then
die "unhandled make variable in ${upper}_MODULE_MAKE_OPTS: $pkg_opts
build-mister-modules.sh is bash, not make, so \$(...) there is command substitution.
Add a translation next to the EXPORTED_LINUX_DIR note in $(basename "${BASH_SOURCE[0]}")."
fi
pkg_tar="$REPO_ROOT/dl/$pkg/$pkg-$pkg_version.tar.gz"
[[ -f $pkg_tar ]] || die "missing source tarball: $pkg_tar
Populate Buildroot's download cache first: make $pkg-source"
# Same fail-closed rule as the kernel: the hash file is authority, no hash no export.
pkg_expected="$(sed -n "s|^sha256[[:space:]]\+\([0-9a-f]\{64\}\)[[:space:]]\+$pkg-$pkg_version\.tar\.gz$|\1|p" \
"$REPO_ROOT/package/$pkg/$pkg.hash" | tail -1)"
[[ -n $pkg_expected ]] || die "no sha256 for $pkg-$pkg_version.tar.gz in package/$pkg/$pkg.hash"
pkg_actual="$(sha256sum "$pkg_tar" | cut -d' ' -f1)"
[[ $pkg_actual == "$pkg_expected" ]] ||
die "hash mismatch for $pkg-$pkg_version.tar.gz
expected $pkg_expected
actual $pkg_actual"
mkdir -p "$dest"
tar -xzf "$pkg_tar" -C "$dest" --strip-components=1
# --force again: these trees ship their own .gitignore files (build artifacts,
# *.mod.c, Module.symvers). Without it we would drop tracked sources that happen to
# match, the same way the fork's own v5.15.1 base lost 11 files.
git add --force "$dest"
GIT_AUTHOR_DATE="$base_date" GIT_COMMITTER_DATE="$base_date" \
git commit --quiet --file=- <<EOF
$pkg: vendor $pkg_version at $dest
Out-of-tree kernel module the MiSTer image ships, vendored here so this tree
builds what MiSTer actually runs rather than a kernel silently missing it.
upstream $(sed -n "s/^${upper}_SITE = //p" "$mk" | tail -1)
pin $pkg_version
sha256 $pkg_expected
Sources are verbatim upstream, at the path the 5.15 branch uses. They are NOT
wired into Kconfig -- build them with ./build-mister-modules.sh, which uses this
package's own supported out-of-tree recipe. See that script for why.
Generated by scripts/export-kernel-tree.sh in Buildroot_MiSTer; the pin lives in
package/$pkg/$pkg.mk there.
EOF
module_build_lines+=("build_module $dest${pkg_opts:+ $pkg_opts}")
module_doc_rows+=("| \`$dest\` | $pkg_version | ${pkg_opts:-—} |")
done
# --- 6c. The build script, emitted from the .mk recipes ------------------------------------
say 'Writing build-mister-modules.sh'
cat >build-mister-modules.sh <<'MODEOF'
#!/usr/bin/env bash
#
# build-mister-modules.sh — build the out-of-tree drivers this tree vendors.
#
# The kernel builds with:
# make ARCH=arm MiSTer_defconfig
# make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage
#
# These drivers do NOT build from that, by design. They are vendored at the paths the
# 5.15 branch uses, but they are not wired into Kconfig, because their own Makefiles do
# parse-time work keyed off `$(shell pwd)`:
#
# export TopDIR ?= $(shell pwd)
# $(shell cp $(TopDIR)/autoconf_..._linux.h $(TopDIR)/include/autoconf.h)
#
# In an in-tree build `pwd` is the kernel root rather than the module directory, so that
# copy silently lands in the wrong place and the driver's generated autoconf.h never
# appears. These Makefiles assume they are always built out-of-tree. So that is how this
# builds them — which is upstream's own supported path, not a workaround.
#
# Usage: ./build-mister-modules.sh [ARCH] [CROSS_COMPILE]
# defaults: arm, arm-linux-gnueabihf-
#
# REQUIRES A FULLY BUILT KERNEL FIRST -- not just `modules_prepare`:
#
# make ARCH=arm MiSTer_defconfig
# make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- LOCALVERSION= zImage modules
# ./build-mister-modules.sh
#
# PASS `LOCALVERSION=` TO THE KERNEL BUILD, exactly as above. Empty, but SET.
#
# This tree is a git repo whose HEAD sits dozens of commits past the v<ver> base commit, so
# scripts/setlocalversion correctly concludes the source is modified and appends a "+",
# giving `6.18.38+`. Buildroot builds the same source from a tarball with no git around
# it, so its identical-but-also-patched kernel reports plain `6.18.38`. The two disagree
# only because one build can see its own history and the other cannot.
#
# That single "+" lands in vermagic, and vermagic is what the kernel matches on when
# loading a module:
#
# vermagic=6.18.38+ SMP mod_unload ARMv7 p2v8 <- built here, without LOCALVERSION=
# vermagic=6.18.38 SMP mod_unload ARMv7 p2v8 <- Buildroot, and this tree WITH it
#
# Mismatch that and modprobe rejects every module ("version magic ... should be ..."),
# which reads like a broken driver and is not. Setting LOCALVERSION= (even to empty)
# makes setlocalversion skip the "+" entirely, so this tree's kernel and modules are
# interchangeable with the shipped image's.
#
# `modules_prepare` is NOT enough, and the way it fails is worth knowing because the
# error blames the driver rather than the real cause. An external module is linked
# against the kernel's symbol table in Module.symvers, and that file is produced by
# modpost during `make modules`, which in turn needs vmlinux from the `zImage` build.
# Without it every kernel symbol the driver uses reads as undefined:
#
# ERROR: modpost: "skb_pull" [8812au.ko] undefined!
#
# Nothing is wrong with the driver there -- the kernel symbol table simply is not built
# yet. Hence the check below, which says so directly.
set -o errexit
set -o nounset
set -o pipefail
readonly KDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly ARCH="${1:-arm}"
readonly CROSS_COMPILE="${2:-arm-linux-gnueabihf-}"
[[ -f $KDIR/.config ]] || {
printf 'No .config — run `make ARCH=%s MiSTer_defconfig` first.\n' "$ARCH" >&2
exit 1
}
[[ -f $KDIR/Module.symvers ]] || {
cat >&2 <<EOF
No Module.symvers — the kernel is not built yet, so modpost has no symbol table and
every kernel symbol these drivers use would be reported as undefined.
Build the kernel first, then re-run this:
make ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE zImage modules
(\`modules_prepare\` alone does NOT produce Module.symvers -- it needs vmlinux.)
EOF
exit 1
}
build_module() {
local dir="$1"; shift
printf '\n=== %s\n' "$dir"