Skip to content

Commit 94a7ba0

Browse files
authored
Simplify runtime detection and binary loader, add musl support (#186)
* Replace the complicated NAPI-RS loader and runtime detection with a simpler option that improves code security * Update CD * fix globalwithprocess * bump to 0.8.1
1 parent daab2cf commit 94a7ba0

11 files changed

Lines changed: 241 additions & 85 deletions

File tree

.github/workflows/release.yml

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ jobs:
3232
- host: ubuntu-24.04-arm
3333
target: aarch64-unknown-linux-gnu
3434
npm_platform: linux-arm64-gnu
35+
- host: ubuntu-latest
36+
target: x86_64-unknown-linux-musl
37+
npm_platform: linux-x64-musl
38+
docker: node:22-alpine
39+
- host: ubuntu-latest
40+
target: aarch64-unknown-linux-musl
41+
npm_platform: linux-arm64-musl
42+
docker: node:22-alpine
43+
docker_arch: linux/arm64
3544
steps:
3645
# Speed up apt-get operations by disabling unnecessary man-db auto-update
3746
- name: Disable man-db auto-update
@@ -52,19 +61,61 @@ jobs:
5261
- name: Setup Task runner
5362
uses: go-task/setup-task@v1
5463
- name: Install Linux dependencies
55-
if: runner.os == 'Linux'
64+
if: runner.os == 'Linux' && !matrix.settings.docker
5665
run: |
5766
sudo apt-get update
5867
sudo apt-get install -y libcups2-dev pkg-config clang
5968
- name: Install dependencies
69+
if: ${{ !matrix.settings.docker }}
6070
run: npm ci
61-
- name: Build
71+
- name: Build (native)
72+
if: ${{ !matrix.settings.docker }}
6273
shell: bash
6374
run: |
6475
# Our build script auto-detects the platform and builds for the current target
6576
echo "Building for target: ${{ matrix.settings.target }}"
6677
task build:napi
78+
- name: Setup QEMU for cross-compilation
79+
if: matrix.settings.docker && matrix.settings.docker_arch
80+
uses: docker/setup-qemu-action@v3
81+
with:
82+
platforms: ${{ matrix.settings.docker_arch }}
83+
- name: Build (Docker/musl)
84+
if: matrix.settings.docker
85+
uses: addnab/docker-run-action@v3
86+
with:
87+
image: ${{ matrix.settings.docker }}
88+
options: --user 0:0 --platform ${{ matrix.settings.docker_arch || 'linux/amd64' }} -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build
89+
run: |
90+
set -e
91+
92+
# Install build dependencies
93+
apk add --no-cache \
94+
cups-dev \
95+
pkgconf \
96+
clang \
97+
lld \
98+
musl-dev \
99+
rust \
100+
cargo \
101+
git
67102
103+
# Add Rust target
104+
rustup target add ${{ matrix.settings.target }}
105+
106+
# Enable corepack and install Node.js dependencies
107+
corepack enable
108+
npm ci
109+
110+
# Build the N-API module
111+
npx napi build --platform --release --target ${{ matrix.settings.target }} --esm --output-dir npm/${{ matrix.settings.npm_platform }}
112+
113+
# Run post-build scripts
114+
node scripts/remove-env-check.js --dir=npm/${{ matrix.settings.npm_platform }}
115+
node scripts/fix-platform-packages.js
116+
- name: Post-build cleanup
117+
shell: bash
118+
run: |
68119
# Remove all NPM directory contents except the current platform (cross-platform)
69120
cd npm/
70121
for dir in */; do

CLAUDE.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,13 @@ ALWAYS run these after changes through the taskfile:
178178
for cross-platform compatibility
179179
9. **Universal entrypoint**: Always import from `src/index.ts` for consistent
180180
runtime detection and behavior
181-
10. **Android support**: Intentionally excluded from N-API builds
182-
11. **Deno N-API support**: Deno requires `"nodeModulesDir": "auto"` in deno.json
181+
10. **Platform limitations**:
182+
- Android intentionally excluded from N-API builds
183+
11. **Musl support**: Linux musl (Alpine) fully supported
184+
- Detection via `process.report` API (no shell/filesystem access)
185+
- Built with Docker using `node:22-alpine` for security
186+
- Supports both x64 and arm64 architectures
187+
12. **Deno N-API support**: Deno requires `"nodeModulesDir": "auto"` in deno.json
183188
and `--allow-ffi` flag for N-API modules to work
184189

185190
## NAPI-RS Publishing and Release Workflow

CONTRIBUTING.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ The GitHub Actions release workflow handles cross-platform builds and npm publis
177177

178178
**Supported platforms:**
179179

180-
- **Linux**: x64-gnu, arm64-gnu (all runtimes)
181-
- **macOS**: x64, arm64 (all runtimes)
182-
- **Windows**: x64-msvc (all runtimes), arm64-msvc (Node.js only)
180+
- **Linux**: x64-gnu, arm64-gnu, x64-musl, arm64-musl (both glibc and musl supported)
181+
- **macOS**: x64, arm64
182+
- **Windows**: x64-msvc, arm64-msvc
183+
184+
**Musl Support:** Alpine Linux, BusyBox-based systems, and other musl-based distributions are fully supported. The library automatically detects musl vs glibc using Node.js's `process.report` API.
183185

184186
## Publishing Architecture
185187

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "printers-js"
33
authors = ["Evan Simkowitz <esimkowitz@users.noreply.github.com>"]
4-
version = "0.8.0"
4+
version = "0.8.1"
55
edition = "2021"
66
publish = false
77

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ Cross-runtime printer library for Node.js, Deno, and Bun with native performance
99
## Features
1010

1111
- 🔄 **Cross-runtime compatibility** - Node.js, Deno, and Bun support
12-
- 🖨️ **Cross-platform printing** - Windows, macOS, and Linux
12+
- 🖨️ **Cross-platform printing** - Windows, macOS, and Linux (glibc and musl)
1313
- 🦀 **Native performance** - Rust backend with Node-API bindings
1414
- 🔒 **Safe testing** - Simulation mode prevents accidental printing
1515
- 📊 **Real-time monitoring** - Printer state changes and job tracking
1616
- 🔧 **Flexible options** - Simple, CUPS, and raw printing configuration
1717
-**Async control** - Choose immediate return or wait for completion
1818

19+
## Platform Support
20+
21+
- **macOS**: x64, arm64
22+
- **Windows**: x64, arm64
23+
- **Linux**: x64, arm64 (both glibc and musl supported, including Alpine Linux)
24+
1925
## Installation
2026

2127
### Node.js

Taskfile.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ tasks:
4848

4949
test:direct:deno:
5050
desc: Run Deno tests directly
51-
cmd: "{{.SIMULATE}} deno test --allow-read --allow-write --allow-env --allow-ffi tests/shared.test.ts"
51+
cmd: "{{.SIMULATE}} deno test --allow-read --allow-write --allow-env --allow-ffi src/tests/shared.test.ts"
5252

5353
test:direct:node:
5454
desc: Run Node.js tests directly
55-
cmd: "{{.SIMULATE}} node tests/node-test-runner.mjs"
55+
cmd: "{{.SIMULATE}} npx tsx src/tests/node-test-runner.ts"
5656

5757
test:direct:bun:
5858
desc: Run Bun tests directly
59-
cmd: "{{.SIMULATE}} bun test tests/shared.test.ts"
59+
cmd: "{{.SIMULATE}} bun test src/tests/shared.test.ts"
6060

6161
test:doc:
6262
desc: Run documentation tests

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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@printers/printers",
3-
"version": "0.8.0",
3+
"version": "0.8.1",
44
"description": "Cross-platform printer library for Node.js, Deno, and Bun",
55
"type": "module",
66
"main": "dist/index.js",
@@ -16,7 +16,9 @@
1616
"x86_64-pc-windows-msvc",
1717
"aarch64-pc-windows-msvc",
1818
"x86_64-unknown-linux-gnu",
19-
"aarch64-unknown-linux-gnu"
19+
"aarch64-unknown-linux-gnu",
20+
"x86_64-unknown-linux-musl",
21+
"aarch64-unknown-linux-musl"
2022
]
2123
},
2224
"engines": {

scripts/remove-env-check.js

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
#!/usr/bin/env node
22

33
/**
4-
* Post-build script to remove the NAPI_RS_NATIVE_LIBRARY_PATH environment
5-
* variable check from the generated index.js file.
4+
* Post-build script to replace NAPI-RS generated loader with a minimal,
5+
* security-friendly version.
66
*
7-
* This eliminates the "unanalyzable dynamic import" warning when publishing to JSR.
7+
* This eliminates Socket.dev security concerns:
8+
* - Removes shell access (child_process.execSync)
9+
* - Removes filesystem reads (/usr/bin/ldd)
10+
* - Removes environment variable checks (NAPI_RS_ENFORCE_VERSION_CHECK)
11+
* - Removes musl detection from platform packages (detection moved to index.ts)
12+
*
13+
* Platform Support:
14+
* - macOS: darwin-x64, darwin-arm64
15+
* - Windows: win32-x64-msvc, win32-arm64-msvc
16+
* - Linux: linux-x64-gnu, linux-arm64-gnu, linux-x64-musl, linux-arm64-musl
17+
*
18+
* Note: Musl detection is handled in src/index.ts using process.report API
19+
* (no shell/filesystem access), keeping platform packages clean and secure.
820
*/
921

1022
import { existsSync, readFileSync, writeFileSync, readdirSync } from "fs";
@@ -36,51 +48,96 @@ function getIndexPaths() {
3648
return platforms.map(platform => join(npmDir, platform, "index.js"));
3749
}
3850

39-
const indexPaths = getIndexPaths();
51+
/**
52+
* Extract exported symbols from the original generated file
53+
*/
54+
function extractExports(content) {
55+
const exportMatch = content.match(/const\s*\{([^}]+)\}\s*=\s*nativeBinding/);
56+
if (!exportMatch) return null;
57+
58+
const symbols = exportMatch[1]
59+
.split(",")
60+
.map(s => s.trim())
61+
.filter(Boolean);
62+
63+
return symbols;
64+
}
65+
66+
/**
67+
* Generate a minimal, secure loader for the platform package
68+
*/
69+
function generateMinimalLoader(platform, exports) {
70+
const binaryName = `printers.${platform}.node`;
71+
72+
return `// prettier-ignore
73+
/* eslint-disable */
74+
/* Minimal secure loader - generated by remove-env-check.js */
75+
76+
import { createRequire } from 'node:module'
77+
const require = createRequire(import.meta.url)
78+
79+
let nativeBinding = null
80+
81+
try {
82+
nativeBinding = require('./${binaryName}')
83+
} catch (localErr) {
84+
try {
85+
nativeBinding = require('@printers/printers-${platform}')
86+
} catch (packageErr) {
87+
throw new Error(
88+
\`Failed to load native binding for ${platform}. \` +
89+
\`Please try reinstalling the package.\`,
90+
{ cause: [localErr, packageErr] }
91+
)
92+
}
93+
}
94+
95+
const { ${exports.join(", ")} } = nativeBinding
96+
${exports.map(sym => `export { ${sym} }`).join("\n")}
97+
`;
98+
}
4099

