@pulseengine/synth 0.39.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 ADDED
@@ -0,0 +1,59 @@
1
+ # @pulseengine/synth
2
+
3
+ `synth` — a WebAssembly-to-native compiler with mechanized correctness proofs
4
+ in Rocq. It compiles WebAssembly to bare-metal ELF binaries for ARM Cortex-M
5
+ (Thumb-2), Cortex-R5 (A32), RISC-V (RV32IMAC), and host-native AArch64.
6
+
7
+ This npm package distributes the `synth` CLI binary so it can be installed and
8
+ invoked from any Node.js environment. It complements the crates.io
9
+ distribution (`cargo install synth-cli`).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ # One-shot (no install)
15
+ npx @pulseengine/synth --version
16
+
17
+ # Global install
18
+ npm install -g @pulseengine/synth
19
+ synth --version
20
+ ```
21
+
22
+ ## How it works
23
+
24
+ There are no third-party dependencies. On install, the `postinstall` hook:
25
+
26
+ 1. Detects your platform and architecture.
27
+ 2. Downloads the matching `synth-v<version>-<triple>.tar.gz` from the GitHub
28
+ Release whose tag equals this package's version.
29
+ 3. **Verifies** the tarball's SHA-256 against the release's signed
30
+ `SHA256SUMS.txt` manifest — a mismatch aborts the install.
31
+ 4. Extracts the `synth` binary into the package's `bin/` directory.
32
+
33
+ The package version tracks the synth release version, so
34
+ `npm install @pulseengine/synth@0.38.0` fetches the `v0.38.0` binaries.
35
+
36
+ Set `SYNTH_NPM_SKIP_DOWNLOAD=1` to skip the download (e.g. air-gapped CI where
37
+ the binary is provided out of band).
38
+
39
+ ## Supported platforms
40
+
41
+ Mirrors the release build matrix (no Windows build):
42
+
43
+ - `darwin-arm64` — `aarch64-apple-darwin`
44
+ - `darwin-x64` — `x86_64-apple-darwin`
45
+ - `linux-arm64` — `aarch64-unknown-linux-gnu`
46
+ - `linux-x64` — `x86_64-unknown-linux-gnu`
47
+
48
+ Binaries are pre-built and published alongside each GitHub release at
49
+ <https://github.com/pulseengine/synth/releases>. On an unsupported platform,
50
+ install from source:
51
+
52
+ ```bash
53
+ cargo install --git https://github.com/pulseengine/synth synth-cli
54
+ ```
55
+
56
+ ## License
57
+
58
+ Apache-2.0. See the [repository](https://github.com/pulseengine/synth) for
59
+ source, documentation, and issues.
package/index.js ADDED
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Platform detection and release-asset naming for @pulseengine/synth.
4
+ //
5
+ // synth is distributed as a single npm package: install.js (postinstall)
6
+ // downloads the matching pre-built binary from the GitHub Release whose tag
7
+ // equals this package's version, verifies it against the release's signed
8
+ // SHA256SUMS manifest, and extracts it into ./bin. This module owns the
9
+ // platform -> Rust-target-triple mapping shared by install.js and run.js and
10
+ // is the single source of truth for asset names.
11
+ //
12
+ // Supported targets mirror .github/workflows/release.yml exactly — four
13
+ // tarballs, no Windows build:
14
+ // - aarch64-apple-darwin (macOS arm64)
15
+ // - x86_64-apple-darwin (macOS x64)
16
+ // - aarch64-unknown-linux-gnu (Linux arm64)
17
+ // - x86_64-unknown-linux-gnu (Linux x64)
18
+
19
+ const os = require("os");
20
+ const path = require("path");
21
+
22
+ const REPO = "pulseengine/synth";
23
+
24
+ /**
25
+ * Map (process.platform, process.arch) to the Rust target triple that
26
+ * release.yml builds and uploads. Throws with a clear message for any
27
+ * combination synth does not ship a binary for (notably Windows, which has
28
+ * no release build — do NOT synthesize a 404 URL for it).
29
+ *
30
+ * @param {string} [platform=os.platform()] node platform id
31
+ * @param {string} [arch=os.arch()] node arch id
32
+ * @returns {string} e.g. "aarch64-apple-darwin"
33
+ */
34
+ function getRustTarget(platform = os.platform(), arch = os.arch()) {
35
+ let osTriple;
36
+ switch (platform) {
37
+ case "darwin":
38
+ osTriple = "apple-darwin";
39
+ break;
40
+ case "linux":
41
+ osTriple = "unknown-linux-gnu";
42
+ break;
43
+ default:
44
+ throw new Error(
45
+ `Unsupported platform: ${platform}. synth ships binaries for ` +
46
+ "macOS (darwin) and Linux only. Build from source with " +
47
+ "`cargo install --git https://github.com/pulseengine/synth synth-cli`.",
48
+ );
49
+ }
50
+
51
+ let archTriple;
52
+ switch (arch) {
53
+ case "x64":
54
+ archTriple = "x86_64";
55
+ break;
56
+ case "arm64":
57
+ archTriple = "aarch64";
58
+ break;
59
+ default:
60
+ throw new Error(
61
+ `Unsupported architecture: ${arch}. synth ships x86_64 (x64) and ` +
62
+ "aarch64 (arm64) binaries only.",
63
+ );
64
+ }
65
+
66
+ return `${archTriple}-${osTriple}`;
67
+ }
68
+
69
+ /**
70
+ * The release-asset tarball name for a given version + platform. Matches the
71
+ * `synth-${VERSION}-${TARGET}.tar.gz` name produced by release.yml, where
72
+ * VERSION is the tag *with* its leading `v` (e.g. v0.38.0).
73
+ *
74
+ * @param {string} version bare semver, e.g. "0.38.0"
75
+ * @param {string} [platform]
76
+ * @param {string} [arch]
77
+ * @returns {string} e.g. "synth-v0.38.0-aarch64-apple-darwin.tar.gz"
78
+ */
79
+ function getTarballName(version, platform = os.platform(), arch = os.arch()) {
80
+ const target = getRustTarget(platform, arch);
81
+ return `synth-v${version}-${target}.tar.gz`;
82
+ }
83
+
84
+ /** The binary name inside the tarball / on disk. synth has no Windows build,
85
+ * so there is never a `.exe`. */
86
+ function getBinaryName() {
87
+ return "synth";
88
+ }
89
+
90
+ /**
91
+ * Absolute path to the extracted synth binary (populated by install.js).
92
+ */
93
+ function getBinaryPath() {
94
+ return path.join(__dirname, "bin", getBinaryName());
95
+ }
96
+
97
+ /**
98
+ * GitHub Release download URL for the platform tarball at this version.
99
+ */
100
+ function getTarballUrl(version, platform = os.platform(), arch = os.arch()) {
101
+ const name = getTarballName(version, platform, arch);
102
+ return `https://github.com/${REPO}/releases/download/v${version}/${name}`;
103
+ }
104
+
105
+ /**
106
+ * GitHub Release download URL for the signed SHA256SUMS manifest.
107
+ */
108
+ function getChecksumsUrl(version) {
109
+ return `https://github.com/${REPO}/releases/download/v${version}/SHA256SUMS.txt`;
110
+ }
111
+
112
+ /**
113
+ * Parse a SHA256SUMS.txt manifest (as produced by `sha256sum ./*`) into a
114
+ * { basename: hash } map. Each line is `<64-hex-hash> ./<name>` — note the
115
+ * two-space separator and the `./` path prefix; we key by basename so a
116
+ * caller can look up `synth-v0.38.0-<triple>.tar.gz` directly.
117
+ *
118
+ * @param {string} text raw manifest contents
119
+ * @returns {Map<string,string>}
120
+ */
121
+ function parseChecksums(text) {
122
+ const map = new Map();
123
+ for (const rawLine of text.split("\n")) {
124
+ const line = rawLine.trim();
125
+ if (!line) continue;
126
+ const m = line.match(/^([0-9a-fA-F]{64})\s+(.+)$/);
127
+ if (!m) continue;
128
+ const hash = m[1].toLowerCase();
129
+ const name = path.basename(m[2].trim());
130
+ map.set(name, hash);
131
+ }
132
+ return map;
133
+ }
134
+
135
+ /**
136
+ * Look up the expected sha256 for a tarball in a parsed manifest.
137
+ * @returns {string} lowercase hex hash
138
+ * @throws if the tarball is absent from the manifest
139
+ */
140
+ function expectedHashFor(tarballName, checksumsText) {
141
+ const map =
142
+ checksumsText instanceof Map ? checksumsText : parseChecksums(checksumsText);
143
+ const hash = map.get(tarballName);
144
+ if (!hash) {
145
+ throw new Error(
146
+ `SHA256SUMS.txt has no entry for ${tarballName} — refusing to install ` +
147
+ "an unverifiable binary.",
148
+ );
149
+ }
150
+ return hash;
151
+ }
152
+
153
+ module.exports = {
154
+ REPO,
155
+ getRustTarget,
156
+ getTarballName,
157
+ getBinaryName,
158
+ getBinaryPath,
159
+ getTarballUrl,
160
+ getChecksumsUrl,
161
+ parseChecksums,
162
+ expectedHashFor,
163
+ };
164
+
165
+ // When invoked directly, print platform info (useful for debugging installs).
166
+ if (require.main === module) {
167
+ try {
168
+ const version = require("./package.json").version;
169
+ console.log("synth npm — platform information:");
170
+ console.log(` Platform: ${os.platform()}`);
171
+ console.log(` Architecture: ${os.arch()}`);
172
+ console.log(` Rust target: ${getRustTarget()}`);
173
+ console.log(` Tarball: ${getTarballName(version)}`);
174
+ console.log(` Download URL: ${getTarballUrl(version)}`);
175
+ console.log(` Binary path: ${getBinaryPath()}`);
176
+ } catch (err) {
177
+ console.error("Error:", err.message);
178
+ process.exit(1);
179
+ }
180
+ }
package/install.js ADDED
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Post-install hook for @pulseengine/synth.
4
+ //
5
+ // Downloads the pre-built `synth` binary for the current platform from the
6
+ // GitHub Release whose tag matches this package's version, VERIFIES the
7
+ // tarball against the release's SHA256SUMS.txt manifest, then extracts the
8
+ // binary into ./bin so run.js can spawn it.
9
+ //
10
+ // The checksum step is mandatory: an install that cannot verify the archive
11
+ // against the published manifest aborts rather than run an unverified binary.
12
+ //
13
+ // Uses only Node built-ins (https, node:crypto, fs, node:child_process) plus
14
+ // the system `tar` — no third-party dependencies.
15
+
16
+ const os = require("os");
17
+ const path = require("path");
18
+ const fs = require("fs");
19
+ const https = require("https");
20
+ const crypto = require("crypto");
21
+ const { execFileSync } = require("child_process");
22
+
23
+ const {
24
+ getRustTarget,
25
+ getBinaryName,
26
+ getBinaryPath,
27
+ getTarballName,
28
+ getTarballUrl,
29
+ getChecksumsUrl,
30
+ expectedHashFor,
31
+ } = require("./index.js");
32
+
33
+ const VERSION = require("./package.json").version;
34
+
35
+ // Allow air-gapped / offline installs to skip the network fetch (e.g. when a
36
+ // binary was pre-placed or is provided by another mechanism).
37
+ if (process.env.SYNTH_NPM_SKIP_DOWNLOAD === "1") {
38
+ console.log(
39
+ "SYNTH_NPM_SKIP_DOWNLOAD=1 set — skipping synth binary download.",
40
+ );
41
+ process.exit(0);
42
+ }
43
+
44
+ function download(url, { asBuffer = false, dest = null } = {}, redirects = 0) {
45
+ return new Promise((resolve, reject) => {
46
+ if (redirects > 5) {
47
+ return reject(new Error(`Too many redirects fetching ${url}`));
48
+ }
49
+ https
50
+ .get(url, { headers: { "User-Agent": "pulseengine-synth-npm" } }, (res) => {
51
+ if (
52
+ res.statusCode >= 300 &&
53
+ res.statusCode < 400 &&
54
+ res.headers.location
55
+ ) {
56
+ res.resume();
57
+ return download(res.headers.location, { asBuffer, dest }, redirects + 1)
58
+ .then(resolve)
59
+ .catch(reject);
60
+ }
61
+ if (res.statusCode !== 200) {
62
+ res.resume();
63
+ return reject(
64
+ new Error(`HTTP ${res.statusCode} ${res.statusMessage} for ${url}`),
65
+ );
66
+ }
67
+ if (asBuffer) {
68
+ const chunks = [];
69
+ res.on("data", (c) => chunks.push(c));
70
+ res.on("end", () => resolve(Buffer.concat(chunks)));
71
+ res.on("error", reject);
72
+ } else {
73
+ const file = fs.createWriteStream(dest);
74
+ res.pipe(file);
75
+ file.on("finish", () => file.close(() => resolve(dest)));
76
+ file.on("error", (err) => {
77
+ fs.unlink(dest, () => {});
78
+ reject(err);
79
+ });
80
+ }
81
+ })
82
+ .on("error", reject);
83
+ });
84
+ }
85
+
86
+ function sha256(buffer) {
87
+ return crypto.createHash("sha256").update(buffer).digest("hex");
88
+ }
89
+
90
+ // Extract via execFile (no shell) — inputs are paths we constructed, but the
91
+ // argv form is the correct idiom regardless.
92
+ function extractTarball(archivePath, destDir) {
93
+ execFileSync("tar", ["-xzf", archivePath, "-C", destDir], {
94
+ stdio: "inherit",
95
+ });
96
+ }
97
+
98
+ async function main() {
99
+ const target = getRustTarget();
100
+ const tarballName = getTarballName(VERSION);
101
+ const tarballUrl = getTarballUrl(VERSION);
102
+ const checksumsUrl = getChecksumsUrl(VERSION);
103
+ const binaryName = getBinaryName();
104
+
105
+ console.log(`synth npm installer — v${VERSION}`);
106
+ console.log(` Platform: ${os.type()} ${os.arch()} (${target})`);
107
+ console.log(` Tarball: ${tarballName}`);
108
+
109
+ const binDir = path.join(__dirname, "bin");
110
+ fs.mkdirSync(binDir, { recursive: true });
111
+ const archivePath = path.join(binDir, tarballName);
112
+
113
+ // 1. Fetch the signed checksum manifest first, so we know what to expect.
114
+ console.log(" Fetching SHA256SUMS.txt ...");
115
+ const checksumsText = (await download(checksumsUrl, { asBuffer: true })).toString(
116
+ "utf8",
117
+ );
118
+ const expected = expectedHashFor(tarballName, checksumsText);
119
+
120
+ // 2. Download the tarball.
121
+ console.log(` Downloading ${tarballUrl} ...`);
122
+ await download(tarballUrl, { dest: archivePath });
123
+
124
+ // 3. Verify BEFORE extracting.
125
+ const actual = sha256(fs.readFileSync(archivePath));
126
+ if (actual !== expected) {
127
+ fs.unlinkSync(archivePath);
128
+ throw new Error(
129
+ `Checksum mismatch for ${tarballName}\n` +
130
+ ` expected: ${expected}\n` +
131
+ ` actual: ${actual}\n` +
132
+ "The download may be corrupted or tampered with. Aborting.",
133
+ );
134
+ }
135
+ console.log(` Checksum OK (sha256 ${actual}).`);
136
+
137
+ // 4. Extract and expose the binary.
138
+ console.log(" Extracting ...");
139
+ extractTarball(archivePath, binDir);
140
+ fs.unlinkSync(archivePath);
141
+
142
+ const binaryPath = getBinaryPath();
143
+ if (!fs.existsSync(binaryPath)) {
144
+ throw new Error(
145
+ `Extraction succeeded but ${binaryName} was not found at ${binaryPath}.`,
146
+ );
147
+ }
148
+ fs.chmodSync(binaryPath, 0o755);
149
+ console.log(` Installed synth -> ${binaryPath}`);
150
+ }
151
+
152
+ main().catch((err) => {
153
+ console.error("");
154
+ console.error("Failed to install the synth binary:", err.message);
155
+ console.error("");
156
+ console.error("Alternatives:");
157
+ console.error(
158
+ " 1. Build from source: cargo install --git " +
159
+ "https://github.com/pulseengine/synth synth-cli",
160
+ );
161
+ console.error(
162
+ " 2. Download a release manually: " +
163
+ "https://github.com/pulseengine/synth/releases",
164
+ );
165
+ // A failed binary install is a hard error: unlike rivet (which has a
166
+ // platform-package primary path and treats the download as a fallback),
167
+ // this package's ONLY delivery mechanism is the verified download. If it
168
+ // fails, `synth` would not exist — surface that loudly.
169
+ process.exit(1);
170
+ });
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@pulseengine/synth",
3
+ "version": "0.39.0",
4
+ "description": "synth — a WebAssembly-to-ARM/RISC-V/AArch64 compiler with mechanized correctness proofs. Produces bare-metal ELF binaries for embedded targets.",
5
+ "bin": {
6
+ "synth": "./run.js"
7
+ },
8
+ "scripts": {
9
+ "postinstall": "node install.js",
10
+ "preuninstall": "node uninstall.js",
11
+ "test": "node --test"
12
+ },
13
+ "keywords": [
14
+ "webassembly",
15
+ "wasm",
16
+ "compiler",
17
+ "arm",
18
+ "cortex-m",
19
+ "riscv",
20
+ "aarch64",
21
+ "embedded",
22
+ "bare-metal",
23
+ "verified",
24
+ "rocq",
25
+ "coq",
26
+ "rust",
27
+ "cli"
28
+ ],
29
+ "author": "PulseEngine <https://github.com/pulseengine>",
30
+ "license": "Apache-2.0",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/pulseengine/synth.git"
34
+ },
35
+ "homepage": "https://github.com/pulseengine/synth#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/pulseengine/synth/issues"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "os": [
43
+ "darwin",
44
+ "linux"
45
+ ],
46
+ "cpu": [
47
+ "x64",
48
+ "arm64"
49
+ ],
50
+ "files": [
51
+ "index.js",
52
+ "install.js",
53
+ "run.js",
54
+ "uninstall.js",
55
+ "README.md"
56
+ ],
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }
package/run.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Thin launcher that spawns the synth binary extracted by install.js.
4
+ // Forwards argv, stdio, and exit code; propagates signals to the child.
5
+
6
+ const { spawn } = require("child_process");
7
+ const fs = require("fs");
8
+ const { getBinaryPath } = require("./index.js");
9
+
10
+ const binaryPath = getBinaryPath();
11
+
12
+ if (!fs.existsSync(binaryPath)) {
13
+ console.error("synth binary not found at:", binaryPath);
14
+ console.error("");
15
+ console.error("This usually means the postinstall download did not run or");
16
+ console.error("failed. Try reinstalling:");
17
+ console.error(" npm install --force @pulseengine/synth");
18
+ console.error("");
19
+ console.error(
20
+ "Or build from source: cargo install --git " +
21
+ "https://github.com/pulseengine/synth synth-cli",
22
+ );
23
+ process.exit(1);
24
+ }
25
+
26
+ const child = spawn(binaryPath, process.argv.slice(2), {
27
+ stdio: "inherit",
28
+ env: process.env,
29
+ });
30
+
31
+ child.on("error", (err) => {
32
+ console.error("Failed to start synth:", err.message);
33
+ process.exit(1);
34
+ });
35
+
36
+ child.on("exit", (code, signal) => {
37
+ if (signal) {
38
+ process.kill(process.pid, signal);
39
+ } else {
40
+ process.exit(code == null ? 0 : code);
41
+ }
42
+ });
43
+
44
+ process.on("SIGINT", () => child.kill("SIGINT"));
45
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
package/uninstall.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Pre-uninstall hook: remove the binary downloaded into ./bin by install.js.
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const binDir = path.join(__dirname, "bin");
9
+
10
+ if (fs.existsSync(binDir)) {
11
+ try {
12
+ fs.rmSync(binDir, { recursive: true, force: true });
13
+ } catch (err) {
14
+ console.error("Failed to clean synth bin/:", err.message);
15
+ }
16
+ }