From c6209fb3d6c2dbb7e37928768d6746f75c15686c Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 14 Aug 2026 11:14:56 +0800 Subject: [PATCH 1/5] fix: don't compile with -march=native by default The build unconditionally added -march=native on UNIX, so rapidjson.so inherited whatever CPU the build host happened to have. A module built on a modern packaging machine then dies with SIGILL on older CPUs, and on hypervisors that expose a baseline CPU model. Move it behind LUA_RAPIDJSON_ARCH_NATIVE (default OFF) so distributed builds are portable, and assert in CI that the built module carries no AVX instructions. Also add the release workflow used by the other api7 rock repositories: pushing a rockspec/ file to master with a "feat: release v" commit tags the release and uploads it to luarocks. --- .github/workflows/main.yml | 10 +++++++ .github/workflows/release.yml | 55 +++++++++++++++++++++++++++++++++++ CMakeLists.txt | 15 +++++++--- 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe64d98..10717bf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,5 +29,15 @@ jobs: - name: Build run: luarocks make + - name: Check the built module is portable + run: | + # A build that leaks the runner's CPU features (-march=native, see + # CMakeLists.txt) dies with SIGILL on older CPUs, and nothing else + # catches it until the module is loaded on such a machine. + if objdump -d build.luarocks/rapidjson.so | grep -qE '%[yz]mm|vfmadd|vpbroadcast'; then + echo "rapidjson.so contains AVX instructions; the build is not portable" >&2 + exit 1 + fi + - name: Test run: busted diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..af11e8c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,55 @@ +name: Release + +on: + push: + branches: + - "master" + paths: + - 'rockspec/**' + +permissions: + contents: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Lua + uses: leafo/gh-actions-lua@v10 + + - name: Install Luarocks + uses: leafo/gh-actions-luarocks@v4 + + - name: Extract release name + id: release_env + shell: bash + env: + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + run: | + title="${COMMIT_MESSAGE}" + re="^feat: release v*(\S+)" + if [[ $title =~ $re ]]; then + echo "version=v${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" + echo "version_without_v=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" + else + echo "commit message must be 'feat: release v'" + exit 1 + fi + + - name: Create release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.release_env.outputs.version }} + run: gh release create "${VERSION}" --title "${VERSION}" --generate-notes + + - name: Upload to luarocks + env: + LUAROCKS_TOKEN: ${{ secrets.LUAROCKS_TOKEN }} + VERSION: ${{ steps.release_env.outputs.version_without_v }} + run: | + luarocks install dkjson + luarocks upload "rockspec/api7-lua-rapidjson-${VERSION}-0.rockspec" --api-key="${LUAROCKS_TOKEN}" diff --git a/CMakeLists.txt b/CMakeLists.txt index 117c69d..8875ac5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,10 +32,17 @@ if(UNIX) set(LINK_FLAGS "-shared") endif(APPLE) add_compile_options(-g -Wall -fPIC) - include(CheckCXXCompilerFlag) - CHECK_CXX_COMPILER_FLAG("-march=native" COMPILER_SUPPORTS_ARCH_NATIVE) - if (COMPILER_SUPPORTS_ARCH_NATIVE) - add_compile_options(-march=native) + # -march=native bakes the build host's CPU features into rapidjson.so, so a + # module built on a modern machine dies with SIGILL on an older one. Keep it + # opt-in: builds that get distributed (packages, images, luarocks) must stay + # portable. + option(LUA_RAPIDJSON_ARCH_NATIVE "Optimize for the build host CPU; produces a non-portable binary" OFF) + if (LUA_RAPIDJSON_ARCH_NATIVE) + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("-march=native" COMPILER_SUPPORTS_ARCH_NATIVE) + if (COMPILER_SUPPORTS_ARCH_NATIVE) + add_compile_options(-march=native) + endif() endif() else(UNIX) if(WIN32) From d4f93455fe3ef71f382ee7567a0a4dadbde44f25 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 14 Aug 2026 11:17:53 +0800 Subject: [PATCH 2/5] ci: repair the Lua matrix luajit.org no longer serves the LuaJIT 2.0.5 and 2.1.0-beta3 tarballs, so gh-actions-lua fails with a 404 and the whole matrix has been red since 2025-08-26. Move to luajit-2.1, which the current action pulls from git, and stop fail-fast from cancelling the other entries and hiding which one actually broke. --- .github/workflows/main.yml | 9 +++++---- .github/workflows/release.yml | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 10717bf..370e9e0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,20 +5,21 @@ on: [push] jobs: build: strategy: + fail-fast: false matrix: os: [ubuntu-latest] - lua_version: [5.1, 5.2, 5.3, 5.4, luajit-2.0.5, luajit-2.1.0-beta3] + lua_version: [5.1, 5.2, 5.3, 5.4, luajit-2.1] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - - uses: leafo/gh-actions-lua@v8.0.0 + - uses: leafo/gh-actions-lua@v13 with: luaVersion: ${{ matrix.lua_version }} - - uses: leafo/gh-actions-luarocks@v4.0.0 + - uses: leafo/gh-actions-luarocks@v6 - name: Setup rocks depended run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af11e8c..68802b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,10 +19,10 @@ jobs: uses: actions/checkout@v4 - name: Install Lua - uses: leafo/gh-actions-lua@v10 + uses: leafo/gh-actions-lua@v13 - name: Install Luarocks - uses: leafo/gh-actions-luarocks@v4 + uses: leafo/gh-actions-luarocks@v6 - name: Extract release name id: release_env From 1e4a8c47d03f785a331a524e7e6c80ee4153ae1c Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 14 Aug 2026 11:24:05 +0800 Subject: [PATCH 3/5] ci: harden the portability check and the release trigger The portability check silently passed when the module was missing or objdump failed, and its pattern only matched 256-bit operands, so a build using VEX-encoded 128-bit instructions or bare vzeroupper slipped through. Assert the artifact exists, fail on a broken pipeline, and match any VEX/EVEX mnemonic instead. The release trigger matched "v*", which also accepted a version with no leading v and would have tagged it anyway. --- .github/workflows/main.yml | 15 ++++++++++++--- .github/workflows/release.yml | 5 ++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 370e9e0..9d47220 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,12 +31,21 @@ jobs: run: luarocks make - name: Check the built module is portable + shell: bash run: | + set -euo pipefail # A build that leaks the runner's CPU features (-march=native, see # CMakeLists.txt) dies with SIGILL on older CPUs, and nothing else - # catches it until the module is loaded on such a machine. - if objdump -d build.luarocks/rapidjson.so | grep -qE '%[yz]mm|vfmadd|vpbroadcast'; then - echo "rapidjson.so contains AVX instructions; the build is not portable" >&2 + # catches it until the module is loaded on such a machine. Every + # VEX/EVEX encoded instruction has a v- or k-prefixed mnemonic, so + # their absence is what makes the module safe to ship. + module=build.luarocks/rapidjson.so + test -f "$module" + vex=$(objdump -d --no-show-raw-insn "$module" \ + | grep -E '^[[:space:]]+[0-9a-f]+:[[:space:]]+[vk][a-z0-9]+' || true) + if [ -n "$vex" ]; then + printf '%s\n' "$vex" | awk 'NR <= 5' + echo "rapidjson.so uses VEX/EVEX encoded instructions; the build is not portable" >&2 exit 1 fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 68802b8..007f29b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install Lua uses: leafo/gh-actions-lua@v13 @@ -31,7 +33,8 @@ jobs: COMMIT_MESSAGE: ${{ github.event.head_commit.message }} run: | title="${COMMIT_MESSAGE}" - re="^feat: release v*(\S+)" + # tolerate the " (#N)" a squash merge appends to the title + re="^feat: release v([^[:space:]]+)" if [[ $title =~ $re ]]; then echo "version=v${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" echo "version_without_v=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" From 5617e1522b8f9dac0dff7e0707d69d16f26a7bb5 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 14 Aug 2026 11:47:09 +0800 Subject: [PATCH 4/5] ci: check the baseline properly and keep the release retryable The portability check only looked for VEX/EVEX mnemonics, so a build that targets a pre-AVX CPU passed it while still using SSE4.1 (pmovzxdq, roundsd) or SSE4.2 (pcmpistri, reachable through RAPIDJSON_SSE42) and still dying with SIGILL on older machines. Assert the recorded compile flags stay on the x86-64 baseline, and widen the disassembly scan to every instruction family above it. Restore LuaJIT 2.0 to the matrix through the luajit-2.0 alias, which builds from the version branch rather than the tarball luajit.org withdrew; it is a runtime the README claims support for. Creating the GitHub release now tolerates an existing release, so a failed luarocks upload can be re-run. --- .github/workflows/main.yml | 33 ++++++++++++++++++++++----------- .github/workflows/release.yml | 9 ++++++++- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9d47220..972b76f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - lua_version: [5.1, 5.2, 5.3, 5.4, luajit-2.1] + lua_version: [5.1, 5.2, 5.3, 5.4, luajit-2.0, luajit-2.1] runs-on: ${{ matrix.os }} @@ -34,18 +34,29 @@ jobs: shell: bash run: | set -euo pipefail - # A build that leaks the runner's CPU features (-march=native, see - # CMakeLists.txt) dies with SIGILL on older CPUs, and nothing else - # catches it until the module is loaded on such a machine. Every - # VEX/EVEX encoded instruction has a v- or k-prefixed mnemonic, so - # their absence is what makes the module safe to ship. + # Anything above the x86-64 baseline (SSE2) makes the module die with + # SIGILL on older CPUs, and nothing else catches that until someone + # loads it there. The recorded compile flags are the exact check; the + # disassembly is the backstop for instructions that arrive by other + # means, such as intrinsics behind RAPIDJSON_SSE42. + flags=$(grep -h '^CXX_FLAGS' build.luarocks/CMakeFiles/*/flags.make) + if grep -qE -- '-m(sse[34]|ssse3|avx|fma|bmi|popcnt|aes|pclmul|f16c|abm|lzcnt|movbe|adx|sha)' <<<"$flags" || + grep -oE -- '-march=[^[:space:]]+' <<<"$flags" | grep -qvE -- '-march=x86-64(-v1)?$'; then + echo "$flags" + echo "the module is built for a specific CPU, not the x86-64 baseline" >&2 + exit 1 + fi + module=build.luarocks/rapidjson.so test -f "$module" - vex=$(objdump -d --no-show-raw-insn "$module" \ - | grep -E '^[[:space:]]+[0-9a-f]+:[[:space:]]+[vk][a-z0-9]+' || true) - if [ -n "$vex" ]; then - printf '%s\n' "$vex" | awk 'NR <= 5' - echo "rapidjson.so uses VEX/EVEX encoded instructions; the build is not portable" >&2 + beyond=$(objdump -d --no-show-raw-insn "$module" \ + | grep -oE '^[[:space:]]+[0-9a-f]+:[[:space:]]+[a-z][a-z0-9]*' \ + | awk '{print $2}' | sort -u \ + | grep -E '^(v[a-z][a-z0-9]*|k[a-z][a-z0-9]*|popcnt|lzcnt|tzcnt|crc32|movbe|adcx|adox|prefetchw|andn|bextr|bls[imr]|bzhi|mulx|pdep|pext|rorx|sarx|shlx|shrx|aes[a-z]*|pclmul[a-z]*|sha1[a-z]*|sha256[a-z]*|pcmp[ei]str[im]|pcmpgtq|ptest|pblendw|pblendvb|blendp[sd]|blendvp[sd]|dpp[sd]|pmov[sz]x[a-z]*|pmulld|pmuldq|packusdw|phminposuw|mpsadbw|movntdqa|round[sp][sd]|insertps|extractps|phadd[a-z]*|phsub[a-z]*|pmaddubsw|pmulhrsw|pabs[bwd]|palignr|pshufb|psign[bwd]|lddqu|movddup|movs[hl]dup|addsubp[sd]|haddp[sd]|hsubp[sd]|pm(in|ax)(s[bd]|u[dw]))$' || true) + if [ -n "$beyond" ]; then + printf '%s\n' "$beyond" | awk 'NR <= 6' | tr '\n' ' ' + echo + echo "rapidjson.so uses instructions above the x86-64 baseline" >&2 exit 1 fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 007f29b..c4c82a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,14 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.release_env.outputs.version }} - run: gh release create "${VERSION}" --title "${VERSION}" --generate-notes + run: | + # keep this retryable: a failed upload below is re-run with the + # release already created + if gh release view "${VERSION}" >/dev/null 2>&1; then + echo "release ${VERSION} already exists" + else + gh release create "${VERSION}" --title "${VERSION}" --generate-notes + fi - name: Upload to luarocks env: From 16844dbd6d9a5f3786961edc153c1ae86ec9b044 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 14 Aug 2026 11:58:22 +0800 Subject: [PATCH 5/5] ci: widen the instruction scan and drop the persisted token Cover the extension families a compiler cannot reach from -march but inline intrinsics could: RDRAND/RDSEED, TSX, FSGSBASE, AMX and friends. The build job runs dependency and test code, so it has no reason to keep the workflow token in .git/config. --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 972b76f..e54a181 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: leafo/gh-actions-lua@v13 with: @@ -52,7 +54,7 @@ jobs: beyond=$(objdump -d --no-show-raw-insn "$module" \ | grep -oE '^[[:space:]]+[0-9a-f]+:[[:space:]]+[a-z][a-z0-9]*' \ | awk '{print $2}' | sort -u \ - | grep -E '^(v[a-z][a-z0-9]*|k[a-z][a-z0-9]*|popcnt|lzcnt|tzcnt|crc32|movbe|adcx|adox|prefetchw|andn|bextr|bls[imr]|bzhi|mulx|pdep|pext|rorx|sarx|shlx|shrx|aes[a-z]*|pclmul[a-z]*|sha1[a-z]*|sha256[a-z]*|pcmp[ei]str[im]|pcmpgtq|ptest|pblendw|pblendvb|blendp[sd]|blendvp[sd]|dpp[sd]|pmov[sz]x[a-z]*|pmulld|pmuldq|packusdw|phminposuw|mpsadbw|movntdqa|round[sp][sd]|insertps|extractps|phadd[a-z]*|phsub[a-z]*|pmaddubsw|pmulhrsw|pabs[bwd]|palignr|pshufb|psign[bwd]|lddqu|movddup|movs[hl]dup|addsubp[sd]|haddp[sd]|hsubp[sd]|pm(in|ax)(s[bd]|u[dw]))$' || true) + | grep -E '^(v[a-z][a-z0-9]*|k[a-z][a-z0-9]*|popcnt|lzcnt|tzcnt|crc32|movbe|adcx|adox|prefetchw|andn|bextr|bls[imr]|bzhi|mulx|pdep|pext|rorx|sarx|shlx|shrx|aes[a-z]*|pclmul[a-z]*|sha1[a-z]*|sha256[a-z]*|pcmp[ei]str[im]|pcmpgtq|ptest|pblendw|pblendvb|blendp[sd]|blendvp[sd]|dpp[sd]|pmov[sz]x[a-z]*|pmulld|pmuldq|packusdw|phminposuw|mpsadbw|movntdqa|round[sp][sd]|insertps|extractps|phadd[a-z]*|phsub[a-z]*|pmaddubsw|pmulhrsw|pabs[bwd]|palignr|pshufb|psign[bwd]|lddqu|movddup|movs[hl]dup|addsubp[sd]|haddp[sd]|hsubp[sd]|pm(in|ax)(s[bd]|u[dw])|rdrand|rdseed|rdpid|xbegin|xend|xabort|xtest|rd[fg]sbase|wr[fg]sbase|tile[a-z]*|ldtilecfg|sttilecfg|clwb|clflushopt|movdir[a-z0-9]*|serialize)$' || true) if [ -n "$beyond" ]; then printf '%s\n' "$beyond" | awk 'NR <= 6' | tr '\n' ' ' echo