Skip to content

Commit 6036b53

Browse files
committed
fix: replace simple-binary-install with custom script for pnpm compat
Replaces simple-binary-install with a custom implementation using node-fetch and tar to ensure compatibility with pnpm's symlink-based directory structure. This resolves issues with incorrect extraction paths and Z_DATA_ERROR errors. Includes improvements such as Windows binary support, retry logic, security filters, and better error handling. Adds CI tests to verify npm and pnpm installations on all platforms and ensures releases only proceed if the tests pass. The implementation downloads binaries directly, extracts tarballs, and correctly handles both npm and pnpm directory structures.
1 parent bf348d0 commit 6036b53

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)