Skip to content

Commit 3ae841f

Browse files
authored
fix: Replace simple-binary-install with custom implementation for pnpm compatibility (#170)
## Summary - Replaces `simple-binary-install` with custom implementation using `node-fetch` and `tar` - Fixes pnpm compatibility issues caused by pnpm's symlink-based directory structure - Adds comprehensive CI tests for npm and pnpm installation on all platforms ## Problem The `cx-protofetch` npm package was incompatible with pnpm due to issues with the `simple-binary-install` library. pnpm uses a symlink-based directory structure that causes the library to calculate incorrect extraction paths, resulting in `Z_DATA_ERROR: incorrect header check` errors during binary extraction. ## Solution Implemented a custom binary download and extraction solution that: - Downloads binaries directly using `node-fetch` - Extracts tarballs using the `tar` library - Works correctly with both npm and pnpm directory structures - Provides better error handling and logging ## Changes ### Package Code - **package.json**: Replaced `simple-binary-install` with `node-fetch` and `tar` dependencies - **getBinary.js**: Complete rewrite with direct download and extraction logic - **scripts.js**: Updated to handle async `downloadBinary` function - **run.js**: Updated to spawn binary from `bin/` directory - **.gitignore**: Added `bin/` directory to ignore list ### CI/CD - Added `test-npm-package` job to CI workflow - Tests npm installation on Ubuntu, macOS (Intel & ARM), Windows - Tests pnpm installation on all platforms - Release job now depends on npm package tests passing ## Testing Performed - Tested npm installation locally on macOS ARM64 - Tested pnpm installation locally on macOS ARM64 - Verified binary execution with `--version` and `--help` - CI tests will verify on all platforms ## Breaking Changes None - the API remains unchanged. Consumers use the package the same way.
1 parent bf348d0 commit 3ae841f

File tree

6 files changed

+247
-21
lines changed

6 files changed

+247
-21
lines changed

.github/npm/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/package-lock.json
22
/node_modules/
3+
/bin/

.github/npm/getBinary.js

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,112 @@
1-
import { Binary } from 'simple-binary-install';
2-
import * as os from 'os';
3-
import * as fs from 'fs';
1+
import fetch from 'node-fetch';
2+
import { mkdirSync, chmodSync, existsSync, readFileSync } from 'fs';
3+
import { fileURLToPath } from 'url';
4+
import { dirname, join } from 'path';
5+
import { pipeline } from 'stream/promises';
6+
import * as tar from 'tar';
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const __dirname = dirname(__filename);
410

511
function getPlatform() {
6-
const type = os.type();
7-
const arch = os.arch();
12+
const type = process.platform;
13+
const arch = process.arch;
814

9-
if (type === 'Windows_NT' && arch === 'x64') {
15+
if (type === 'win32' && arch === 'x64') {
1016
return 'x86_64-pc-windows-msvc';
1117
}
1218

13-
if (type === 'Linux' && arch === 'x64') {
19+
if (type === 'linux' && arch === 'x64') {
1420
return 'x86_64-unknown-linux-musl';
1521
}
1622

17-
if (type === 'Linux' && arch === 'arm64') {
23+
if (type === 'linux' && arch === 'arm64') {
1824
return 'aarch64-unknown-linux-musl';
1925
}
2026

21-
if (type === 'Darwin' && arch === 'x64') {
27+
if (type === 'darwin' && arch === 'x64') {
2228
return 'x86_64-apple-darwin';
2329
}
2430

25-
if (type === 'Darwin' && arch === 'arm64') {
31+
if (type === 'darwin' && arch === 'arm64') {
2632
return 'aarch64-apple-darwin';
2733
}
2834

2935
throw new Error(`Unsupported platform: ${type} ${arch}. Please create an issue at https://github.com/coralogix/protofetch/issues`);
3036
}
3137

32-
export function getBinary() {
38+
function getVersion() {
39+
const packageJsonPath = join(__dirname, 'package.json');
40+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
41+
return packageJson.version;
42+
}
43+
44+
async function downloadBinary(options = {}) {
3345
const platform = getPlatform();
34-
const { version } = JSON.parse(fs.readFileSync('./package.json'));
35-
const url = `https://github.com/coralogix/protofetch/releases/download/v${version}/protofetch_${platform}.tar.gz`;
36-
const name = 'protofetch';
46+
const version = getVersion();
47+
48+
// Support custom URL for testing (not exposed via postinstall, only via direct script call)
49+
const url = options.url || `https://github.com/coralogix/protofetch/releases/download/v${version}/protofetch_${platform}.tar.gz`;
50+
51+
const binDir = join(__dirname, 'bin');
52+
const isWindows = process.platform === 'win32';
53+
const binaryName = isWindows ? 'protofetch.exe' : 'protofetch';
54+
const binaryPath = join(binDir, binaryName);
55+
56+
if (!existsSync(binDir)) {
57+
mkdirSync(binDir, { recursive: true });
58+
}
59+
60+
console.log(`Downloading protofetch binary from ${url}...`);
3761

38-
return new Binary(name, url)
62+
let lastError;
63+
for (let attempt = 1; attempt <= 3; attempt++) {
64+
try {
65+
const response = await fetch(url, {
66+
redirect: 'follow',
67+
timeout: 60000
68+
});
69+
70+
if (!response.ok) {
71+
throw new Error(`Failed to download binary (HTTP ${response.status}): ${response.statusText}`);
72+
}
73+
74+
await pipeline(
75+
response.body,
76+
tar.extract({
77+
cwd: binDir,
78+
strip: 1,
79+
strict: true,
80+
preservePaths: false,
81+
preserveOwner: false,
82+
filter: (path, entry) => {
83+
const allowedFiles = ['protofetch', 'protofetch.exe'];
84+
const fileName = path.split('/').pop();
85+
return entry.type === 'File' && allowedFiles.includes(fileName);
86+
}
87+
})
88+
);
89+
90+
if (!isWindows && existsSync(binaryPath)) {
91+
chmodSync(binaryPath, 0o755);
92+
}
93+
94+
if (existsSync(binaryPath)) {
95+
console.log('protofetch binary installed successfully');
96+
return;
97+
} else {
98+
throw new Error(`Binary extraction failed - ${binaryName} not found after extraction`);
99+
}
100+
} catch (error) {
101+
lastError = error;
102+
if (attempt < 3) {
103+
console.log(`Download attempt ${attempt} failed, retrying...`);
104+
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
105+
}
106+
}
107+
}
108+
109+
throw new Error(`Failed to download protofetch after 3 attempts: ${lastError.message}`);
39110
}
111+
112+
export { downloadBinary };

.github/npm/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"postinstall": "node scripts.js install"
1414
},
1515
"dependencies": {
16-
"simple-binary-install": "^0.2.1"
16+
"node-fetch": "^3.3.2",
17+
"tar": "^7.4.3"
1718
},
1819
"keywords": [
1920
"proto",

.github/npm/run.js

100644100755
Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
11
#!/usr/bin/env node
2-
import { getBinary } from './getBinary.js';
2+
import { spawn } from 'child_process';
3+
import path from 'path';
4+
import { fileURLToPath } from 'url';
35

4-
getBinary().run();
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
9+
const isWindows = process.platform === 'win32';
10+
const binaryName = isWindows ? 'protofetch.exe' : 'protofetch';
11+
const binaryPath = path.join(__dirname, 'bin', binaryName);
12+
13+
const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
14+
15+
child.on('error', (error) => {
16+
console.error(`Failed to start protofetch: ${error.message}`);
17+
console.error('The binary may be missing or corrupted. Try reinstalling the package:');
18+
console.error(' npm install --force');
19+
console.error(' or');
20+
console.error(' pnpm install --force');
21+
process.exit(1);
22+
});
23+
24+
child.on('close', (code) => {
25+
process.exit(code);
26+
});

.github/npm/scripts.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
1-
import { getBinary } from './getBinary.js';
1+
import { downloadBinary } from './getBinary.js';
22

33
if (process.argv.includes('install')) {
4-
getBinary().install();
4+
// Check for --url argument for testing (only localhost allowed for security)
5+
const urlArg = process.argv.find(arg => arg.startsWith('--url='));
6+
let url = null;
7+
8+
if (urlArg) {
9+
const providedUrl = urlArg.split('=')[1];
10+
try {
11+
const parsedUrl = new URL(providedUrl);
12+
// Only allow localhost URLs for testing
13+
if (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1') {
14+
url = providedUrl;
15+
} else {
16+
console.error('Error: --url parameter only allows localhost URLs for security reasons');
17+
process.exit(1);
18+
}
19+
} catch (error) {
20+
console.error('Error: Invalid URL provided to --url parameter');
21+
process.exit(1);
22+
}
23+
}
24+
25+
downloadBinary({ url })
26+
.then(() => {
27+
console.log('Installation complete');
28+
process.exit(0);
29+
})
30+
.catch((error) => {
31+
console.error('Installation failed:', error.message);
32+
process.exit(1);
33+
});
534
}

.github/workflows/ci.yml

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,110 @@ jobs:
157157
name: package-${{ matrix.target.rust }}
158158
path: protofetch_${{ matrix.target.rust }}.tar.gz
159159

160+
test-npm-package:
161+
needs: [ package ]
162+
strategy:
163+
fail-fast: false
164+
matrix:
165+
os:
166+
- runner: ubuntu-latest
167+
platform: x86_64-unknown-linux-musl
168+
- runner: macos-14
169+
platform: aarch64-apple-darwin
170+
- runner: macos-13
171+
platform: x86_64-apple-darwin
172+
- runner: windows-latest
173+
platform: x86_64-pc-windows-msvc
174+
runs-on: ${{ matrix.os.runner }}
175+
steps:
176+
- name: Checkout
177+
uses: actions/checkout@v3
178+
179+
- name: Setup Node.js
180+
uses: actions/setup-node@v4
181+
with:
182+
node-version: '20'
183+
184+
- name: Install pnpm
185+
uses: pnpm/action-setup@v4
186+
with:
187+
version: 8
188+
189+
- name: Download artifacts
190+
uses: actions/download-artifact@v4
191+
with:
192+
name: package-${{ matrix.os.platform }}
193+
path: artifacts
194+
195+
- name: Setup test environment
196+
shell: bash
197+
run: |
198+
cd .github/npm
199+
# Replace version placeholder with test version
200+
sed 's/VERSION#TO#REPLACE/0.0.0-test/g' package.json > package.test.json
201+
mv package.test.json package.json
202+
203+
- name: Start HTTP server
204+
shell: bash
205+
run: |
206+
cd artifacts
207+
# Start HTTP server in background
208+
python3 -m http.server 8000 &
209+
echo $! > /tmp/http_server.pid
210+
sleep 2
211+
echo "HTTP server started on port 8000"
212+
213+
- name: Test npm installation
214+
shell: bash
215+
run: |
216+
cd .github/npm
217+
218+
# Install dependencies
219+
npm install --ignore-scripts
220+
221+
# Run installation script with custom URL pointing to local HTTP server
222+
node scripts.js install --url=http://localhost:8000/protofetch_${{ matrix.os.platform }}.tar.gz
223+
224+
# Verify binary was extracted and works
225+
if [ "${{ runner.os }}" = "Windows" ]; then
226+
./bin/protofetch.exe --version
227+
else
228+
./bin/protofetch --version
229+
fi
230+
231+
# Clean up
232+
rm -rf node_modules package-lock.json bin/
233+
234+
- name: Test pnpm installation
235+
shell: bash
236+
run: |
237+
cd .github/npm
238+
239+
# Install dependencies
240+
pnpm install --ignore-scripts
241+
242+
# Run installation script with custom URL pointing to local HTTP server
243+
node scripts.js install --url=http://localhost:8000/protofetch_${{ matrix.os.platform }}.tar.gz
244+
245+
# Verify binary was extracted and works
246+
if [ "${{ runner.os }}" = "Windows" ]; then
247+
./bin/protofetch.exe --version
248+
else
249+
./bin/protofetch --version
250+
fi
251+
252+
- name: Stop HTTP server
253+
if: always()
254+
shell: bash
255+
run: |
256+
if [ -f /tmp/http_server.pid ]; then
257+
kill $(cat /tmp/http_server.pid) || true
258+
fi
259+
160260
release:
161261
runs-on: ubuntu-latest
162262
if: github.repository == 'coralogix/protofetch' && startsWith(github.ref, 'refs/tags/')
163-
needs: [ package ]
263+
needs: [ package, test-npm-package ]
164264
env:
165265
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
166266
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}

0 commit comments

Comments
 (0)