41100
function processIndexFile(indexPath) {
42101
if (!existsSync(indexPath)) {
43-
console.log(`⚠️ ${indexPath} not found - skipping env check removal`);
102+
console.log(`⚠️ ${indexPath} not found - skipping`);
44103
return false;
45104
}
46105

47106
try {
48107
const content = readFileSync(indexPath, "utf-8");
49108

50-
// Remove TypeScript-specific comments that cause issues with Deno
51-
let modifiedContent = content.replace(/\/\/\s*@ts-nocheck\s*\n/g, "");
52-
53-
// Remove the entire NAPI_RS_NATIVE_LIBRARY_PATH check block
54-
// This regex matches the if block and its else-if continuation
55-
// Updated to handle the current NAPI-RS structure
56-
modifiedContent = modifiedContent.replace(
57-
/\s+if\s+\(process\.env\.NAPI_RS_NATIVE_LIBRARY_PATH\)\s+\{[\s\S]*?\n\s*\}\s*else\s+if\s+\(process\.platform\s*===\s*['"]android['"]\)/,
58-
"\n if (process.platform === 'android')"
59-
);
60-
61-
if (content !== modifiedContent) {
62-
writeFileSync(indexPath, modifiedContent);
63-
console.log(
64-
`✅ Removed NAPI_RS_NATIVE_LIBRARY_PATH check from ${indexPath}`
65-
);
66-
return true;
67-
} else {
68-
console.log(
69-
`ℹ️ NAPI_RS_NATIVE_LIBRARY_PATH check not found or already removed in ${indexPath}`
70-
);
109+
// Extract platform from path (e.g., "darwin-arm64" from "npm/darwin-arm64/index.js")
110+
const platformMatch = indexPath.match(/npm[/\\]([^/\\]+)[/\\]index\.js/);
111+
if (!platformMatch) {
112+
console.log(`⚠️ Could not extract platform from ${indexPath}`);
71113
return false;
72114
}
115+
const platform = platformMatch[1];
116+
117+
// Extract exported symbols
118+
const exports = extractExports(content);
119+
if (!exports) {
120+
console.log(`⚠️ Could not extract exports from ${indexPath}`);
121+
return false;
122+
}
123+
124+
// Generate minimal loader
125+
const minimalContent = generateMinimalLoader(platform, exports);
126+
127+
writeFileSync(indexPath, minimalContent);
128+
console.log(`✅ Replaced loader with minimal version: ${indexPath}`);
129+
return true;
73130
} catch (error) {
74131
console.error(`❌ Error processing ${indexPath}:`, error);
75132
throw error;
76133
}
77134
}
78135

136+
const indexPaths = getIndexPaths();
137+
79138
try {
80139
if (indexPaths.length === 0) {
81-
console.log(
82-
"⚠️ No npm platform directories found - skipping env check removal"
83-
);
140+
console.log("⚠️ No npm platform directories found");
84141
process.exit(0);
85142
}
86143

0 commit comments

Comments
 (0)