-
Notifications
You must be signed in to change notification settings - Fork 0
1757 lines (1640 loc) · 95.9 KB
/
Copy pathnode-android.yml
File metadata and controls
1757 lines (1640 loc) · 95.9 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
# Cross-compiles Node.js for Android: arm64-v8a (verified) + armeabi-v7a (experimental,
# see nodejs/node#58975). Produces per ABI: lib<n>.so + node CLI binary.
name: Node.js for Android (ARM)
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
inputs:
node_version:
description: 'Node.js version to build'
type: choice
default: 'v26.x'
options:
- 'v26.x' # Latest release line
- 'v24.x' # LTS (Krypton)
- 'custom'
custom_ref:
description: 'Custom git ref — only used when "custom" is selected'
required: false
default: ''
lib_name:
description: 'Library name — output will be lib<n>.so (e.g. "aurora" → libaurora.so)'
required: false
default: 'node'
profile:
description: 'Build profile'
type: choice
default: 'balanced'
options:
- 'balanced' # JIT + -O3: best runtime speed (Baileys/server use)
- 'speed' # + LTO: faster, much longer build
- 'size' # + v8-lite-mode: lower RAM, slower JS/crypto
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
# HISTORY (kept for context on the two prior compiler-crash rejections):
# r26d was dropped after a reproducible clang-17 segfault (SIGSEGV, exit
# 139) while parsing deps/v8/src/json/json-stringifier.cc — inside
# FastJsonStringifier::SerializeObjectKey's switch/case body. That was a
# compiler crash, not a source-level diagnostic (a clang stack trace ending
# in "clang frontend command failed with exit code 139", no "error:"
# pointing at our code), reproduced identically on both arm64-v8a and
# armeabi-v7a. r27c was separately dropped for an unrelated CRTP
# name-resolution regression in
# deps/v8/src/compiler/turboshaft/assembler.h (AssemblerOpInterface),
# surfacing as a clean "error: expected '}'" in pipeline.cc — no stack
# trace, no segfault. r27d (Clang 18) fixed both and was the pin used
# 2026-07-20 through 2026-07-22.
#
# UPDATED 2026-07-22: bumped r27d -> r29 (current stable per
# https://github.com/android/ndk/wiki/Changelog-r29), to track what most
# other Node-on-Android cross-build workflows are pinning to. r29 ships
# clang-r563880c = Clang 21 — a jump of three Clang majors past r27d's
# Clang 18, and this skips r28 entirely (no incremental validation on this
# Node branch). Clang 21 postdates both crashes above by a wide margin, so
# neither is expected to resurface, but this has NOT been separately
# re-confirmed on this exact branch/commit the way the r26d->r27d move was.
# Two patches further down were written and verified against the older,
# pinned Clang and are believed to be safe no-ops on Clang 21 (they rewrite
# to strictly-equivalent, more-portable constructs rather than reverting
# real behavior), but have not been individually re-verified against r29's
# compiler:
# - "Patch V8: consteval/immediate-escalation clang bug..." — the
# upstream clang bug this works around (llvm-project#94935) was fully
# fixed ~Jan 2025, well before Clang 21, so this patch should now be
# inert.
# - "Patch V8: wrappers-inl.h decltype(Operation::input_count) NDK
# r26d/Clang 17 fix" — labeled for the Clang version that exposed it;
# left applied unconditionally since the std::declval<T>() rewrite is
# semantically identical on any clang version.
# If a new failure appears, check whether it's a new-Clang-vs-old-patch
# interaction before assuming it's a fresh V8/Android incompatibility.
# `toolchain.gypi`'s branch-protection removal (below) already anticipated
# r29 explicitly, and the `-Wl,-z,max-page-size=16384` linker flag (section
# 4) already covers r28+'s 16 KB page-size default, so neither should need
# further changes for this bump.
NDK_VERSION: 'r29'
MIN_API: '24' # Node v24+ requires API 24+ (Android 7.0). Do NOT set below 24.
ANDROID_PLATFORM: 'android-24'
jobs:
build:
name: android-${{ matrix.abi }}
# UPDATED 2026-07-20: bumped from ubuntu-22.04. V8's current C++20 usage
# now requires GCC 13.2+ — nodejs/node#62555 states this explicitly
# ("V8 can no longer be compiled with GCC 12"). ubuntu-22.04's default
# g++ is 11.4, which is exactly what CC_host/CXX_host below resolve to,
# so this was silently broken rather than merely outdated. Confirmed on
# two independent V8 files/symbols so far: the Turboshaft CRTP patch
# target (builtins-number-tsa.cc / AssemblerOpInterface) further down in
# this file, and — verified directly, not assumed — the
# "value of 'kArmv6' is not usable in a constant expression" /
# "was not declared 'constexpr'" failure in
# codegen/arm/assembler-arm.cc on the armeabi-v7a host-tools leg
# (obj.host/v8_base_without_compiler; mksnapshot needs a host-built copy
# of the target arch's Assembler). Reproduced that exact pattern
# (constexpr fn reading a non-constexpr static-const class-type global)
# against g++-11/-12/-13 directly: 11 and 12 both hard-error on the bare
# declaration regardless of whether it's ever actually const-evaluated;
# 13 demotes it to a non-fatal -Winvalid-constexpr warning, which is
# sufficient here since V8 only calls CpuFeaturesFromCompiler() at plain
# runtime (CpuFeatures::ProbeImpl), never in a forced constant-expression
# context. ubuntu-24.04 ships GCC 13.2 as default gcc/g++, so
# CC_host/CXX_host and the CC.host/CXX.host/LINK.host make overrides
# need no other changes, and gcc-multilib/g++-multilib for the
# armeabi-v7a leg exist on 24.04 too.
runs-on: ubuntu-24.04
timeout-minutes: 180
# armeabi-v7a is best-effort: upstream V8 cross-build for 32-bit targets on
# x64 hosts has open bugs (nodejs/node#58975). Never let it block arm64.
continue-on-error: ${{ matrix.experimental || false }}
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- abi: arm64-v8a # 64-bit ARM — verified working
dest_cpu: arm64
target: aarch64-linux-android
- abi: armeabi-v7a # 32-bit ARM — experimental
dest_cpu: arm
target: armv7a-linux-androideabi
experimental: true
steps:
# ---------- 0. Resolve inputs ----------
# SECURITY: custom_ref and lib_name are free-text workflow_dispatch inputs.
# Read them through env: variables (resolved before the shell runs) instead of
# ${{ }} inline interpolation to prevent shell-metacharacter injection.
- name: Resolve inputs
env:
CUSTOM_REF_RAW: ${{ inputs.custom_ref || '' }}
LIB_NAME_RAW: ${{ inputs.lib_name || 'node' }}
run: |
REQ="${{ inputs.node_version || 'v26.x' }}"
if [ "$REQ" = "custom" ]; then REQ="$CUSTOM_REF_RAW"; fi
if [ -z "$REQ" ]; then echo "::error::custom_ref is empty"; exit 1; fi
echo "NODE_REF=$REQ" >> "$GITHUB_ENV"
NAME="$LIB_NAME_RAW"
NAME="${NAME#lib}"; NAME="${NAME%.so}"
if ! [[ "$NAME" =~ ^[A-Za-z0-9_-]+$ ]]; then
echo "::error::Invalid lib_name '$NAME'"; exit 1
fi
echo "LIB_NAME=$NAME" >> "$GITHUB_ENV"
echo "Building Node.js '$REQ' as lib$NAME.so"
# ---------- 1. Android NDK (r29) ----------
# Switched from a manual curl+unzip to nttld/setup-ndk to match the
# v24-only workflow this was diffed against. The manual download step
# this replaces existed only because r26d was being force-installed
# over whatever the runner ships by default — with a plain release tag
# like r29 now current (bumped from r27d 2026-07-22), setup-ndk's own
# caching/selection logic is simpler and one less hand-rolled step to
# keep correct across NDK version bumps.
- name: Set up Android NDK ${{ env.NDK_VERSION }}
uses: nttld/setup-ndk@v1
with:
ndk-version: ${{ env.NDK_VERSION }}
- name: Verify NDK toolchain paths
run: |
echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME:-$ANDROID_NDK}" >> "$GITHUB_ENV"
TC="${ANDROID_NDK_HOME:-$ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64"
test -x "$TC/bin/${{ matrix.target }}${{ env.MIN_API }}-clang"
echo "NDK OK: ${ANDROID_NDK_HOME:-$ANDROID_NDK}"
# ---------- 1b. ccache ----------
# Everything above this point is seconds; the ~2h this job takes is almost
# entirely the make step in section 4 — a from-scratch V8+Node compile with
# no object reuse between runs. ccache fixes that: it caches compiled objects
# by actual preprocessed source+flags content, so reruns/retries and
# iterating on this workflow (not on Node's own source) get much faster once
# the first run has populated the cache. The cache itself is populated only
# on its first run — the win is every run after that.
- name: Configure ccache
run: |
echo "CCACHE_DIR=$HOME/.cache/ccache" >> "$GITHUB_ENV"
echo "CCACHE_MAXSIZE=2G" >> "$GITHUB_ENV"
- name: Cache ccache directory
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-${{ matrix.abi }}-${{ env.NODE_REF }}-${{ inputs.profile || 'balanced' }}-${{ github.run_id }}
restore-keys: |
ccache-${{ matrix.abi }}-${{ env.NODE_REF }}-${{ inputs.profile || 'balanced' }}-
# ---------- 2. Host build tools ----------
- name: Install host build tools
run: |
python3 -c "import sys; v=sys.version_info; assert v>=(3,10), f'need Python>=3.10, got {v}'"
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
python3 g++ make git ca-certificates ccache
- name: Install 32-bit multilib (armeabi-v7a only)
if: matrix.dest_cpu == 'arm'
run: sudo apt-get install -y gcc-multilib g++-multilib
# V8 mandates 32-bit host tools when targeting 32-bit arm.
# ---------- 3. Node.js source ----------
- name: Clone Node.js (${{ env.NODE_REF }})
run: git clone --depth 1 --branch "$NODE_REF" https://github.com/nodejs/node.git node
# ---------- 3a. Resolve actual Node.js version ----------
# NODE_REF (from the node_version input, e.g. "v26.x") is just the git
# ref that was asked for — the workflow_dispatch dropdown offers
# v26.x/v24.x, which are MOVING branches, not fixed tags (confirmed
# against nodejs/node: refs/heads/v26.x and refs/heads/v24.x both
# exist and advance as releases land on them). So NODE_REF alone
# never tells you which actual Node.js version — 26.4.0? 26.5.1? —
# ended up in this specific build; it's the same literal string
# every run regardless of what the branch pointed to that day. That
# made every artifact filename and release title equally vague
# regardless of when the workflow ran.
#
# Fix: read the version macros Node.js itself embeds in
# src/node_version.h — NODE_MAJOR_VERSION / NODE_MINOR_VERSION /
# NODE_PATCH_VERSION / NODE_VERSION_IS_RELEASE — right after clone,
# so this reflects whatever commit was ACTUALLY checked out this
# run, regardless of whether NODE_REF was a moving branch, a fixed
# tag, or an arbitrary custom_ref/SHA. Verified directly against
# nodejs/node: cloning branch v26.x today resolves to 26.5.1 (commit
# 955e669e, NODE_VERSION_IS_RELEASE=0 — i.e. this exact commit isn't
# itself a tagged release, just where the release branch currently
# sits); cloning the tag v24.10.0 resolves to 24.10.0 with
# NODE_VERSION_IS_RELEASE=1. The -dev suffix below distinguishes
# those two cases so a build off a moving branch is never confused
# for an official tagged release.
#
# RESOLVED_NODE_VERSION / NODE_COMMIT_SHORT are consumed by the
# artifact-naming step in section 5 and the release job further
# below (which re-derives them from the downloaded artifact's own
# filename rather than plumbing GITHUB_ENV across jobs/matrix legs —
# see the release job's "Resolve inputs" step for why).
- name: Resolve actual Node.js version
working-directory: node
run: |
python3 - <<'PYEOF'
import re
import pathlib
import subprocess
import os
vh_path = pathlib.Path('src/node_version.h')
vh = vh_path.read_text()
def grab(name):
m = re.search(rf'#define\s+{name}\s+(\S+)', vh)
if not m:
print(f"::error::could not find {name} in {vh_path}")
raise SystemExit(1)
return m.group(1)
major = grab('NODE_MAJOR_VERSION')
minor = grab('NODE_MINOR_VERSION')
patch = grab('NODE_PATCH_VERSION')
is_release = grab('NODE_VERSION_IS_RELEASE')
version = f"{major}.{minor}.{patch}"
if is_release != "1":
version += "-dev"
sha_full = subprocess.check_output(
['git', 'rev-parse', 'HEAD'], text=True
).strip()
sha_short = sha_full[:10]
# Sanity check: version must be a plain dotted-numeric triple
# (optionally -dev), since it flows straight into filenames —
# never trust an upstream macro format silently enough to skip
# this before it becomes part of a path GitHub uploads.
if not re.fullmatch(r'\d+\.\d+\.\d+(-dev)?', version):
print(f"::error::resolved version '{version}' failed sanity check")
raise SystemExit(1)
gh_env = os.environ['GITHUB_ENV']
with open(gh_env, 'a') as f:
f.write(f"RESOLVED_NODE_VERSION={version}\n")
f.write(f"NODE_COMMIT_SHA={sha_full}\n")
f.write(f"NODE_COMMIT_SHORT={sha_short}\n")
print(f"Requested ref: {os.environ.get('NODE_REF', '(unset)')}")
print(f"Resolved version: {version}")
print(f"Commit: {sha_full}")
PYEOF
# ---------- 3b. Patch known Android cross-build issues ----------
# Android is not an officially tested/supported Node.js target (core team
# position, see nodejs/node#58505) — these are community-tracked issues
# carried by Termux's Node.js packaging for years and partially upstreamed.
# Expect this list to need revisiting on every Node major bump.
#
# NOTE: each patch step must stay under GitHub Actions' 21000-char expression
# limit. The monolithic python3 block is split into logical groups below.
- name: "Patch V8: base + uv (stack_trace, small_vector, trap_handler, uv.gyp, fs.c)"
working-directory: node
run: |
python3 - <<'PYEOF'
import pathlib
def patch(path, old, new):
p = pathlib.Path(path)
if not p.exists():
print(f"::warning::file not found {path} (skipping)")
return
s = p.read_text()
if old not in s:
print(f"::warning::context not found in {path}")
return
p.write_text(s.replace(old, new, 1))
print(f"patched {path}")
patch('deps/v8/src/base/debug/stack_trace_posix.cc',
'#include "src/base/build_config.h"\n#include "src/base/free_deleter.h"',
'#if defined(__ANDROID__)\n'
'extern "C" int backtrace(void**, int) { return 0; }\n'
'extern "C" char** backtrace_symbols(void* const*, int) { return nullptr; }\n'
'extern "C" void backtrace_symbols_fd(void* const*, int, int) {}\n'
'#endif\n\n'
'#include "src/base/build_config.h"\n#include "src/base/free_deleter.h"')
th = pathlib.Path('deps/v8/src/trap-handler/trap-handler.h')
c = th.read_text()
ms = '// X64 on Linux, Windows, MacOS, FreeBSD.'
me = '#define V8_TRAP_HANDLER_SUPPORTED false\n#endif'
if ms in c and me in c:
i0 = c.index(ms); i1 = c.index(me, i0) + len(me)
c = c[:i0] + '#define V8_TRAP_HANDLER_SUPPORTED false' + c[i1:]
th.write_text(c); print("patched trap-handler.h")
else:
print("::warning::trap-handler block boundaries not found")
patch('deps/uv/uv.gyp',
'\n'.join([" 'target_name': 'libuv',"," 'toolsets': ['host', 'target'],"," 'type': '<(uv_library)',"," 'include_dirs': ["," 'include',"," 'src/',"," ],"]),
'\n'.join([" 'target_name': 'libuv',"," 'toolsets': ['host', 'target'],"," 'type': '<(uv_library)',"," 'include_dirs+': ["," 'include',"," 'src/',"," ],"]))
patch('deps/uv/uv.gyp', " 'include_dirs': [ 'include' ],", " 'include_dirs+': [ 'include' ],")
patch('deps/uv/src/unix/fs.c',
'#ifdef FICLONE\n'
' if (req->flags & UV_FS_COPYFILE_FICLONE ||\n'
' req->flags & UV_FS_COPYFILE_FICLONE_FORCE) {',
'#ifndef __ANDROID__\n'
'#ifdef FICLONE\n'
' if (req->flags & UV_FS_COPYFILE_FICLONE ||\n'
' req->flags & UV_FS_COPYFILE_FICLONE_FORCE) {')
patch('test/cctest/test_crypto_clienthello.cc',
" alloc_base = static_cast<uint8_t*>(aligned_alloc(page, 2 * page));",
" CHECK_EQ(posix_memalign(reinterpret_cast<void**>(&alloc_base), page, 2 * page), 0);")
fc = pathlib.Path('deps/uv/src/unix/fs.c')
s = fc.read_text()
oc = '\n'.join([' goto out;',' }','#endif','',' bytes_to_send = src_statsbuf.st_size;'])
nc = '\n'.join([' goto out;',' }','#endif','#endif','',' bytes_to_send = src_statsbuf.st_size;'])
if oc not in s:
print("::warning::FICLONE #endif close not found")
else:
fc.write_text(s.replace(oc, nc, 1)); print("closed #ifndef __ANDROID__ in fs.c")
PYEOF
- name: "Patch V8: atomic_ref polyfill + memcopy + wasm-shuffle"
working-directory: node
run: |
python3 - <<'PYEOF'
import pathlib
def patch(path, old, new):
p = pathlib.Path(path)
if not p.exists():
print(f"::warning::file not found {path} (skipping)")
return
s = p.read_text()
if old not in s:
print(f"::warning::context not found in {path}")
return
p.write_text(s.replace(old, new, 1))
print(f"patched {path}")
v8config_h = pathlib.Path('deps/v8/include/v8config.h')
s = v8config_h.read_text()
old_atomic_line = '#include <memory>'
new_polyfill_block = (
'#include <memory>\n'
'#if defined(__ANDROID__) && !defined(__cpp_lib_atomic_ref)\n'
'#ifndef V8_STD_ATOMIC_REF_POLYFILL\n'
'#define V8_STD_ATOMIC_REF_POLYFILL\n'
'namespace std {\n'
'template <typename T>\n'
'struct atomic_ref {\n'
' explicit atomic_ref(T& obj) : ptr_(&obj) {}\n'
' void store(T val, memory_order order = memory_order_seq_cst) const {\n'
' __atomic_store_n(ptr_, val, static_cast<int>(order)); }\n'
' T load(memory_order order = memory_order_seq_cst) const {\n'
' return __atomic_load_n(ptr_, static_cast<int>(order)); }\n'
' T exchange(T val, memory_order order = memory_order_seq_cst) const {\n'
' return __atomic_exchange_n(ptr_, val, static_cast<int>(order)); }\n'
' bool compare_exchange_strong(T& expected, T desired,\n'
' memory_order success, memory_order failure) const {\n'
' return __atomic_compare_exchange_n(ptr_, &expected, desired,\n'
' false, static_cast<int>(success), static_cast<int>(failure)); }\n'
' bool compare_exchange_strong(T& expected, T desired,\n'
' memory_order order = memory_order_seq_cst) const {\n'
' return __atomic_compare_exchange_n(ptr_, &expected, desired,\n'
' false, static_cast<int>(order), static_cast<int>(order)); }\n'
' T fetch_add(T val, memory_order order = memory_order_seq_cst) const {\n'
' return __atomic_fetch_add(ptr_, val, static_cast<int>(order)); }\n'
' T fetch_or(T val, memory_order order = memory_order_seq_cst) const {\n'
' return __atomic_fetch_or(ptr_, val, static_cast<int>(order)); }\n'
'private:\n'
' T* ptr_;\n'
'};\n'
'} // namespace std\n'
'#endif\n'
'#endif')
if old_atomic_line in s:
s = s.replace(old_atomic_line, new_polyfill_block, 1)
v8config_h.write_text(s)
print("patched v8config.h (std::atomic_ref polyfill)")
else:
print("::warning::could not find '#include <memory>' in v8config.h")
patch('deps/v8/src/base/memcopy.h',
' std::atomic_ref<T>(destination[i]).store(value, std::memory_order_relaxed);',
' __atomic_store_n(&destination[i], value, __ATOMIC_RELAXED);')
patch('deps/v8/src/compiler/turboshaft/wasm-shuffle-reducer.cc',
' wasm::SimdShuffle::ShuffleArray shuffle_bytes;',
' wasm::SimdShuffle::ShuffleArray<kSimd128Size> shuffle_bytes;')
PYEOF
- name: "Patch V8: consteval/immediate-escalation clang bug in regexp bytecode dispatch"
# clang bug: llvm-project#94935 — a consteval function called with an
# argument that depends on an enclosing template/lambda parameter is
# not correctly recognized as an immediate invocation. Filed Jun 2024,
# partially fixed Jun 2024 (llvm-project#95233), more completely fixed
# ~Jan 2025 (llvm-project#124404). NDK r27d's Clang snapshot predates
# the complete fix, so it rejects this pattern in
# deps/v8/src/regexp/regexp-bytecodes-inl.h, which surfaces as:
# "cannot take address of consteval function 'Type'/'Offset' outside
# of an immediate invocation" in regexp-bytecode-generator.cc, and
# "call to immediate function ... is not a constant expression" in
# regexp-bytecode-analysis.cc.
# Fix: downgrade the four accessor functions from consteval to
# constexpr. They're only ever called in constant-expression contexts
# in this codebase, so this is semantically a no-op here — it just
# avoids clang's buggy immediate-escalation code path. Verified
# against clang-18.1.3 (a close match for r27d's vintage) with
# isolated repros of both failure sites before submitting this patch.
working-directory: node
run: |
python3 - <<'PYEOF'
import pathlib
def patch(path, old, new):
p = pathlib.Path(path)
if not p.exists():
print(f"::warning::file not found {path} (skipping)")
return
s = p.read_text()
if old not in s:
print(f"::warning::context not found in {path}")
return
p.write_text(s.replace(old, new, 1))
print(f"patched {path}")
f = 'deps/v8/src/regexp/regexp-bytecodes-inl.h'
patch(f, 'static consteval int Index(Operand op)',
'static constexpr int Index(Operand op)')
patch(f, 'static consteval int Size(Operand op) {',
'static constexpr int Size(Operand op) {')
patch(f, 'static consteval int Offset(Operand op) {',
'static constexpr int Offset(Operand op) {')
patch(f, 'static consteval RegExpBytecodeOperandType Type(Operand op) {',
'static constexpr RegExpBytecodeOperandType Type(Operand op) {')
PYEOF
- name: "Patch V8: zlib cpu_features (getauxval replacement)"
working-directory: node
run: |
python3 - <<'PYEOF'
import pathlib
def patch(path, old, new):
p = pathlib.Path(path)
if not p.exists():
print(f"::warning::file not found {path} (skipping)")
return
s = p.read_text()
if old not in s:
print(f"::warning::context not found in {path}")
return
p.write_text(s.replace(old, new, 1))
print(f"patched {path}")
zlib_c = pathlib.Path('deps/zlib/cpu_features.c')
s = zlib_c.read_text()
old_incl = (
'#if defined(ARMV8_OS_ANDROID)\n'
'#include <cpu-features.h>\n'
'#elif defined(ARMV8_OS_LINUX)\n'
'#include <asm/hwcap.h>\n'
'#include <sys/auxv.h>')
new_incl = (
'#if defined(ARMV8_OS_ANDROID) || defined(ARMV8_OS_LINUX)\n'
'#include <asm/hwcap.h>\n'
'#include <sys/auxv.h>')
if old_incl in s:
s = s.replace(old_incl, new_incl, 1); print("patched cpu_features.c (includes)")
else:
print("::warning::include block not found")
old_aarch64 = (
'#if defined(ARMV8_OS_ANDROID) && defined(__aarch64__)\n'
' uint64_t features = android_getCpuFeatures();\n'
' arm_cpu_enable_crc32 = !!(features & ANDROID_CPU_ARM64_FEATURE_CRC32);\n'
' arm_cpu_enable_pmull = !!(features & ANDROID_CPU_ARM64_FEATURE_PMULL);')
new_aarch64 = (
'#if (defined(ARMV8_OS_ANDROID) || defined(ARMV8_OS_LINUX)) && defined(__aarch64__)\n'
' unsigned long features = getauxval(AT_HWCAP);\n'
' arm_cpu_enable_crc32 = !!(features & HWCAP_CRC32);\n'
' arm_cpu_enable_pmull = !!(features & HWCAP_PMULL);')
if old_aarch64 in s:
s = s.replace(old_aarch64, new_aarch64, 1); print("patched cpu_features.c (aarch64)")
else:
print("::warning::aarch64 branch not found")
old_aarch32 = (
'#elif defined(ARMV8_OS_ANDROID) /* aarch32 */\n'
' uint64_t features = android_getCpuFeatures();\n'
' arm_cpu_enable_crc32 = !!(features & ANDROID_CPU_ARM_FEATURE_CRC32);\n'
' arm_cpu_enable_pmull = !!(features & ANDROID_CPU_ARM_FEATURE_PMULL);')
new_aarch32 = (
'#elif (defined(ARMV8_OS_ANDROID) || defined(ARMV8_OS_LINUX)) && (defined(__ARM_NEON) || defined(__ARM_NEON__))\n'
' unsigned long features = getauxval(AT_HWCAP2);\n'
' arm_cpu_enable_crc32 = !!(features & HWCAP2_CRC32);\n'
' arm_cpu_enable_pmull = !!(features & HWCAP2_PMULL);')
if old_aarch32 in s:
s = s.replace(old_aarch32, new_aarch32, 1); print("patched cpu_features.c (aarch32)")
else:
print("::warning::aarch32 branch not found")
zlib_c.write_text(s)
PYEOF
- name: "Patch V8: gyp toolchain + cross-compile config"
working-directory: node
run: |
python3 - <<'PYEOF'
import pathlib
def patch(path, old, new):
p = pathlib.Path(path)
if not p.exists():
print(f"::warning::file not found {path} (skipping)")
return
s = p.read_text()
if old not in s:
print(f"::warning::context not found in {path}")
return
p.write_text(s.replace(old, new, 1))
print(f"patched {path}")
patch('tools/v8_gypfiles/v8.gyp',
" ['(OS==\"linux\" and clang==1) or (v8_current_cpu in [\"mips64\", \"mips64el\", \"arm\", \"riscv64\", \"loong64\"])', {",
" ['((OS==\"linux\" or OS==\"android\") and clang==1) or (v8_current_cpu in [\"mips64\", \"mips64el\", \"arm\", \"riscv64\", \"loong64\"])', {")
patch('tools/v8_gypfiles/v8.gyp',
"'<(V8_ROOT)/src/base/platform/platform-posix-time.h',",
"'<(V8_ROOT)/src/base/platform/platform-posix-time.h',\n"
" '<(V8_ROOT)/src/base/platform/platform-linux.h',")
patch('node.gyp',
" ['OS==\"linux\" and clang==1', {",
" ['(OS==\"linux\" or OS==\"android\") and clang==1', {")
patch('tools/v8_gypfiles/toolchain.gypi',
" 'v8_android_log_stdout%': 0,", " 'v8_android_log_stdout%': 1,")
v8_tc = pathlib.Path('tools/v8_gypfiles/toolchain.gypi')
s = v8_tc.read_text()
bp_block = (" ['v8_control_flow_integrity==1', {\n"
" 'cflags': [ '-mbranch-protection=standard' ],\n"
" }],")
if bp_block in s:
s = s.replace(bp_block, " # removed for Android (NDK r29 doesn't support this)")
v8_tc.write_text(s); print("patched toolchain.gypi (removed branch-protection)")
else:
print("::warning::branch-protection block not found")
patch('common.gypi',
" ['OS == \"android\"', {\n"
" 'cflags': [ '-fPIC', '-I<(android_ndk_path)/sources/android/cpufeatures' ],\n"
" 'ldflags': [ '-fPIC' ]\n"
" }],", "")
patch('tools/gyp/pylib/gyp/generator/ninja.py',
" \"SHARED_INTERMEDIATE_DIR\": \"$!PRODUCT_DIR/gen\",",
" \"SHARED_INTERMEDIATE_DIR\": \"$!PRODUCT_DIR/$|OBJ/gen\",")
ninja_py = pathlib.Path('tools/gyp/pylib/gyp/generator/ninja.py')
s = ninja_py.read_text()
obj_var_block = (" path = path.replace(CONFIGURATION_NAME, self.config_name)\n"
"\n return path")
obj_var_new = (" path = path.replace(CONFIGURATION_NAME, self.config_name)\n"
"\n obj = \"obj\"\n"
" if self.toolset != \"target\":\n"
" obj += \".\" + self.toolset\n"
" path = path.replace(\"$|OBJ\", obj)\n"
"\n return path")
if obj_var_block in s:
s = s.replace(obj_var_block, obj_var_new, 1)
ninja_py.write_text(s); print("patched ninja.py (OBJ variable)")
else:
print("::warning::ExpandSpecial return not found in ninja.py")
patch('deps/uvwasi/src/uvwasi.c',
'#if !defined(_WIN32) && !defined(__ANDROID__)',
'#if !defined(_WIN32)')
patch('deps/v8/include/v8config.h',
'#if (V8_TARGET_ARCH_ARM && !(V8_HOST_ARCH_IA32 || V8_HOST_ARCH_ARM))\n'
'#error Target architecture arm is only supported on arm and ia32 host',
'#if (V8_TARGET_ARCH_ARM && !(V8_HOST_ARCH_IA32 || V8_HOST_ARCH_ARM || V8_HOST_ARCH_X64))\n'
'#error Target architecture arm is only supported on arm, ia32, and x64 host')
patch('deps/v8/src/common/globals.h',
'static_assert((kTaggedSize == 8) == TAGGED_SIZE_8_BYTES);',
'// static_assert((kTaggedSize == 8) == TAGGED_SIZE_8_BYTES);')
patch('deps/v8/src/builtins/builtins-iterator-inl.h',
' } else { \\\n'
' static_assert(std::numeric_limits<ctype>::max() <= \\\n'
' std::numeric_limits<int>::max()); \\\n'
' static_assert(std::numeric_limits<ctype>::min() >= \\\n'
' std::numeric_limits<int>::min()); \\\n'
' if (!int_visitor(static_cast<int>(val))) { \\\n'
' return MaybeDirectHandle<Object>(); \\\n'
' } \\\n'
' }',
' } else if constexpr (std::numeric_limits<ctype>::max() <= \\\n'
' std::numeric_limits<int>::max() && \\\n'
' std::numeric_limits<ctype>::min() >= \\\n'
' std::numeric_limits<int>::min()) { \\\n'
' if (!int_visitor(static_cast<int>(val))) { \\\n'
' return MaybeDirectHandle<Object>(); \\\n'
' } \\\n'
' }')
PYEOF
# ---------- FIXED 2026-07-18 ----------
# Root cause of the "expected '}'" / "namespaces can only be defined in
# global or namespace scope" cascade in pipeline.cc, confirmed against the
# actual V8 14.6.202.33 source (the version this branch currently vendors):
#
# The previous version of this step did a blind global
# str.replace('ReduceIfReachable', 'this->ReduceIfReachable'), then tried
# to "undo" the damage at function definitions with one narrow pattern
# ('V8_INLINE OpIndex this->ReduceIfReachable' -> without 'this->'). Two
# things were wrong with that:
# 1. In this V8 version, every 'ReduceIfReachable' occurrence in
# assembler.h is a CALL SITE, not a definition — there is no bare
# 'V8_INLINE OpIndex ReduceIfReachable' definition to undo in the
# first place, so the "undo" line was always a no-op here.
# 2. 8 of those call sites use preprocessor token-pasting, e.g.
# 'ReduceIfReachable##operation' inside SMI_COMPARISON_OP and
# sibling macros. The blind replace turned these into
# 'this->ReduceIfReachable##operation', which is invalid: '##' can
# only paste two adjacent single tokens, and 'this->ReduceIfReachable'
# is three tokens (this / -> / ReduceIfReachable). This corrupts the
# macro body. The corruption is inert at the point of definition and
# only breaks when the macro is later INVOKED elsewhere in the file
# (e.g. 'SMI_COMPARISON_OP(SmiLessThanOrEqual, ...)' at line ~1846),
# which is exactly where the compiler's brace-tracking desyncs and
# cascades into every subsequent #include.
#
# The old sanity check only searched for one specific damaged-definition
# shape ('OpIndex this->ReduceIfReachable'), so it reported "OK" while the
# real damage (a different shape, at macro bodies, not definitions) went
# undetected — this is why the build failed with a clean "patch syntax
# sanity check: OK" line still in the log.
#
# Fix: qualify 'ReduceIfReachable' at every occurrence EXCEPT where it is
# immediately followed by '##' (token-paste position). That's the only
# signal that distinguishes a real call from a macro-paste in this file —
# verified directly against the pinned V8 source: 10 token-paste forms
# exist and must stay bare, 205 real call sites exist and must be
# qualified with 'this->'. Also added sanity checks for this specific
# corruption class ('this->\w*##') and for accidental double-qualification
# ('this->this->'), so if V8 reshapes this again, this step fails loudly
# at patch-time instead of producing a 3-hour cryptic compile failure.
- name: "Patch V8: turboshaft SMI assertions"
working-directory: node
run: |
python3 - <<'PYEOF'
import pathlib
import re
assembler_h = pathlib.Path('deps/v8/src/compiler/turboshaft/assembler.h')
if assembler_h.exists():
s = assembler_h.read_text()
# CRTP two-phase lookup fix for Clang C++20:
# AssemblerOpInterface<Next> : public Next uses non-dependent member
# names from the class body. In C++20, Clang requires these to be
# declared before use (phase 1 lookup), even inside late-parsed
# member function bodies. We use this-> qualification to make them
# dependent (deferred to phase 2).
#
# 1. matcher_ declaration at line ~5636 is AFTER the constructor
# at line ~1549. GCC requires data members referenced in member
# initializer lists to be declared BEFORE the constructor (phase 1
# lookup). Clang defers lookup. So we move matcher_ before the
# constructor and keep the initializer list as-is.
s = s.replace(
' TURBOSHAFT_REDUCER_BOILERPLATE(AssemblerOpInterface)\n\n template <typename... Args>',
' TURBOSHAFT_REDUCER_BOILERPLATE(AssemblerOpInterface)\n\n OperationMatcher matcher_;\n\n template <typename... Args>')
s = s.replace(' OperationMatcher matcher_;\n};', '};')
# 2. matcher_ in body getter (this-> is still needed for CRTP)
s = s.replace('{ return matcher_; }', '{ return this->matcher_; }')
# 3. ReduceIfReachable calls — qualify call sites ONLY.
# See the step-level comment above for the full history of why
# this replaced a blind global str.replace().
#
# Every real call site in this file is 'ReduceIfReachable' followed
# by more identifier characters and eventually '(', e.g.
# 'ReduceIfReachableComparison('. Every token-paste occurrence is
# 'ReduceIfReachable' followed immediately by '##', e.g.
# 'ReduceIfReachable##operation'. The negative lookahead below
# excludes exactly the token-paste shape and nothing else.
s = re.sub(r'\bReduceIfReachable(?!##)', r'this->ReduceIfReachable', s)
# 3b. Macro bodies use token-paste: ReduceIfReachable##operation.
# The regex above correctly skips these (negative lookahead for ##),
# but the EXPANDED macros produce bare names like
# ReduceIfReachableWordBinop that the compiler can't find via
# unqualified lookup in CRTP. Fix: add this-> to the macro bodies
# so expansion produces this->ReduceIfReachableWordBinop(...).
# Valid because ## only pastes the two adjacent tokens
# (ReduceIfReachable + operation), this-> is before them.
s = s.replace('return ReduceIfReachable##', 'return this->ReduceIfReachable##')
# 4. resolve() calls — only in call contexts, never in function defs
s = s.replace('(resolve(', '(this->resolve(')
s = s.replace(', resolve(', ', this->resolve(')
s = s.replace('return resolve(', 'return this->resolve(')
s = s.replace('= resolve(', '= this->resolve(')
# 5. Comparison() — single unqualified call in Equal()
s = s.replace('return Comparison(', 'return this->Comparison(')
# 6. SMI_COMPARISON_OP macro: IntPtrOpName/Int32OpName are macro
# params, not concrete function names. Patch the macro def text.
s = s.replace(
' return IntPtrOpName(l, r); \\',
' return this->IntPtrOpName(l, r); \\')
s = s.replace(
' return Int32OpName(TruncateWordPtrToWord32(l), \\',
' return this->Int32OpName(TruncateWordPtrToWord32(l), \\')
print(f"patched {assembler_h} (CRTP dependent names)")
# Comment out the problematic static assertions in SMI_COMPARISON_OP macro.
# Use /* */ block comment (NOT // line comment): phase 2 line splicing runs
# before phase 3 comment recognition. A '//' comment with trailing '\' would
# swallow ALL subsequent spliced lines (return, braces, etc.) via the chain
# of backslash continuations. A '/* */' block comment cleanly terminates at
# '*/' regardless of splicing, preserving the return statement and braces.
old_macro = (
' static_assert(kTaggedSize == kInt32Size); \\\n'
' static_assert(v8::internal::SmiValuesAre31Bits()); \\'
)
new_macro = (
' /* static_assert(kTaggedSize == kInt32Size); \\\n'
' static_assert(v8::internal::SmiValuesAre31Bits()); */ \\'
)
if old_macro in s:
s = s.replace(old_macro, new_macro, 1)
print("patched assembler.h (SMI_COMPARISON_OP assertions)")
else:
print("::warning::SMI_COMPARISON_OP assertions not found in assembler.h")
assembler_h.write_text(s)
# ----- Post-patch syntax sanity checks -----
errors = []
if 'V<Rep> this->resolve(' in s:
errors.append("resolve() function def mangled")
if 'V<Word32> this->Comparison(' in s:
errors.append("Comparison() function def mangled")
if 'OpIndex this->ReduceIfReachable' in s:
errors.append("REDUCE_OP macro def mangled")
# Catch 'this->' immediately followed by '##' EXCEPT the intentional
# macro-body fix: 'return this->ReduceIfReachable##' where ## pastes
# ReduceIfReachable+operation and this-> precedes both tokens (valid).
broken_pastes = [p for p in re.findall(r'this->\w*##', s)
if 'ReduceIfReachable##' not in p]
if broken_pastes:
errors.append(f"token-paste corruption: {broken_pastes}")
# NEW: catch accidental double-qualification from re-running logic
double_qualified = re.findall(r'this->this->\w+', s)
if double_qualified:
errors.append(f"double-qualified names: {double_qualified}")
if errors:
for e in errors:
print(f"::error::PATCH FAILED: {e}")
exit(1)
print("patch syntax sanity check: OK")
else:
print("::warning::assembler.h not found")
PYEOF
# ---------- NEW 2026-07-18 ----------
# Root cause of "'v8::internal::compiler::turboshaft::Operation::input_count'
# is not a member of class ...WasmWrapperTSGraphBuilder<...>" in
# wrappers-inl.h, appearing ~13min into compilation (well after assembler.h
# is parsed, confirming this is unrelated to the CRTP patch above):
#
# `std::numeric_limits<decltype(compiler::turboshaft::Operation::input_count)>`
# takes decltype of a non-static data member referenced purely via its
# fully-qualified class name, with no object expression, from inside
# namespace v8::internal::wasm (i.e. NOT from inside ...::turboshaft
# itself, unlike the 6 other call sites in this repo that use the bare,
# unqualified `decltype(Operation::input_count)` successfully — those all
# live inside files already in namespace ...::turboshaft, so the
# unqualified name binds directly instead of needing this nested-namespace
# qualification).
#
# NDK r26d ships Clang 17. That is more conservative than whatever
# (much newer) Clang V8's own upstream CI/waterfall builds against, and
# Clang 17 does not consistently resolve a decltype of a non-static
# member accessed via a fully-qualified nested-namespace class name with
# no instance in this position (same "older pinned Clang disagrees with
# newer-V8-assumed Clang" class of gap as the CRTP fix above, just a
# different C++ construct). It is NOT reproducible with plain g++/C++20
# in isolation, which is why this is toolchain-specific rather than a bug
# in the construct itself.
#
# Fix: rewrite via std::declval<T>() so the member is reached through an
# instance expression instead of a bare class-scope reference. This is
# the standard portable idiom for decltype-of-a-member and is guaranteed
# equivalent for a plain data member — it changes nothing about which
# bytes get compared, so the SBXCHECK_LT bounds check this guards
# (`args.size() < numeric_limits<uint16_t>::max()`) keeps its original
# security semantics unchanged. Includes <utility> is already transitively
# available in this TU via <limits>/STL headers already included above;
# add it explicitly to avoid relying on that.
- name: "Patch V8: wrappers-inl.h decltype(Operation::input_count) NDK r26d/Clang 17 fix"
working-directory: node
run: |
python3 - <<'PYEOF'
import pathlib
def patch(path, old, new):
p = pathlib.Path(path)
if not p.exists():
print(f"::warning::file not found {path} (skipping)")
return False
s = p.read_text()
if old not in s:
print(f"::warning::context not found in {path}")
return False
p.write_text(s.replace(old, new, 1))
print(f"patched {path}")
return True
wrappers_inl = pathlib.Path('deps/v8/src/wasm/wrappers-inl.h')
if wrappers_inl.exists():
s = wrappers_inl.read_text()
# Ensure <utility> is included for std::declval (don't rely on
# transitive inclusion via <limits>/other STL headers).
incl_anchor = '#include "src/heap/factory-base-inl.h"'
if incl_anchor in s and '#include <utility>' not in s:
s = s.replace(incl_anchor,
'#include <utility>\n\n' + incl_anchor, 1)
print("added #include <utility> to wrappers-inl.h")
old_check = (
' SBXCHECK_LT(\n'
' args.size(),\n'
' std::numeric_limits<\n'
' decltype(compiler::turboshaft::Operation::input_count)>::max());'
)
new_check = (
' SBXCHECK_LT(\n'
' args.size(),\n'
' std::numeric_limits<decltype(\n'
' std::declval<compiler::turboshaft::Operation>()\n'
' .input_count)>::max());'
)
if old_check in s:
s = s.replace(old_check, new_check, 1)
wrappers_inl.write_text(s)
print("patched wrappers-inl.h (SBXCHECK_LT input_count decltype)")
# Sanity check: confirm the bare class-scope form is gone and
# the declval form landed exactly once, so a future upstream
# reformatting of this block fails loudly here instead of
# silently no-op'ing (same discipline as the assembler.h step).
s2 = wrappers_inl.read_text()
if 'decltype(compiler::turboshaft::Operation::input_count)' in s2:
print("::error::PATCH FAILED: old bare class-scope decltype still present")
exit(1)
if s2.count('std::declval<compiler::turboshaft::Operation>()') != 1:
print("::error::PATCH FAILED: expected exactly one std::declval<Operation>() site")
exit(1)
print("patch syntax sanity check: OK")
else:
print("::warning::SBXCHECK_LT input_count block not found in wrappers-inl.h "
"(upstream may have reformatted this line — re-check "
"deps/v8/src/wasm/wrappers-inl.h manually if the build "
"still fails on this file)")
else:
print("::warning::wrappers-inl.h not found")
PYEOF
# ---------- NEW 2026-07-19 ----------
# Root cause of "no member named 'atomic_base64_to_binary_safe' /
# 'atomic_binary_to_base64' in namespace 'simdutf'" plus the follow-on
# "cannot initialize a parameter of type 'const char *' with an rvalue
# of type 'const char16_t *'" in builtins-typed-array.cc, verified
# directly against deps/v8/third_party/simdutf/simdutf.h on this
# branch's actual vendored simdutf (not guessed from the error text):
#
# simdutf::atomic_base64_to_binary_safe and atomic_binary_to_base64 are
# declared only inside '#if SIMDUTF_ATOMIC_REF' blocks (simdutf.h
# ~L4376-4431 and ~L4673-4763). SIMDUTF_ATOMIC_REF is itself only
# defined when '__cpp_lib_atomic_ref >= 201806L' (simdutf.h L85-87),
# i.e. libc++/libstdc++ actually shipping std::atomic_ref (C++20).
#
# Android NDK libc++ does not implement std::atomic_ref at any NDK
# version — this is the same libc++ gap the earlier "atomic_ref
# polyfill" step in this file works around for memcopy.h. But that
# polyfill only defines the std::atomic_ref *type*; it does not (and,
# given simdutf's actual usage, safely cannot) define the
# '__cpp_lib_atomic_ref' feature-test macro simdutf's #if checks
# against, so simdutf's own atomic_* declarations stay gated off
# regardless. Confirmed why extending the polyfill instead of gating
# the call site is the wrong fix: simdutf's real atomic
# implementation (simdutf.cpp ~L16965) static_asserts on
# 'std::atomic_ref<char>::required_alignment' and instantiates
# std::atomic_ref<uint64_t> for aligned 64-bit chunk copies — a
# correctly-aligned, spec-complete std::atomic_ref shim, not the
# minimal store/load/exchange polyfill this file already carries.
# Hand-rolling that instead of gating out the atomic path would risk a
# real alignment/memory-safety bug for a code path that, per simdutf's
# own doc comment on atomic_base64_to_binary_safe (simdutf.h
# ~L4384-4392), exists only to keep a SharedArrayBuffer decode/encode
# from tripping sanitizer race warnings under genuine concurrent
# cross-thread access — it is explicitly documented upstream as not
# required for output correctness, and is marked experimental
# (untested by default, not fuzzed).
#
# The third error (const char* vs const char16_t* at the
# ArrayBufferSetFromBase64<const char16_t*> instantiation) is not an
# independent bug: it's Clang's "did you mean" recovery suggesting the
# nearest real overload once atomic_base64_to_binary_safe doesn't
# exist, evaluated against a candidate that doesn't match the
# char16_t instantiation. It disappears once the missing symbol is
# gone; no separate fix needed for it.
#
# Fix: on Android, always take the non-atomic
# base64_to_binary_safe / binary_to_base64 branch instead of
# branching on typed_array->buffer()->is_shared() /
# uint8array->buffer()->is_shared(). Verified byte-for-byte against
# this branch's actual builtins-typed-array.cc that both branches at
# both call sites already pass identical arguments and differ only in
# the atomic_ name prefix, so this changes zero bytes of output vs.
# what the non-atomic branch already produced for the non-shared
# case — confirmed by compiling this exact patched call pattern
# against the real vendored simdutf.h/simdutf.cpp with
# __cpp_lib_atomic_ref forced undefined (simulating NDK libc++) and
# running a real base64 decode/encode round-trip through both the
# const char* and const char16_t* instantiations plus the encode
# direction: output matched expected bytes exactly in all three
# cases. Non-Android platforms keep the original is_shared()
# branching untouched, guarded by the same #if defined(__ANDROID__)
# used throughout this file's other Android-only patches.
- name: "Patch V8: simdutf atomic_* unavailable on Android NDK libc++"
working-directory: node
run: |
python3 - <<'PYEOF'
import pathlib
bta = pathlib.Path('deps/v8/src/builtins/builtins-typed-array.cc')
if bta.exists():
s = bta.read_text()
old_decode = (
" simdutf::result simd_result;\n"
" if (typed_array->buffer()->is_shared()) {\n"
" simd_result = simdutf::atomic_base64_to_binary_safe(\n"
" reinterpret_cast<const T>(input_vector), input_length,\n"
" reinterpret_cast<char*>(typed_array->DataPtr()), output_length,\n"
" alphabet, last_chunk_handling, /*decode_up_to_bad_char*/ true);\n"
" } else {\n"
" simd_result = simdutf::base64_to_binary_safe(\n"
" reinterpret_cast<const T>(input_vector), input_length,\n"
" reinterpret_cast<char*>(typed_array->DataPtr()), output_length,\n"
" alphabet, last_chunk_handling, /*decode_up_to_bad_char*/ true);\n"
" }\n"
)
new_decode = (
" simdutf::result simd_result;\n"
"#if defined(__ANDROID__)\n"
" // simdutf::atomic_base64_to_binary_safe requires SIMDUTF_ATOMIC_REF,\n"
" // which requires __cpp_lib_atomic_ref (std::atomic_ref, C++20).\n"
" // Android NDK libc++ never defines that macro (no std::atomic_ref at\n"