4141 run : |
4242 sudo apt-get update
4343 sudo apt-get install -y cmake build-essential gfortran libfftw3-dev libboost-all-dev pkg-config patchelf
44+ printf '%s\n' \
45+ 'subroutine trampoline_check' \
46+ 'end subroutine trampoline_check' \
47+ > /tmp/trampoline-check.f90
48+ if gfortran -Wtrampolines -Werror=trampolines -c /tmp/trampoline-check.f90 -o /tmp/trampoline-check.o; then
49+ echo "FFLAGS=-Wtrampolines -Werror=trampolines" >> "$GITHUB_ENV"
50+ fi
4451
4552 # macOS dependencies
4653 - name : Install dependencies (macOS)
@@ -135,16 +142,35 @@ jobs:
135142 cp build/Release/libwsjtx_core.so "$TARGET_DIR/" 2>/dev/null || true
136143
137144 # Bundle runtime shared libraries
138- ldd "$NODE_FILE" | awk '{print $3}' | while read lib; do
139- if [ -f "$lib" ] && [[ "$lib" == *libfftw* || "$lib" == *libgfortran* || "$lib" == *libgcc* || "$lib" == *libquadmath* || "$lib" == *libstdc++* ]]; then
140- cp -n "$lib" "$TARGET_DIR/" 2>/dev/null || true
141- fi
145+ for binary in "$NODE_FILE" "$TARGET_DIR/libwsjtx_core.so"; do
146+ [ -f "$binary" ] || continue
147+ ldd "$binary" | awk '{print $3}' | while read lib; do
148+ if [ -f "$lib" ] && [[ "$lib" == *libfftw* || "$lib" == *libgfortran* || "$lib" == *libgcc* || "$lib" == *libquadmath* || "$lib" == *libstdc++* ]]; then
149+ cp -n "$lib" "$TARGET_DIR/" 2>/dev/null || true
150+ fi
151+ done
142152 done
143153 patchelf --set-rpath '$ORIGIN' "$NODE_FILE" || true
144154 patchelf --set-rpath '$ORIGIN' "$TARGET_DIR/libwsjtx_core.so" 2>/dev/null || true
145155
146- echo '{}' | jq --arg p "${{ matrix.platform }}" --arg a "${{ matrix.arch }}" \
147- '{platform: $p, arch: $a, build_time: now | todate}' > "$TARGET_DIR/build-info.json"
156+ stack_report=""
157+ for elf in "$NODE_FILE" "$TARGET_DIR/libwsjtx_core.so"; do
158+ [ -f "$elf" ] || continue
159+ flags=$(readelf -W -l "$elf" | awk '/GNU_STACK/ {print $(NF-1)}')
160+ echo "GNU_STACK $(basename "$elf"): ${flags:-missing}"
161+ stack_report="$stack_report $(basename "$elf")=${flags:-missing}"
162+ if [ -z "$flags" ]; then
163+ echo "Missing GNU_STACK program header in Linux prebuild: $elf" >&2
164+ exit 1
165+ fi
166+ if [[ "$flags" == *E* ]]; then
167+ echo "Executable stack is not allowed in Linux prebuilds: $elf ($flags)" >&2
168+ exit 1
169+ fi
170+ done
171+
172+ echo '{}' | jq --arg p "${{ matrix.platform }}" --arg a "${{ matrix.arch }}" --arg stack "$stack_report" \
173+ '{platform: $p, arch: $a, build_time: now | todate, requires_executable_stack: false, gnu_stack: ($stack | ltrimstr(" "))}' > "$TARGET_DIR/build-info.json"
148174 ls -la "$TARGET_DIR"
149175
150176 - name : Package prebuilds (macOS)
@@ -166,6 +192,43 @@ jobs:
166192 # dylibbundler follows transitive deps: .node → libwsjtx_core.dylib → fftw, gfortran, etc.
167193 dylibbundler -x "$NODE_FILE" -d "$TARGET_DIR" -p "@loader_path/" $SP_ARGS -b -of
168194
195+ # dylibbundler can append the same LC_RPATH more than once when
196+ # transitive dependencies already carry matching rpaths. Keep the
197+ # packaged Mach-O files deterministic and easy to re-sign downstream.
198+ python3 - <<'PY'
199+ import subprocess
200+ from pathlib import Path
201+
202+ target = Path("prebuilds/${{ matrix.platform }}-${{ matrix.arch }}")
203+ files = [target / "wsjtx_lib_nodejs.node", *sorted(target.glob("*.dylib"))]
204+
205+ def rpaths(path):
206+ out = subprocess.check_output(["otool", "-l", str(path)], text=True)
207+ paths = []
208+ lines = out.splitlines()
209+ for i, line in enumerate(lines):
210+ if line.strip() == "cmd LC_RPATH":
211+ for j in range(i, min(i + 8, len(lines))):
212+ s = lines[j].strip()
213+ if s.startswith("path "):
214+ paths.append(s.split(" ", 2)[1])
215+ return paths
216+
217+ for file in files:
218+ while file.exists():
219+ paths = rpaths(file)
220+ duplicate = next((p for p in paths if paths.count(p) > 1), None)
221+ if duplicate is None:
222+ break
223+ print(f"Deleting duplicate LC_RPATH {duplicate} from {file}")
224+ subprocess.check_call(["install_name_tool", "-delete_rpath", duplicate, str(file)])
225+ PY
226+
227+ for f in "$NODE_FILE" "$TARGET_DIR"/*.dylib; do
228+ [ -f "$f" ] || continue
229+ codesign --force --deep --preserve-metadata=entitlements,requirements,flags,runtime --sign - "$f"
230+ done
231+
169232 echo '{}' | jq --arg p "${{ matrix.platform }}" --arg a "${{ matrix.arch }}" \
170233 '{platform: $p, arch: $a, build_time: now | todate}' > "$TARGET_DIR/build-info.json"
171234 ls -la "$TARGET_DIR"
@@ -255,6 +318,19 @@ jobs:
255318 for p in linux-x64 linux-arm64 darwin-arm64 darwin-x64 win32-x64; do
256319 test -f "prebuilds/$p/wsjtx_lib_nodejs.node" || { echo "Missing: $p"; exit 1; }
257320 done
321+ for elf in prebuilds/linux-*/wsjtx_lib_nodejs.node prebuilds/linux-*/libwsjtx_core.so; do
322+ test -f "$elf" || { echo "Missing Linux ELF: $elf"; exit 1; }
323+ flags=$(readelf -W -l "$elf" | awk '/GNU_STACK/ {print $(NF-1)}')
324+ echo "GNU_STACK $elf: ${flags:-missing}"
325+ if [ -z "$flags" ]; then
326+ echo "Missing GNU_STACK program header in Linux prebuild: $elf" >&2
327+ exit 1
328+ fi
329+ if [[ "$flags" == *E* ]]; then
330+ echo "Executable stack is not allowed in Linux prebuilds: $elf ($flags)" >&2
331+ exit 1
332+ fi
333+ done
258334 echo "All 5 platform prebuilds verified."
259335
260336 - run : npm publish
0 commit comments