@the-open-engine-company/zeroshot 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/bin/zeroshot.js +24 -0
- package/install.js +9 -0
- package/lib/install.js +136 -0
- package/lib/release-artifacts.js +103 -0
- package/package.json +30 -0
- package/targets.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# `@the-open-engine-company/zeroshot`
|
|
2
|
+
|
|
3
|
+
Thin installer for the canonical `zeroshot` executable. The package selects the release archive for the current Node platform and architecture, verifies it against that release's `SHA256SUMS`, and installs only the verified executable.
|
|
4
|
+
|
|
5
|
+
Installation fails closed with `UNSUPPORTED_ZEROSHOT_HOST` when the host has no declared release target. Source compilation and cross-target substitution are not supported.
|
|
6
|
+
|
|
7
|
+
Supported hosts are Linux x64/arm64, macOS x64/arm64, and Windows x64.
|
package/bin/zeroshot.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const { selectTarget } = require('../lib/install');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const { executable } = selectTarget();
|
|
11
|
+
const binary = path.join(__dirname, 'native', executable);
|
|
12
|
+
if (!fs.existsSync(binary)) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`NATIVE_BINARY_MISSING: ${binary}; reinstall @the-open-engine-company/zeroshot`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.signal) process.kill(process.pid, result.signal);
|
|
20
|
+
else process.exitCode = result.status === null ? 1 : result.status;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write(`zeroshot: ${error.message}\n`);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
package/install.js
ADDED
package/lib/install.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { URL } = require('url');
|
|
7
|
+
const {
|
|
8
|
+
archiveName,
|
|
9
|
+
extractExecutable,
|
|
10
|
+
parseChecksumManifest,
|
|
11
|
+
targetForHost,
|
|
12
|
+
verifyArchive,
|
|
13
|
+
} = require('./release-artifacts');
|
|
14
|
+
|
|
15
|
+
const RELEASE_BASE_URL = 'https://github.com/the-open-engine/zeroshot/releases/download';
|
|
16
|
+
const RELEASE_TAG_PREFIX = 'v';
|
|
17
|
+
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
18
|
+
const MAX_ARCHIVE_BYTES = 256 * 1024 * 1024;
|
|
19
|
+
const TARGETS = Object.freeze(
|
|
20
|
+
require('../targets.json').map((declaration) => Object.freeze({ ...declaration }))
|
|
21
|
+
);
|
|
22
|
+
const HOST_TARGETS = Object.freeze(
|
|
23
|
+
Object.fromEntries(TARGETS.map((target) => [`${target.platform}/${target.arch}`, target]))
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function selectTarget(platform = process.platform, arch = process.arch) {
|
|
27
|
+
const { target, executable } = targetForHost(TARGETS, platform, arch);
|
|
28
|
+
return { target, executable };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function download(url, maximumBytes, redirects = 0) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const request = https.get(
|
|
34
|
+
url,
|
|
35
|
+
{ headers: { 'user-agent': '@the-open-engine-company/zeroshot' } },
|
|
36
|
+
(response) => {
|
|
37
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
38
|
+
response.resume();
|
|
39
|
+
if (redirects >= 5)
|
|
40
|
+
return reject(new Error(`DOWNLOAD_FAILED: too many redirects for ${url}`));
|
|
41
|
+
return download(
|
|
42
|
+
new URL(response.headers.location, url).toString(),
|
|
43
|
+
maximumBytes,
|
|
44
|
+
redirects + 1
|
|
45
|
+
).then(resolve, reject);
|
|
46
|
+
}
|
|
47
|
+
if (response.statusCode !== 200) {
|
|
48
|
+
response.resume();
|
|
49
|
+
return reject(new Error(`DOWNLOAD_FAILED: ${url} returned HTTP ${response.statusCode}`));
|
|
50
|
+
}
|
|
51
|
+
const chunks = [];
|
|
52
|
+
let length = 0;
|
|
53
|
+
response.on('data', (chunk) => {
|
|
54
|
+
length += chunk.length;
|
|
55
|
+
if (length > maximumBytes)
|
|
56
|
+
request.destroy(new Error(`DOWNLOAD_FAILED: ${url} exceeds ${maximumBytes} bytes`));
|
|
57
|
+
else chunks.push(chunk);
|
|
58
|
+
});
|
|
59
|
+
response.on('end', () => resolve(Buffer.concat(chunks)));
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
request.on('error', reject);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isReleaseVersion(version) {
|
|
67
|
+
if (typeof version !== 'string') return false;
|
|
68
|
+
const prereleaseStart = version.indexOf('-');
|
|
69
|
+
const core = prereleaseStart === -1 ? version : version.slice(0, prereleaseStart);
|
|
70
|
+
const prerelease = prereleaseStart === -1 ? '' : version.slice(prereleaseStart + 1);
|
|
71
|
+
const coreParts = core.split('.');
|
|
72
|
+
if (coreParts.length !== 3 || coreParts.some((part) => !part || !/^\d+$/.test(part))) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return (
|
|
76
|
+
!prerelease ||
|
|
77
|
+
prerelease
|
|
78
|
+
.split('.')
|
|
79
|
+
.every(
|
|
80
|
+
(part) =>
|
|
81
|
+
part &&
|
|
82
|
+
[...part].every(
|
|
83
|
+
(character) =>
|
|
84
|
+
(character >= '0' && character <= '9') ||
|
|
85
|
+
(character >= 'A' && character <= 'Z') ||
|
|
86
|
+
(character >= 'a' && character <= 'z') ||
|
|
87
|
+
character === '-'
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function install(options = {}) {
|
|
94
|
+
const packageRoot = options.packageRoot || path.resolve(__dirname, '..');
|
|
95
|
+
const metadata =
|
|
96
|
+
options.packageMetadata ||
|
|
97
|
+
JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
98
|
+
if (!isReleaseVersion(metadata.version) || metadata.version === '0.0.0-development') {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`UNRELEASED_SHIM_VERSION: cannot install binary for package version ${metadata.version}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const selected = selectTarget(options.platform, options.arch);
|
|
104
|
+
const filename = archiveName(metadata.version, selected.target);
|
|
105
|
+
const baseUrl = `${RELEASE_BASE_URL}/${RELEASE_TAG_PREFIX}${metadata.version}`;
|
|
106
|
+
const fetchBuffer = options.fetchBuffer || download;
|
|
107
|
+
const manifest = await fetchBuffer(`${baseUrl}/SHA256SUMS`, MAX_MANIFEST_BYTES);
|
|
108
|
+
const archive = await fetchBuffer(`${baseUrl}/${filename}`, MAX_ARCHIVE_BYTES);
|
|
109
|
+
verifyArchive(filename, archive, manifest.toString('utf8'));
|
|
110
|
+
const executable = extractExecutable(archive, selected.executable);
|
|
111
|
+
|
|
112
|
+
const nativeDirectory = path.join(packageRoot, 'bin', 'native');
|
|
113
|
+
const destination = path.join(nativeDirectory, selected.executable);
|
|
114
|
+
const temporary = `${destination}.${process.pid}.tmp`;
|
|
115
|
+
fs.mkdirSync(nativeDirectory, { recursive: true });
|
|
116
|
+
try {
|
|
117
|
+
fs.writeFileSync(temporary, executable, { mode: 0o755, flag: 'wx' });
|
|
118
|
+
fs.renameSync(temporary, destination);
|
|
119
|
+
if (process.platform !== 'win32') fs.chmodSync(destination, 0o755);
|
|
120
|
+
} finally {
|
|
121
|
+
fs.rmSync(temporary, { force: true });
|
|
122
|
+
}
|
|
123
|
+
return destination;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
HOST_TARGETS,
|
|
128
|
+
RELEASE_BASE_URL,
|
|
129
|
+
RELEASE_TAG_PREFIX,
|
|
130
|
+
archiveName,
|
|
131
|
+
extractExecutable,
|
|
132
|
+
install,
|
|
133
|
+
parseChecksumManifest,
|
|
134
|
+
selectTarget,
|
|
135
|
+
verifyArchive,
|
|
136
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const zlib = require('zlib');
|
|
5
|
+
|
|
6
|
+
function archiveName(version, target) {
|
|
7
|
+
return `zeroshot-v${version}-${target}.tar.gz`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function targetForHost(declarations, platform, arch) {
|
|
11
|
+
const found = declarations.find(
|
|
12
|
+
(candidate) => candidate.platform === platform && candidate.arch === arch
|
|
13
|
+
);
|
|
14
|
+
if (!found) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`UNSUPPORTED_ZEROSHOT_HOST: no prebuilt binary for ${platform}/${arch}; supported hosts: ${declarations
|
|
17
|
+
.map((candidate) => `${candidate.platform}/${candidate.arch}`)
|
|
18
|
+
.join(', ')}`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseChecksumManifest(text) {
|
|
25
|
+
const checksums = new Map();
|
|
26
|
+
for (const line of text.split(/\r?\n/)) {
|
|
27
|
+
if (!line) continue;
|
|
28
|
+
const match = /^([0-9a-f]{64}) {2}([^/\\]+)$/.exec(line);
|
|
29
|
+
if (!match) throw new Error(`invalid SHA256SUMS line: ${line}`);
|
|
30
|
+
if (checksums.has(match[2])) throw new Error(`duplicate SHA256SUMS entry: ${match[2]}`);
|
|
31
|
+
checksums.set(match[2], match[1]);
|
|
32
|
+
}
|
|
33
|
+
return checksums;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sha256(contents) {
|
|
37
|
+
return crypto.createHash('sha256').update(contents).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function verifyChecksum(filename, contents, manifest) {
|
|
41
|
+
const checksums = manifest instanceof Map ? manifest : parseChecksumManifest(manifest);
|
|
42
|
+
const expected = checksums.get(filename);
|
|
43
|
+
if (!expected) throw new Error(`CHECKSUM_MISSING: SHA256SUMS has no entry for ${filename}`);
|
|
44
|
+
const actual = sha256(contents);
|
|
45
|
+
if (actual !== expected) {
|
|
46
|
+
throw new Error(`CHECKSUM_MISMATCH: ${filename} expected ${expected} but received ${actual}`);
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function verifyArchive(filename, archive, manifestText) {
|
|
52
|
+
return verifyChecksum(filename, archive, manifestText);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseTarSize(header) {
|
|
56
|
+
const value = header.toString('ascii').replace(/\0.*$/, '').trim();
|
|
57
|
+
if (!/^[0-7]+$/.test(value)) throw new Error('invalid tar entry size');
|
|
58
|
+
return Number.parseInt(value, 8);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractExecutable(archive, expectedName) {
|
|
62
|
+
let tar;
|
|
63
|
+
try {
|
|
64
|
+
tar = zlib.gunzipSync(archive);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(`ARCHIVE_INVALID: cannot decompress release archive: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
let offset = 0;
|
|
69
|
+
let executable = null;
|
|
70
|
+
while (offset + 512 <= tar.length) {
|
|
71
|
+
const header = tar.subarray(offset, offset + 512);
|
|
72
|
+
if (header.every((byte) => byte === 0)) {
|
|
73
|
+
if (!tar.subarray(offset).every((byte) => byte === 0)) {
|
|
74
|
+
throw new Error('ARCHIVE_INVALID: unexpected data after tar terminator');
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '');
|
|
79
|
+
const size = parseTarSize(header.subarray(124, 136));
|
|
80
|
+
const start = offset + 512;
|
|
81
|
+
const end = start + size;
|
|
82
|
+
if (end > tar.length) throw new Error('ARCHIVE_INVALID: truncated tar entry');
|
|
83
|
+
if (name === expectedName) {
|
|
84
|
+
if (executable) throw new Error(`ARCHIVE_INVALID: duplicate ${expectedName}`);
|
|
85
|
+
executable = Buffer.from(tar.subarray(start, end));
|
|
86
|
+
} else {
|
|
87
|
+
throw new Error(`ARCHIVE_INVALID: unexpected archive entry ${name}`);
|
|
88
|
+
}
|
|
89
|
+
offset = start + Math.ceil(size / 512) * 512;
|
|
90
|
+
}
|
|
91
|
+
if (!executable) throw new Error(`ARCHIVE_INVALID: archive does not contain ${expectedName}`);
|
|
92
|
+
return executable;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
archiveName,
|
|
97
|
+
extractExecutable,
|
|
98
|
+
parseChecksumManifest,
|
|
99
|
+
sha256,
|
|
100
|
+
targetForHost,
|
|
101
|
+
verifyArchive,
|
|
102
|
+
verifyChecksum,
|
|
103
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@the-open-engine-company/zeroshot",
|
|
3
|
+
"version": "8.0.0",
|
|
4
|
+
"description": "Verified prebuilt binary installer for the standalone Zeroshot engine",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/the-open-engine/zeroshot.git"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.0.0"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"zeroshot": "bin/zeroshot.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "node install.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/",
|
|
21
|
+
"lib/",
|
|
22
|
+
"install.js",
|
|
23
|
+
"targets.json",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/targets.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"platform": "linux",
|
|
4
|
+
"arch": "x64",
|
|
5
|
+
"target": "x86_64-unknown-linux-musl",
|
|
6
|
+
"executable": "zeroshot"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"platform": "linux",
|
|
10
|
+
"arch": "arm64",
|
|
11
|
+
"target": "aarch64-unknown-linux-musl",
|
|
12
|
+
"executable": "zeroshot"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"platform": "darwin",
|
|
16
|
+
"arch": "x64",
|
|
17
|
+
"target": "x86_64-apple-darwin",
|
|
18
|
+
"executable": "zeroshot"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"platform": "darwin",
|
|
22
|
+
"arch": "arm64",
|
|
23
|
+
"target": "aarch64-apple-darwin",
|
|
24
|
+
"executable": "zeroshot"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"platform": "win32",
|
|
28
|
+
"arch": "x64",
|
|
29
|
+
"target": "x86_64-pc-windows-msvc",
|
|
30
|
+
"executable": "zeroshot.exe"
|
|
31
|
+
}
|
|
32
|
+
]
|