Skip to content

Commit a66ac8c

Browse files
boybookclaude
andauthored
feat: add pure C shim layer for cross-compiler ABI safety (#1)
* feat: add pure C shim layer for cross-compiler ABI safety on Windows Introduce a shim layer between the Node.js addon (MSVC) and Hamlib (MinGW), eliminating the MSVC/MinGW struct layout incompatibility that caused immediate crashes on Windows when importing the module. Architecture: Node.js → hamlib.node (C++ NAPI, MSVC) → hamlib_shim (pure C) → libhamlib Key changes: - New src/shim/hamlib_shim.{h,c}: ~95 pure C wrapper functions with opaque handle pattern, replacing all direct Hamlib struct accesses - Rewrite src/hamlib.cpp to use shim_rig_* functions exclusively (no conditional compilation, all platforms use same code path) - New scripts/build-shim.js: cross-platform shim build script (static .a on Linux/macOS, DLL on Windows via MinGW) - Simplified binding.gyp: removed complex Hamlib path search logic, pthread dependency, and MinGW conditional compilation for Windows - Updated CI: added MSYS2/MinGW setup for Windows shim DLL build - Removed obsolete hamlib_compat.h and 7 temporary debug scripts Verified on Windows: module loads and all 84 API methods work correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: separate shim build flow for Windows vs Unix in CI On Unix, build-all.js handles hamlib setup + shim build + addon build in sequence. On Windows, shim DLL is built separately via MSYS2 before the MSVC addon build. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: CI Windows shim build - use pwsh shell with MINGW_CC env var The MSYS2 shell doesn't have Node.js in PATH. Use pwsh shell instead and set MINGW_CC to point to the MSYS2 MinGW gcc. Also add fail-fast: false to allow all platform builds to complete independently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: move shim output to shim-build/ to survive prebuildify rebuild prebuildify internally runs node-gyp rebuild which cleans the build/ directory, deleting the previously built shim .a/.lib files. Moving shim output to a separate shim-build/ directory prevents this. Also adds pip install setuptools to CI for Python 3.12 compatibility with node-gyp 9.4.1 (distutils was removed in Python 3.12). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: copy all libhamlib.so* variants for Linux bundling The dynamic linker needs the soname-versioned file (e.g., libhamlib.so.5) but the bundler was only copying a single file. Now copies all libhamlib.so* files from the lib directory, resolving symlinks. Also improved findHamlibSo() to search for any version (so.4, so.5, etc.) instead of hardcoding so.4. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add CI functional test for compiled binary validation Adds test_ci_functional.js that validates the compiled binary actually works by exercising core API operations against the Hamlib Dummy device (Model 1). Tests VFO, frequency, PTT, serial config, power control, and instance lifecycle - no rigctld or hardware needed. CI now runs both test_loader.js (API existence) and test_ci_functional.js (API functionality) to ensure binaries are usable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: add automated npm publish workflow on tag push Replaces the manual package/release flow with automated publishing: - Tag push (v*) triggers: build → validate → npm publish → GitHub Release - Validates all 5 platform prebuilds before publishing - Uses NPM_TOKEN secret for npm authentication - Creates GitHub Release with per-platform tar.gz archives - Removes cleanup job (artifacts auto-expire) - Adds PUBLISHING.md with release instructions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: rewrite CLAUDE.md for shim-layer architecture Rewrites the project guide to reflect the current shim-layer architecture. Includes clear step-by-step instructions for adding new API methods across all 4 layers (shim → C++ → JS → TS). Condensed from 333 to 131 lines while retaining all essential info. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d9583c4 commit a66ac8c

15 files changed

Lines changed: 3399 additions & 1773 deletions

.github/workflows/build.yml

Lines changed: 64 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
name: Build Precompiled Binaries
1+
name: Build and Publish
22

33
on:
44
push:
5-
branches: [ main, develop ]
6-
tags: [ 'v*' ]
5+
branches: [main, develop]
6+
tags: ['v*']
77
pull_request:
8-
branches: [ main ]
8+
branches: [main]
99

1010
jobs:
1111
build:
1212
env:
1313
HAMLIB_VERSION: '4.6.5'
14-
PTHREADS_SOURCEWARE_DIR: 'prebuilt-dll-2-9-1-release'
1514

1615
strategy:
16+
fail-fast: false
1717
matrix:
1818
include:
1919
- os: ubuntu-latest
@@ -38,6 +38,9 @@ jobs:
3838
node-version: '18'
3939
cache: 'npm'
4040

41+
- name: Install Python setuptools (for node-gyp)
42+
run: pip3 install setuptools --break-system-packages || pip3 install setuptools || true
43+
4144
- name: Install system dependencies (Linux)
4245
if: startsWith(matrix.os, 'ubuntu')
4346
run: |
@@ -76,177 +79,100 @@ jobs:
7679
Write-Host "HAMLIB root: $root"
7780
"HAMLIB_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
7881
79-
- name: Setup Windows pthread
82+
- name: Setup MSYS2 MinGW (Windows)
8083
if: matrix.os == 'windows-latest'
81-
shell: pwsh
82-
run: |
83-
$dir = $env:PTHREADS_SOURCEWARE_DIR
84-
$base = "https://sourceware.org/pub/pthreads-win32/$dir"
85-
$root = Join-Path $env:RUNNER_TEMP 'pthreads-root'
86-
New-Item -ItemType Directory -Path (Join-Path $root 'lib/x64') -Force | Out-Null
87-
88-
foreach ($f in @('pthread.h','sched.h','semaphore.h')) {
89-
Invoke-WebRequest -Uri "$base/include/$f" -OutFile (Join-Path $root $f)
90-
}
91-
Invoke-WebRequest -Uri "$base/lib/x64/pthreadVC2.lib" -OutFile (Join-Path $root 'lib/x64/pthreadVC2.lib')
92-
"PTHREAD_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
84+
uses: msys2/setup-msys2@v2
85+
with:
86+
msystem: MINGW64
87+
location: D:\msys2
88+
install: mingw-w64-x86_64-gcc mingw-w64-x86_64-binutils
9389

9490
- name: Setup MSVC (Windows)
9591
if: matrix.os == 'windows-latest'
9692
uses: ilammy/msvc-dev-cmd@v1
9793
with:
9894
arch: x64
9995

100-
- name: Fix Hamlib import library (Windows)
96+
- name: Install Node.js dependencies
97+
run: npm ci --ignore-scripts
98+
99+
- name: Build shim DLL (Windows)
101100
if: matrix.os == 'windows-latest'
102101
shell: pwsh
102+
env:
103+
MINGW_CC: D:\msys2\msys64\mingw64\bin\gcc.exe
103104
run: |
104-
$dllPath = Get-ChildItem -Path (Join-Path $env:HAMLIB_ROOT 'bin') -Filter 'libhamlib-*.dll' | Select-Object -First 1
105-
$defPath = Join-Path $env:RUNNER_TEMP 'libhamlib-4.def'
106-
$libPath = Join-Path $env:HAMLIB_ROOT 'lib/libhamlib-4.lib'
107-
108-
$exports = dumpbin /exports $dllPath.FullName
109-
$defContent = "LIBRARY libhamlib-4.dll`nEXPORTS`n"
110-
$inExports = $false
111-
foreach ($line in $exports -split "`n") {
112-
if ($line -match '^\s+ordinal\s+hint\s+RVA\s+name') { $inExports = $true; continue }
113-
if ($inExports -and $line -match '^\s+\d+\s+[0-9A-F]+\s+[0-9A-F]+\s+(\S+)') {
114-
$defContent += " $($matches[1])`n"
115-
}
116-
}
117-
118-
Set-Content -Path $defPath -Value $defContent -Encoding ASCII
119-
$libDir = Join-Path $env:HAMLIB_ROOT 'lib'
120-
if (-not (Test-Path $libDir)) { New-Item -ItemType Directory -Path $libDir -Force | Out-Null }
121-
lib /def:$defPath /out:$libPath /machine:x64
105+
$env:Path = "D:\msys2\msys64\mingw64\bin;$env:Path"
106+
node scripts/build-shim.js --verbose
122107
123-
- name: Install Node.js dependencies
124-
run: npm ci --ignore-scripts
108+
- name: Build (Windows)
109+
if: matrix.os == 'windows-latest'
110+
run: npm run build:all -- --skip-hamlib --skip-shim --minimal
125111

126-
- name: Build (Linux/macOS - unified)
112+
- name: Build (Unix)
127113
if: matrix.os != 'windows-latest'
128-
run: |
129-
npm run build:all -- --minimal
130-
npm run verify
114+
run: npm run build:all -- --minimal
131115

132-
- name: Build (Windows - staged)
133-
if: matrix.os == 'windows-latest'
116+
- name: Run tests
134117
run: |
135-
node scripts/run-prebuildify.js
136-
npm run bundle
137-
npm run verify
118+
node test/test_loader.js
119+
node test/test_ci_functional.js
138120
139-
- name: Upload prebuilds artifact
121+
- name: Upload prebuilds
140122
uses: actions/upload-artifact@v4
141123
with:
142-
name: prebuilds-${{ matrix.target }}
143-
path: prebuilds/**
144-
retention-days: 1
124+
name: prebuilt-${{ matrix.target }}
125+
path: prebuilds/
126+
retention-days: 7
145127

146-
package:
128+
publish:
129+
name: Publish
147130
needs: build
148131
runs-on: ubuntu-latest
149-
132+
if: startsWith(github.ref, 'refs/tags/v')
133+
permissions:
134+
contents: write
150135
steps:
151136
- uses: actions/checkout@v4
152137

153-
- name: Setup Node.js
154-
uses: actions/setup-node@v4
138+
- uses: actions/setup-node@v4
155139
with:
156140
node-version: '18'
141+
registry-url: 'https://registry.npmjs.org'
157142

158-
- name: Download all prebuilds
159-
uses: actions/download-artifact@v4
143+
- uses: actions/download-artifact@v4
160144
with:
161-
path: prebuilds-temp
145+
path: all-artifacts
162146

163147
- name: Organize prebuilds
164148
run: |
165149
mkdir -p prebuilds
166-
for platform_dir in prebuilds-temp/prebuilds-*/; do
167-
if [ -d "$platform_dir/prebuilds" ]; then
168-
mv "$platform_dir/prebuilds"/* prebuilds/ 2>/dev/null || true
169-
else
170-
mv "$platform_dir"/* prebuilds/ 2>/dev/null || true
171-
fi
150+
for dir in all-artifacts/prebuilt-*/; do
151+
[ -d "$dir" ] && cp -r "$dir"/* prebuilds/
172152
done
173153
174-
- name: Install dependencies
175-
run: npm ci --ignore-scripts
176-
177-
- name: Verify build
178-
run: npm run verify:all
179-
180-
- name: Create build info
154+
- name: Validate prebuilds
181155
run: |
182-
cat > prebuilds/BUILD_INFO.txt << EOF
183-
Node-HamLib Precompiled Binaries
184-
================================
185-
Build Time: $(date -u)
186-
Git Commit: ${{ github.sha }}
187-
Git Ref: ${{ github.ref }}
188-
189-
Supported Platforms:
190-
EOF
191-
192-
for dir in prebuilds/*/; do
193-
if [ -d "$dir" ]; then
194-
echo "- $(basename "$dir")" >> prebuilds/BUILD_INFO.txt
195-
fi
156+
for p in linux-x64 linux-arm64 darwin-arm64 darwin-x64 win32-x64; do
157+
test -f "prebuilds/$p/node.napi.node" || { echo "Missing: $p"; exit 1; }
196158
done
159+
echo "All 5 platform prebuilds verified."
160+
find prebuilds -name "*.node" -exec ls -la {} \;
197161
198-
echo "" >> prebuilds/BUILD_INFO.txt
199-
echo "Installation: Extract to your project's prebuilds/ directory" >> prebuilds/BUILD_INFO.txt
162+
- run: npm ci --ignore-scripts
200163

201-
cd prebuilds
202-
zip -r ../node-hamlib-prebuilds.zip . -x "*.DS_Store"
203-
cd ..
204-
205-
- name: Upload unified package
206-
uses: actions/upload-artifact@v4
207-
with:
208-
name: node-hamlib-prebuilds
209-
path: node-hamlib-prebuilds.zip
210-
retention-days: 90
164+
- run: npm publish
165+
env:
166+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
211167

212168
- name: Create GitHub Release
213-
if: startsWith(github.ref, 'refs/tags/v')
214-
uses: ncipollo/release-action@v1
215-
with:
216-
tag: ${{ github.ref_name }}
217-
name: "Node-HamLib ${{ github.ref_name }}"
218-
artifacts: node-hamlib-prebuilds.zip
219-
body: |
220-
# Node-HamLib ${{ github.ref_name }}
221-
222-
## Precompiled Binaries
223-
224-
Includes precompiled binaries for:
225-
- Linux x64/ARM64
226-
- macOS ARM64 (Apple Silicon) & x64 (Intel)
227-
- Windows x64
228-
229-
### Installation
230-
231-
```bash
232-
npm install node-hamlib
233-
```
234-
235-
Binaries are automatically detected and used when available.
236-
237-
cleanup:
238-
needs: [build, package]
239-
runs-on: ubuntu-latest
240-
if: always()
241-
242-
steps:
243-
- name: Delete temporary artifacts
244-
uses: geekyeggo/delete-artifact@v5
245-
with:
246-
name: |
247-
prebuilds-linux-x64
248-
prebuilds-linux-arm64
249-
prebuilds-darwin-arm64
250-
prebuilds-darwin-x64
251-
prebuilds-win32-x64
252-
failOnError: false
169+
env:
170+
GH_TOKEN: ${{ github.token }}
171+
run: |
172+
VERSION=${GITHUB_REF#refs/tags/}
173+
for dir in prebuilds/*/; do
174+
platform=$(basename "$dir")
175+
tar -czf "node-hamlib-${VERSION}-${platform}.tar.gz" -C prebuilds "$platform"
176+
done
177+
gh release create "$VERSION" --title "Node-HamLib $VERSION" --generate-notes \
178+
node-hamlib-${VERSION}-*.tar.gz

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Build outputs
22
build/
3+
shim-build/
34
*.node
45
prebuilds/
56

@@ -50,4 +51,5 @@ temp/
5051
build.sh
5152
Dockerfile
5253

53-
Hamlib/
54+
Hamlib/
55+
hamlib/

0 commit comments

Comments
 (0)