@supermachine/core 0.4.25
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/LICENSE +202 -0
- package/README.md +407 -0
- package/examples/01-hello.mjs +32 -0
- package/examples/02-pool.mjs +54 -0
- package/examples/03-snapshot-then-restore.mjs +54 -0
- package/examples/04-expose-tcp.mjs +80 -0
- package/examples/05-spawn-streaming.mjs +59 -0
- package/examples/README.md +43 -0
- package/index.d.ts +532 -0
- package/index.js +290 -0
- package/package.json +59 -0
package/index.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// @supermachine/core — entry point.
|
|
2
|
+
//
|
|
3
|
+
// 1. Locates the platform-specific .node addon (shipped in
|
|
4
|
+
// @supermachine/core-<platform>-<arch>, installed via npm's
|
|
5
|
+
// optionalDependencies + os/cpu filter).
|
|
6
|
+
// 2. Sets SUPERMACHINE_WORKER_BIN in process.env so the underlying
|
|
7
|
+
// Rust crate finds the pre-signed worker binary that ships
|
|
8
|
+
// alongside the .node file. Done BEFORE require()'ing the
|
|
9
|
+
// addon so the Rust module_init picks it up.
|
|
10
|
+
// 3. Re-exports the napi classes (Image, Pool, Vm).
|
|
11
|
+
|
|
12
|
+
const { existsSync } = require("node:fs");
|
|
13
|
+
const { join, dirname } = require("node:path");
|
|
14
|
+
|
|
15
|
+
// Map (process.platform, process.arch) → npm package name + binary
|
|
16
|
+
// filename. napi-rs's CLI auto-generates these names per platform;
|
|
17
|
+
// keep this table in sync with the matrix in the release workflow.
|
|
18
|
+
const TARGETS = {
|
|
19
|
+
"darwin-arm64": {
|
|
20
|
+
pkg: "@supermachine/core-darwin-arm64",
|
|
21
|
+
nodeFile: "supermachine-core.darwin-arm64.node",
|
|
22
|
+
workerFile: "supermachine-worker",
|
|
23
|
+
},
|
|
24
|
+
// Future targets — add when KVM/WHP backends land:
|
|
25
|
+
// "linux-arm64-gnu": { pkg: "@supermachine/core-linux-arm64-gnu",
|
|
26
|
+
// nodeFile: "supermachine-core.linux-arm64-gnu.node",
|
|
27
|
+
// workerFile: "supermachine-worker" },
|
|
28
|
+
// "linux-x64-gnu": { ... },
|
|
29
|
+
// "win32-x64-msvc": { ... },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function platformKey() {
|
|
33
|
+
// libc detection (musl vs glibc) goes here when we add linux
|
|
34
|
+
// targets; for now darwin-arm64 is the only supported combo.
|
|
35
|
+
return `${process.platform}-${process.arch}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolvePlatformPackage() {
|
|
39
|
+
const key = platformKey();
|
|
40
|
+
const target = TARGETS[key];
|
|
41
|
+
if (!target) {
|
|
42
|
+
const supported = Object.keys(TARGETS).join(", ");
|
|
43
|
+
throw new Error(
|
|
44
|
+
`@supermachine/core: no prebuilt binary for ${key}. ` +
|
|
45
|
+
`Supported: ${supported}. ` +
|
|
46
|
+
`Linux KVM and Windows WHP support are on the roadmap; ` +
|
|
47
|
+
`for now use macOS on Apple Silicon, or call the Rust crate ` +
|
|
48
|
+
`directly via FFI.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Two resolution strategies, in order:
|
|
53
|
+
// 1. Sibling node_modules — production install: the platform
|
|
54
|
+
// package was installed as an optionalDependency.
|
|
55
|
+
// 2. Local dev path — when working in this monorepo, the
|
|
56
|
+
// .node file sits in npm/<platform>/ next to this index.js.
|
|
57
|
+
let pkgDir;
|
|
58
|
+
try {
|
|
59
|
+
// require.resolve points at the package.json; dirname is the
|
|
60
|
+
// package root.
|
|
61
|
+
pkgDir = dirname(require.resolve(`${target.pkg}/package.json`));
|
|
62
|
+
} catch (_) {
|
|
63
|
+
pkgDir = join(__dirname, "npm", key);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nodePath = join(pkgDir, target.nodeFile);
|
|
67
|
+
if (!existsSync(nodePath)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`@supermachine/core: native addon not found at ${nodePath}. ` +
|
|
70
|
+
`If you installed via npm, this is a bug — please report it. ` +
|
|
71
|
+
`If you're running from source, build it first: ` +
|
|
72
|
+
`\`cd npm/supermachine-core && npm run build\`.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const workerPath = join(pkgDir, target.workerFile);
|
|
77
|
+
return { nodePath, workerPath };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { nodePath, workerPath } = resolvePlatformPackage();
|
|
81
|
+
|
|
82
|
+
// Point the Rust crate's worker-locator at the bundled binary.
|
|
83
|
+
// Must happen BEFORE require()'ing the addon — the addon's
|
|
84
|
+
// module_init reads SUPERMACHINE_WORKER_BIN on first call.
|
|
85
|
+
//
|
|
86
|
+
// Don't clobber an explicit user-set value (devs running against
|
|
87
|
+
// a custom-built worker need that to win).
|
|
88
|
+
if (!process.env.SUPERMACHINE_WORKER_BIN && existsSync(workerPath)) {
|
|
89
|
+
process.env.SUPERMACHINE_WORKER_BIN = workerPath;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const addon = require(nodePath);
|
|
93
|
+
|
|
94
|
+
// Wrap addon.Image.build to massage the JS-friendly API into the
|
|
95
|
+
// Rust signature:
|
|
96
|
+
// - JS user writes: `Image.build({ ref: "...", warmup: async (vm) => ... })`
|
|
97
|
+
// - Rust expects: `Image.build(options, warmup?)` (two args, warmup
|
|
98
|
+
// can't be inside the options object because
|
|
99
|
+
// JsFunction isn't Send)
|
|
100
|
+
// - Inside the warmup callback, JS user calls `vm.exec(...)`. The
|
|
101
|
+
// Rust side passes us a string (the exec-socket path); we wrap
|
|
102
|
+
// it into a Vm-like object that delegates to addon._execAtPath.
|
|
103
|
+
//
|
|
104
|
+
// This is invisible to JS users — they see the documented API:
|
|
105
|
+
//
|
|
106
|
+
// Image.build({
|
|
107
|
+
// ref: "rust:1-slim",
|
|
108
|
+
// warmup: async (vm) => { await vm.exec({ argv: [...] }) },
|
|
109
|
+
// })
|
|
110
|
+
class Image {
|
|
111
|
+
static async build(options = {}) {
|
|
112
|
+
const { warmup, ...rest } = options;
|
|
113
|
+
if (warmup === undefined) {
|
|
114
|
+
return wrapImageInstance(await addon.Image.build(rest, null));
|
|
115
|
+
}
|
|
116
|
+
// Wrap user's warmup with a Vm-like adapter. Rust passes the
|
|
117
|
+
// exec-socket path as the callback arg; we build a Vm object
|
|
118
|
+
// whose .exec delegates to _execAtPath. The Rust side awaits
|
|
119
|
+
// the Promise we return.
|
|
120
|
+
const wrappedWarmup = async (execPath) => {
|
|
121
|
+
const warmupVm = makeWarmupVm(execPath);
|
|
122
|
+
try {
|
|
123
|
+
await warmup(warmupVm);
|
|
124
|
+
} finally {
|
|
125
|
+
warmupVm._invalidate();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
return wrapImageInstance(await addon.Image.build(rest, wrappedWarmup));
|
|
129
|
+
}
|
|
130
|
+
static async fromSnapshot(path) {
|
|
131
|
+
return wrapImageInstance(await addon.Image.fromSnapshot(path));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Wrap a native Image instance with our prototype-extending shim so
|
|
136
|
+
// `image.pool(...)` returns Pool (instead of the raw addon Pool).
|
|
137
|
+
// Image getters (snapshotPath / memoryMib / vcpus) are read-once at
|
|
138
|
+
// wrap time — they're stable for the lifetime of the snapshot, and
|
|
139
|
+
// re-reading on every access would burn an unnecessary napi
|
|
140
|
+
// roundtrip per property read.
|
|
141
|
+
function wrapImageInstance(native) {
|
|
142
|
+
return {
|
|
143
|
+
async pool(options) {
|
|
144
|
+
return wrapPoolInstance(await native.pool(options ?? null));
|
|
145
|
+
},
|
|
146
|
+
get snapshotPath() {
|
|
147
|
+
return native.snapshotPath;
|
|
148
|
+
},
|
|
149
|
+
get memoryMib() {
|
|
150
|
+
return native.memoryMib;
|
|
151
|
+
},
|
|
152
|
+
get vcpus() {
|
|
153
|
+
return native.vcpus;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function wrapPoolInstance(native) {
|
|
159
|
+
return {
|
|
160
|
+
async acquire() {
|
|
161
|
+
return wrapVmInstance(await native.acquire());
|
|
162
|
+
},
|
|
163
|
+
async shutdown() {
|
|
164
|
+
return native.shutdown();
|
|
165
|
+
},
|
|
166
|
+
// Sync — fast (~1 µs, just a mutex lock under the hood).
|
|
167
|
+
stats() {
|
|
168
|
+
return native.stats();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function wrapVmInstance(native) {
|
|
174
|
+
const handle = {
|
|
175
|
+
async exec(options) {
|
|
176
|
+
return native.exec(options);
|
|
177
|
+
},
|
|
178
|
+
async spawn(options) {
|
|
179
|
+
return wrapExecChild(await native.spawn(options));
|
|
180
|
+
},
|
|
181
|
+
async writeFile(path, bytes) {
|
|
182
|
+
return native.writeFile(path, bytes);
|
|
183
|
+
},
|
|
184
|
+
async readFile(path) {
|
|
185
|
+
return native.readFile(path);
|
|
186
|
+
},
|
|
187
|
+
async snapshot(destDir) {
|
|
188
|
+
return wrapImageInstance(await native.snapshot(destDir));
|
|
189
|
+
},
|
|
190
|
+
async workloadSignal(signum) {
|
|
191
|
+
return native.workloadSignal(signum);
|
|
192
|
+
},
|
|
193
|
+
async exposeTcp(hostPort, guestPort) {
|
|
194
|
+
return wrapTcpForwarder(await native.exposeTcp(hostPort, guestPort));
|
|
195
|
+
},
|
|
196
|
+
// Path accessors are sync — they're just String getters on the
|
|
197
|
+
// native side. Throw if the Vm has been released (the native
|
|
198
|
+
// getter rejects with a descriptive Error).
|
|
199
|
+
get vsockPath() {
|
|
200
|
+
return native.vsockPath;
|
|
201
|
+
},
|
|
202
|
+
get execPath() {
|
|
203
|
+
return native.execPath;
|
|
204
|
+
},
|
|
205
|
+
async release() {
|
|
206
|
+
return native.release();
|
|
207
|
+
},
|
|
208
|
+
async dispose() {
|
|
209
|
+
return native.dispose();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
// Symbol.asyncDispose support (Node 20+ disposer pattern).
|
|
213
|
+
if (typeof Symbol.asyncDispose === "symbol") {
|
|
214
|
+
handle[Symbol.asyncDispose] = () => handle.release();
|
|
215
|
+
}
|
|
216
|
+
return handle;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function wrapExecChild(native) {
|
|
220
|
+
return {
|
|
221
|
+
async writeStdin(bytes) {
|
|
222
|
+
return native.writeStdin(bytes);
|
|
223
|
+
},
|
|
224
|
+
async closeStdin() {
|
|
225
|
+
return native.closeStdin();
|
|
226
|
+
},
|
|
227
|
+
async readStdout(maxBytes) {
|
|
228
|
+
return native.readStdout(maxBytes ?? null);
|
|
229
|
+
},
|
|
230
|
+
async readStderr(maxBytes) {
|
|
231
|
+
return native.readStderr(maxBytes ?? null);
|
|
232
|
+
},
|
|
233
|
+
async signal(signum) {
|
|
234
|
+
return native.signal(signum);
|
|
235
|
+
},
|
|
236
|
+
async resize(cols, rows) {
|
|
237
|
+
return native.resize(cols, rows);
|
|
238
|
+
},
|
|
239
|
+
async wait() {
|
|
240
|
+
return native.wait();
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Thin wrapper around the napi TcpForwarder class. We could return
|
|
246
|
+
// the raw instance, but the shim approach keeps the JS API
|
|
247
|
+
// consistent (everything else is a plain object) and lets us add
|
|
248
|
+
// JS-only behavior (e.g. a `Symbol.dispose` future) without
|
|
249
|
+
// reaching into the napi class.
|
|
250
|
+
function wrapTcpForwarder(native) {
|
|
251
|
+
return {
|
|
252
|
+
get localAddr() {
|
|
253
|
+
return native.localAddr;
|
|
254
|
+
},
|
|
255
|
+
async stop() {
|
|
256
|
+
return native.stop();
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Build a Vm-like object backed by an exec-socket path. Used inside
|
|
262
|
+
// the warmup callback — the Rust side passes us the path string,
|
|
263
|
+
// JS code uses this object as if it were a normal Vm.
|
|
264
|
+
function makeWarmupVm(execPath) {
|
|
265
|
+
let valid = true;
|
|
266
|
+
const vm = {
|
|
267
|
+
async exec(options) {
|
|
268
|
+
if (!valid) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
"warmup Vm is no longer valid — the warmup callback returned. " +
|
|
271
|
+
"Don't hold references to the Vm past the callback; do all " +
|
|
272
|
+
"your exec calls inside it.",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return addon.execAtPath(execPath, options);
|
|
276
|
+
},
|
|
277
|
+
_invalidate() {
|
|
278
|
+
valid = false;
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
// Disallow `using vm` on warmup vms — they're not regular pool
|
|
282
|
+
// entries and don't have a release-to-pool semantics.
|
|
283
|
+
return vm;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = {
|
|
287
|
+
Image,
|
|
288
|
+
Pool: addon.Pool,
|
|
289
|
+
Vm: addon.Vm,
|
|
290
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@supermachine/core",
|
|
3
|
+
"version": "0.4.25",
|
|
4
|
+
"description": "Run any OCI/Docker image as a hardware-isolated microVM. Node/Bun/Deno binding for the supermachine Rust crate.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"index.d.ts",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"examples/"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.17.0"
|
|
17
|
+
},
|
|
18
|
+
"os": [
|
|
19
|
+
"darwin"
|
|
20
|
+
],
|
|
21
|
+
"cpu": [
|
|
22
|
+
"arm64"
|
|
23
|
+
],
|
|
24
|
+
"optionalDependencies": {
|
|
25
|
+
"@supermachine/core-darwin-arm64": "0.4.25"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build:rust": "TYPE_DEF_TMP_PATH=$(pwd)/scripts/.napi_type_defs.tmp cargo build --release -p supermachine-napi && cargo build --release --bin supermachine-worker -p supermachine",
|
|
29
|
+
"build:dts": "node ./scripts/gen-dts.js ./scripts/.napi_type_defs.tmp ./index.d.ts",
|
|
30
|
+
"build": "rm -f ./scripts/.napi_type_defs.tmp && npm run build:rust && npm run build:dts && node ./scripts/postbuild.js",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@napi-rs/cli": "^3.6.2",
|
|
36
|
+
"typescript": "^6.0.3",
|
|
37
|
+
"vitest": "^2.1.0"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/supercorp/supermachine"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/supercorp/supermachine/tree/main/npm/supermachine-core#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/supercorp/supermachine/issues"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"microvm",
|
|
49
|
+
"hypervisor",
|
|
50
|
+
"oci",
|
|
51
|
+
"docker",
|
|
52
|
+
"snapshot",
|
|
53
|
+
"sandbox",
|
|
54
|
+
"vm",
|
|
55
|
+
"macos",
|
|
56
|
+
"hvf",
|
|
57
|
+
"apple-silicon"
|
|
58
|
+
]
|
|
59
|
+
}
|