Skip to content

Commit c1e6ef5

Browse files
committed
Release wsjtx-lib 2.1.1
1 parent 726c14b commit c1e6ef5

6 files changed

Lines changed: 103 additions & 15 deletions

File tree

.github/workflows/build.yml

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ jobs:
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

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ Runtime binary loading uses `node-gyp-build` with prebuildify layout
4747
(`prebuilds/<platform>-<arch>/*.node`), and falls back to
4848
`build/Release/*.node` for local development builds.
4949

50+
Linux prebuilds are built and checked so neither `wsjtx_lib_nodejs.node` nor
51+
`libwsjtx_core.so` requires an executable stack. Downstreams should not patch
52+
`PT_GNU_STACK` or require `GLIBC_TUNABLES=glibc.rtld.execstack=2`; if that
53+
condition appears, treat it as a source/build regression.
54+
5055
### Building from Source
5156

5257
Only needed if prebuilt binaries are not available for your platform.

docs/prebuilt-bundling.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
- Linux:设置(或覆盖)RPATH/RUNPATH 为 `$ORIGIN`(同级目录)。
2323
- Windows:无需 rpath,DLL 与 `.node` 同级即可被加载器找到。
2424
- 验证:在 CI 中使用 `otool -L`(macOS)、`ldd`(Linux)、`objdump -p`(Windows)输出检查结果,确保无绝对路径残留且依赖可解析。
25+
- Linux 预构建必须原生不要求 executable stack。Fortran callback 结构不得依赖 gfortran stack trampoline;下游不应通过清理 ELF `PT_GNU_STACK` 标记来掩盖源码问题。
2526

2627
平台做法与所用工具
2728

@@ -49,13 +50,14 @@ dylibbundler \
4950
- 验证:
5051
```
5152
otool -L prebuilds/darwin-arm64/wsjtx_lib_nodejs.node
53+
otool -l prebuilds/darwin-arm64/wsjtx_lib_nodejs.node | grep -A2 LC_RPATH
5254
```
53-
应看到 `@loader_path/libfftw3f.3.dylib` 等(同级目录),无 Homebrew 绝对路径。
55+
应看到 `@loader_path/libfftw3f.3.dylib` 等(同级目录),无 Homebrew 绝对路径,且同一个 `LC_RPATH` 不应重复出现
5456

5557
2) Linux(使用 `patchelf` + `ldd`
5658

5759
- 依赖:`apt-get install -y patchelf`(以及 `ldd`
58-
- 复制依赖:用 `ldd` 列出 `.node` 的依赖,筛选并复制目标库到与 `.node` 同级目录:
60+
- 复制依赖:用 `ldd` 列出 `.node` `libwsjtx_core.so` 的依赖,筛选并复制目标库到与 `.node` 同级目录:
5961
- 包括 `libfftw3f*``libgfortran*``libquadmath*``libgcc_s*`(可酌情包含 `libstdc++*`)。
6062
- 排除 glibc(`libc`, `libm`, `libpthread`, `ld-linux` 等)。
6163
- 设置 RPATH:
@@ -65,8 +67,11 @@ patchelf --set-rpath '$ORIGIN' prebuilds/linux-*/wsjtx_lib_nodejs.node
6567
- 验证:
6668
```
6769
ldd prebuilds/linux-*/wsjtx_lib_nodejs.node | grep -v 'linux-vdso\|ld-linux\|libc\|libm\|libpthread\|libdl'
70+
ldd prebuilds/linux-*/libwsjtx_core.so | grep -v 'linux-vdso\|ld-linux\|libc\|libm\|libpthread\|libdl'
71+
readelf -W -l prebuilds/linux-*/wsjtx_lib_nodejs.node | grep GNU_STACK
72+
readelf -W -l prebuilds/linux-*/libwsjtx_core.so | grep GNU_STACK
6873
```
69-
应无 "not found",关键库解析到同目录。
74+
应无 "not found",关键库解析到同目录`GNU_STACK` flags 不得包含 `E`
7075

7176
3) Windows(MinGW/MSYS2 工具链)
7277

@@ -79,20 +84,22 @@ CI 集成(简化、可重复)
7984

8085
- **所有平台统一方案**:将依赖库与 `.node` 文件放在同级目录
8186
- macOS:用 `dylibbundler` 自动复制并改写到 `prebuilds/darwin-*/`;输出 `otool -L` 结果到日志。
82-
- Linux:安装 `patchelf`;复制 `ldd` 识别出的目标库到 `prebuilds/linux-*/``patchelf --set-rpath '$ORIGIN'`;输出 `ldd` 结果。
87+
- macOS:`dylibbundler` 后清理重复 `LC_RPATH` 并重新 ad-hoc codesign,避免下游 Electron/App 打包时再次遇到重复 rpath 或签名失效问题。
88+
- Linux:安装 `patchelf`;复制 `.node``libwsjtx_core.so``ldd` 识别出的目标库到 `prebuilds/linux-*/``patchelf --set-rpath '$ORIGIN'`;输出 `ldd` 结果。
8389
- Windows:用 `objdump` 枚举 DLL 并从 MinGW 目录复制到与 `.node` 同级;输出依赖列表。
8490

8591
发布前校验
8692

8793
- 生成 `prebuilds/*/` 后:
8894
- macOS:`otool -L` 检查路径应为 `@loader_path/<lib>.dylib`(同级目录)。
89-
- Linux:`ldd` 无 "not found",并且关键库解析到同目录。
95+
- Linux:`ldd` 无 "not found",并且关键库解析到同目录`readelf -W -l` 检查 `.node``libwsjtx_core.so``GNU_STACK` 不含 execute flag
9096
- Windows:`objdump -p` 的 DLL 在同级目录存在。
9197

9298
注意事项
9399

94100
- macOS 改写依赖会使原签名失效,dylibbundler 会进行 ad-hoc 签名;若集成到 Electron/App,需对最终产物统一签名/公证。
95101
- Linux 不要尝试捆绑或静态链接 glibc;可考虑 `-static-libstdc++ -static-libgcc` 减少 .so 数量。
102+
- Linux 不要发布需要 `GLIBC_TUNABLES=glibc.rtld.execstack=2` 的产物;如果 `readelf` 发现 `GNU_STACK``E`,应回到 Fortran 源码查找 internal procedure callback / trampoline,而不是对产物做 post-build patch。
96103
- **所有平台统一采用同级目录方案**,避免嵌套路径导致的 `@loader_path/native/native/` 等问题(详见下方"常见陷阱")。
97104

98105
## 常见陷阱与解决方案

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "wsjtx-lib",
3-
"version": "2.1.0",
3+
"version": "2.1.1",
44
"description": "Node.js C++ extension for WSJTX digital radio protocol library",
55
"type": "module",
66
"main": "dist/src/index.js",

0 commit comments

Comments
 (0)