@swmansion/popcorn 0.4.0-next.0 → 0.4.0-next.2
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 +18 -9
- package/dist/events.d.ts +6 -6
- package/dist/index.mjs +32 -6
- package/dist/plugins/esbuild.mjs +2 -8
- package/dist/plugins/rollup.mjs +3 -9
- package/dist/plugins/shared.d.ts +17 -37
- package/dist/plugins/shared.mjs +42 -188
- package/dist/plugins/vite.d.ts +6 -1
- package/dist/plugins/vite.mjs +18 -21
- package/dist/popcorn.d.ts +7 -3
- package/dist/runtimes/core/beam.wasm +0 -0
- package/dist/runtimes/crypto/beam.wasm +0 -0
- package/dist/types.d.ts +6 -0
- package/dist/worker.mjs +72 -11
- package/package.json +3 -3
- package/dist/plugins/beam_tools/lib/popcorn/beam_tools/beam_patcher.ex +0 -184
- package/dist/plugins/beam_tools/lib/popcorn/beam_tools/cli.ex +0 -74
- package/dist/plugins/beam_tools/lib/popcorn/beam_tools/packager.ex +0 -540
- package/dist/plugins/beam_tools/mix.exs +0 -16
- package/dist/plugins/beam_tools/patches/kernel/prim_tty.erl +0 -13
- package/dist/plugins/beam_tools/patches/stdlib/beam_lib.erl +0 -27
package/dist/plugins/vite.mjs
CHANGED
|
@@ -5,9 +5,9 @@ import { r as runtimeDirectory, p as popcorn$1 } from './shared.mjs';
|
|
|
5
5
|
import 'node:child_process';
|
|
6
6
|
import 'node:os';
|
|
7
7
|
import 'node:util';
|
|
8
|
-
import 'node:zlib';
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
/** Headers required by the threaded WebAssembly runtime. */
|
|
10
|
+
const crossOriginIsolationHeaders = {
|
|
11
11
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
12
12
|
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
13
13
|
};
|
|
@@ -22,7 +22,7 @@ const distDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
22
22
|
* import { popcorn } from "@swmansion/popcorn/vite";
|
|
23
23
|
*
|
|
24
24
|
* export default defineConfig({
|
|
25
|
-
* plugins: [popcorn({ rootDir: "../my_app"
|
|
25
|
+
* plugins: [popcorn({ rootDir: "../my_app" })],
|
|
26
26
|
* });
|
|
27
27
|
* ```
|
|
28
28
|
*
|
|
@@ -43,8 +43,8 @@ function popcorn(options) {
|
|
|
43
43
|
let dirty = false;
|
|
44
44
|
let outDir;
|
|
45
45
|
let assetsDir = "assets";
|
|
46
|
-
// Dev/preview: pack once at
|
|
47
|
-
// compiled beams change. A single in-flight promise dedupes concurrent
|
|
46
|
+
// Dev/preview: pack once at server startup, then re-pack lazily after the
|
|
47
|
+
// app's compiled beams change. A single in-flight promise dedupes concurrent
|
|
48
48
|
// requests; a change during a re-pack re-sets `dirty` so it isn't lost.
|
|
49
49
|
const ensurePrepared = () => {
|
|
50
50
|
if (prepared !== undefined && !dirty)
|
|
@@ -95,24 +95,21 @@ function popcorn(options) {
|
|
|
95
95
|
const filePath = resolve(assetsRoot, requestPath.slice(requestRoot.length));
|
|
96
96
|
if (!isUnder(assetsRoot, filePath)) {
|
|
97
97
|
res.statusCode = 403;
|
|
98
|
-
setHeaders(res);
|
|
99
98
|
res.end("Forbidden");
|
|
100
99
|
return;
|
|
101
100
|
}
|
|
102
101
|
try {
|
|
103
102
|
const compressible = isCompressible(filePath);
|
|
104
103
|
const encoding = compressible
|
|
105
|
-
? selectEncoding(req.headers["accept-encoding"]?.toString()
|
|
104
|
+
? selectEncoding(req.headers["accept-encoding"]?.toString())
|
|
106
105
|
: { name: null, suffix: "" };
|
|
107
106
|
if (compressible && encoding.name === null) {
|
|
108
107
|
res.statusCode = 406;
|
|
109
|
-
setHeaders(res);
|
|
110
108
|
res.setHeader("Vary", "Accept-Encoding");
|
|
111
109
|
res.end("No supported content encoding");
|
|
112
110
|
return;
|
|
113
111
|
}
|
|
114
112
|
const content = await readFile(`${filePath}${encoding.suffix}`);
|
|
115
|
-
setHeaders(res);
|
|
116
113
|
setContentType(res, filePath);
|
|
117
114
|
res.setHeader("Vary", "Accept-Encoding");
|
|
118
115
|
if (encoding.name !== null) {
|
|
@@ -122,7 +119,6 @@ function popcorn(options) {
|
|
|
122
119
|
}
|
|
123
120
|
catch {
|
|
124
121
|
res.statusCode = 404;
|
|
125
|
-
setHeaders(res);
|
|
126
122
|
res.end("Not found");
|
|
127
123
|
}
|
|
128
124
|
};
|
|
@@ -144,10 +140,6 @@ function popcorn(options) {
|
|
|
144
140
|
},
|
|
145
141
|
],
|
|
146
142
|
},
|
|
147
|
-
// COOP/COEP must be on every response (SharedArrayBuffer/pthreads),
|
|
148
|
-
// including the worker/beam files Vite serves from the package itself.
|
|
149
|
-
server: { headers: CORS_HEADERS },
|
|
150
|
-
preview: { headers: CORS_HEADERS },
|
|
151
143
|
};
|
|
152
144
|
},
|
|
153
145
|
configResolved(config) {
|
|
@@ -155,8 +147,9 @@ function popcorn(options) {
|
|
|
155
147
|
outDir = resolve(config.root, config.build.outDir);
|
|
156
148
|
assetsDir = config.build.assetsDir;
|
|
157
149
|
},
|
|
158
|
-
configureServer(server) {
|
|
150
|
+
async configureServer(server) {
|
|
159
151
|
const rootDir = resolve(options.rootDir);
|
|
152
|
+
server.httpServer?.prependListener("request", setIsolationHeaders);
|
|
160
153
|
server.watcher.add(rootDir);
|
|
161
154
|
server.watcher.on("all", (_event, file) => {
|
|
162
155
|
if (isUnder(rootDir, file))
|
|
@@ -164,10 +157,14 @@ function popcorn(options) {
|
|
|
164
157
|
});
|
|
165
158
|
server.middlewares.use(serve);
|
|
166
159
|
server.httpServer?.once("close", cleanup);
|
|
160
|
+
await ensurePrepared();
|
|
161
|
+
dirty = false;
|
|
167
162
|
},
|
|
168
|
-
configurePreviewServer(server) {
|
|
163
|
+
async configurePreviewServer(server) {
|
|
164
|
+
server.httpServer?.prependListener("request", setIsolationHeaders);
|
|
169
165
|
server.middlewares.use(serve);
|
|
170
166
|
server.httpServer?.once("close", cleanup);
|
|
167
|
+
await ensurePrepared();
|
|
171
168
|
},
|
|
172
169
|
async closeBundle() {
|
|
173
170
|
assert(outDir !== undefined, "outDir was not resolved");
|
|
@@ -187,8 +184,8 @@ function popcorn(options) {
|
|
|
187
184
|
function isCompressible(path) {
|
|
188
185
|
return path.endsWith(".tar") || path.endsWith(".wasm");
|
|
189
186
|
}
|
|
190
|
-
function selectEncoding(header
|
|
191
|
-
if (
|
|
187
|
+
function selectEncoding(header) {
|
|
188
|
+
if (header?.includes("br")) {
|
|
192
189
|
return { name: "br", suffix: ".br" };
|
|
193
190
|
}
|
|
194
191
|
if (header?.includes("gzip"))
|
|
@@ -199,8 +196,8 @@ function isUnder(dir, file) {
|
|
|
199
196
|
const rel = relative(dir, file);
|
|
200
197
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
201
198
|
}
|
|
202
|
-
function
|
|
203
|
-
for (const [key, value] of Object.entries(
|
|
199
|
+
function setIsolationHeaders(_req, res) {
|
|
200
|
+
for (const [key, value] of Object.entries(crossOriginIsolationHeaders)) {
|
|
204
201
|
res.setHeader(key, value);
|
|
205
202
|
}
|
|
206
203
|
}
|
|
@@ -224,4 +221,4 @@ function assert(ok, message) {
|
|
|
224
221
|
}
|
|
225
222
|
}
|
|
226
223
|
|
|
227
|
-
export { popcorn };
|
|
224
|
+
export { crossOriginIsolationHeaders, popcorn };
|
package/dist/popcorn.d.ts
CHANGED
|
@@ -123,6 +123,7 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
123
123
|
private readonly eventHandlers;
|
|
124
124
|
private readonly pendingSends;
|
|
125
125
|
private readonly pendingCalls;
|
|
126
|
+
private readonly readyWaiters;
|
|
126
127
|
private callSeq;
|
|
127
128
|
private readonly trackedValues;
|
|
128
129
|
private trackedKeySeq;
|
|
@@ -137,7 +138,7 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
137
138
|
*
|
|
138
139
|
* Call {@link boot} to start the VM.
|
|
139
140
|
**/
|
|
140
|
-
constructor(opts
|
|
141
|
+
constructor(opts?: PopcornOpts<Output>);
|
|
141
142
|
private spawnWorker;
|
|
142
143
|
/**
|
|
143
144
|
* Creates an instance and waits for {@link boot}.
|
|
@@ -146,7 +147,7 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
146
147
|
*
|
|
147
148
|
* @returns Ok tuple or `runtime:eval-unavailable` if the page blocks JavaScript evaluation.
|
|
148
149
|
*/
|
|
149
|
-
static init<Output extends TtyOutput = "text">(opts
|
|
150
|
+
static init<Output extends TtyOutput = "text">(opts?: PopcornOpts<Output>): Promise<Result<Popcorn<Output>>>;
|
|
150
151
|
/**
|
|
151
152
|
* Starts the VM and waits for its bridge and entrypoint application.
|
|
152
153
|
*
|
|
@@ -229,8 +230,11 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
229
230
|
private castBridge;
|
|
230
231
|
private nextCallId;
|
|
231
232
|
private runJs;
|
|
233
|
+
private handleReadyRequest;
|
|
234
|
+
private flushReadyWaiters;
|
|
235
|
+
private sendReadyReply;
|
|
232
236
|
private asRef;
|
|
233
|
-
private
|
|
237
|
+
private sendBridgeReply;
|
|
234
238
|
private jsWithCurrentEnv;
|
|
235
239
|
private reviveHandles;
|
|
236
240
|
/** Maps pids and `TrackedValue`s during encoding, collecting handles into
|
|
Binary file
|
|
Binary file
|
package/dist/types.d.ts
CHANGED
|
@@ -66,6 +66,9 @@ export type BeamEvent = {
|
|
|
66
66
|
} | {
|
|
67
67
|
type: "otp:run_js";
|
|
68
68
|
payload: RunJsRequest;
|
|
69
|
+
} | {
|
|
70
|
+
type: "otp:await-ready";
|
|
71
|
+
payload: ReadyRequest;
|
|
69
72
|
} | {
|
|
70
73
|
type: "otp:tracked-value-delete";
|
|
71
74
|
payload: number;
|
|
@@ -76,6 +79,9 @@ export type RunJsRequest = {
|
|
|
76
79
|
replyTo: Uint8Array;
|
|
77
80
|
return: "value" | "ref";
|
|
78
81
|
};
|
|
82
|
+
export type ReadyRequest = {
|
|
83
|
+
replyTo: Uint8Array;
|
|
84
|
+
};
|
|
79
85
|
/**
|
|
80
86
|
* VM shutdown notification.
|
|
81
87
|
*
|
package/dist/worker.mjs
CHANGED
|
@@ -240,7 +240,7 @@ function readMainEvent(value) {
|
|
|
240
240
|
case "popcorn:stdin":
|
|
241
241
|
case "popcorn:tty-resize":
|
|
242
242
|
case "popcorn:send":
|
|
243
|
-
case "popcorn:
|
|
243
|
+
case "popcorn:bridge-reply":
|
|
244
244
|
return data;
|
|
245
245
|
default:
|
|
246
246
|
unreachable();
|
|
@@ -269,6 +269,11 @@ function deserializeBridgeMessage(text) {
|
|
|
269
269
|
return: parsed.return,
|
|
270
270
|
},
|
|
271
271
|
};
|
|
272
|
+
case "await_ready":
|
|
273
|
+
return {
|
|
274
|
+
type: "otp:await-ready",
|
|
275
|
+
payload: { replyTo: base64ToBytes(parsed.reply_to) },
|
|
276
|
+
};
|
|
272
277
|
default:
|
|
273
278
|
return null;
|
|
274
279
|
}
|
|
@@ -289,7 +294,12 @@ function getTransferables(event) {
|
|
|
289
294
|
return [event.payload.buffer];
|
|
290
295
|
}
|
|
291
296
|
function isBridgeEnvelope(value) {
|
|
292
|
-
const KNOWN_MESSAGE_TYPES = [
|
|
297
|
+
const KNOWN_MESSAGE_TYPES = [
|
|
298
|
+
"vm_message",
|
|
299
|
+
"vm_error",
|
|
300
|
+
"run_js",
|
|
301
|
+
"await_ready",
|
|
302
|
+
];
|
|
293
303
|
const data = objectWithKeys(value, ["type"]);
|
|
294
304
|
return data !== null && KNOWN_MESSAGE_TYPES.includes(data.type);
|
|
295
305
|
}
|
|
@@ -314,31 +324,82 @@ const T = {
|
|
|
314
324
|
PREFIX_N: 155,
|
|
315
325
|
/// entry type=dir, '5' in ASCII
|
|
316
326
|
TYPE_DIR: 53,
|
|
327
|
+
/// entry type=PAX extended header, 'x' in ASCII
|
|
328
|
+
TYPE_PAX: 120,
|
|
317
329
|
};
|
|
318
330
|
function extractTar(data, onDir, onFile) {
|
|
319
331
|
check(data.length % T.BLK_N === 0, "tar:bad_chunk");
|
|
320
332
|
const decoder = new TextDecoder();
|
|
321
333
|
let offset = 0;
|
|
334
|
+
let paxPath = null;
|
|
322
335
|
while (offset + T.BLK_N <= data.length) {
|
|
323
336
|
const header = data.slice(offset, offset + T.BLK_N);
|
|
324
|
-
if (isZeroBlock(header))
|
|
337
|
+
if (isZeroBlock(header)) {
|
|
338
|
+
check(paxPath === null, "tar:dangling_pax");
|
|
325
339
|
break;
|
|
340
|
+
}
|
|
326
341
|
const name = readString(decoder, header, T.NAME_OFF, T.NAME_N);
|
|
327
342
|
const prefix = readString(decoder, header, T.PREFIX_OFF, T.PREFIX_N);
|
|
328
|
-
const
|
|
343
|
+
const headerName = prefix ? `${prefix}/${name}` : name;
|
|
329
344
|
const size = parseOctal(readString(decoder, header, T.SIZE_OFF, T.SIZE_N));
|
|
330
345
|
const type = header[T.TYPEFLAG_OFF];
|
|
331
346
|
offset += T.BLK_N;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
347
|
+
check(offset + size <= data.length, "tar:truncated_entry");
|
|
348
|
+
const contents = data.slice(offset, offset + size);
|
|
349
|
+
if (type === T.TYPE_PAX) {
|
|
350
|
+
check(paxPath === null, "tar:unexpected_pax");
|
|
351
|
+
paxPath = readPaxPath(decoder, contents);
|
|
335
352
|
}
|
|
336
|
-
else
|
|
337
|
-
const
|
|
338
|
-
|
|
353
|
+
else {
|
|
354
|
+
const fullName = paxPath ?? headerName;
|
|
355
|
+
paxPath = null;
|
|
356
|
+
const path = fullName.startsWith("/") ? fullName : `/${fullName}`;
|
|
357
|
+
if (type === T.TYPE_DIR) {
|
|
358
|
+
onDir(path);
|
|
359
|
+
}
|
|
360
|
+
else if (fullName) {
|
|
361
|
+
onFile(path, contents);
|
|
362
|
+
}
|
|
339
363
|
}
|
|
340
364
|
offset += Math.ceil(size / T.BLK_N) * T.BLK_N;
|
|
341
365
|
}
|
|
366
|
+
check(paxPath === null, "tar:dangling_pax");
|
|
367
|
+
}
|
|
368
|
+
function readPaxPath(decoder, data) {
|
|
369
|
+
let offset = 0;
|
|
370
|
+
let path = null;
|
|
371
|
+
while (offset < data.length) {
|
|
372
|
+
const { length, recordStart } = readPaxLength(decoder, data, offset);
|
|
373
|
+
const recordEnd = offset + length;
|
|
374
|
+
offset = recordEnd;
|
|
375
|
+
const [key, value] = readPaxRecord(decoder, data, recordStart, recordEnd);
|
|
376
|
+
if (key === "path") {
|
|
377
|
+
check(value.length > 0, "tar:bad_pax_path");
|
|
378
|
+
path = value;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
check(path !== null, "tar:missing_pax_path");
|
|
382
|
+
return path;
|
|
383
|
+
}
|
|
384
|
+
function readPaxLength(decoder, data, offset) {
|
|
385
|
+
const SPACE = 32;
|
|
386
|
+
const separator = data.indexOf(SPACE, offset);
|
|
387
|
+
check(separator > offset, "tar:bad_pax_length");
|
|
388
|
+
const source = decoder.decode(data.subarray(offset, separator));
|
|
389
|
+
const length = Number(source);
|
|
390
|
+
const isInt = Number.isSafeInteger(length) && length >= 0 && String(length) === source;
|
|
391
|
+
check(isInt, "tar:bad_pax_length");
|
|
392
|
+
return { length, recordStart: separator + 1 };
|
|
393
|
+
}
|
|
394
|
+
function readPaxRecord(decoder, data, start, end) {
|
|
395
|
+
const record = decoder.decode(data.subarray(start, end));
|
|
396
|
+
check(record.endsWith("\n"), "tar:bad_pax_record");
|
|
397
|
+
const separator = record.indexOf("=");
|
|
398
|
+
check(separator > 0, "tar:bad_pax_record");
|
|
399
|
+
const key = record.slice(0, separator);
|
|
400
|
+
// drop '\n'
|
|
401
|
+
const value = record.slice(separator + 1, -1);
|
|
402
|
+
return [key, value];
|
|
342
403
|
}
|
|
343
404
|
function isZeroBlock(block) {
|
|
344
405
|
for (let i = 0; i < block.length; i++) {
|
|
@@ -743,7 +804,7 @@ self.onmessage = async (event) => {
|
|
|
743
804
|
});
|
|
744
805
|
break;
|
|
745
806
|
}
|
|
746
|
-
case "popcorn:
|
|
807
|
+
case "popcorn:bridge-reply": {
|
|
747
808
|
// ignore the `send()` result, process could've died
|
|
748
809
|
check(instance !== null);
|
|
749
810
|
instance.send(data.payload.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swmansion/popcorn",
|
|
3
|
-
"version": "0.4.0-next.
|
|
3
|
+
"version": "0.4.0-next.2",
|
|
4
4
|
"description": "JS bindings for BEAM/OTP in WebAssembly",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -56,14 +56,14 @@
|
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@playwright/test": "
|
|
59
|
+
"@playwright/test": "1.63.0",
|
|
60
60
|
"@rollup/plugin-typescript": "^12.1.2",
|
|
61
61
|
"@types/node": "^25.0.8",
|
|
62
62
|
"esbuild": "^0.25.0",
|
|
63
63
|
"rollup": "^4.55.1",
|
|
64
64
|
"tslib": "^2.8.1",
|
|
65
65
|
"typescript": "^5.9.3",
|
|
66
|
-
"vite": "^7.3.
|
|
66
|
+
"vite": "^7.3.6"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "rollup -c",
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
defmodule Popcorn.BeamTools.BeamPatcher do
|
|
2
|
-
@moduledoc """
|
|
3
|
-
Replaces functions in a compiled BEAM with implementations taken from a patch
|
|
4
|
-
module, by merging both modules as Core Erlang and recompiling with the host
|
|
5
|
-
compiler.
|
|
6
|
-
|
|
7
|
-
A patch function replaces the original of the same name and arity, including
|
|
8
|
-
for calls made from the original module's own code. Exported patch functions
|
|
9
|
-
listed in `-compile([{popcorn_patch_private, [{f, a}]}])` stay private.
|
|
10
|
-
`popcorn_module:f(...)` calls the original implementation of `f`.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
@orig_prefix "popcorn_orig"
|
|
14
|
-
@patch_prefix "popcorn_patch"
|
|
15
|
-
|
|
16
|
-
@doc """
|
|
17
|
-
Compiles `patch_path` and merges it into the BEAM at `beam_path`, overwriting it.
|
|
18
|
-
"""
|
|
19
|
-
def patch_beam(beam_path, patch_path) do
|
|
20
|
-
with {:ok, original} <- read_ast(beam_path),
|
|
21
|
-
{:ok, patch} <- compile_patch(patch_path) do
|
|
22
|
-
beam =
|
|
23
|
-
original
|
|
24
|
-
|> merge_modules(patch)
|
|
25
|
-
|> serialize()
|
|
26
|
-
|
|
27
|
-
File.write!(beam_path, beam)
|
|
28
|
-
:ok
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
defp read_ast(beam_path) do
|
|
33
|
-
case :beam_lib.chunks(to_charlist(beam_path), [:abstract_code]) do
|
|
34
|
-
{:ok, {_module, [abstract_code: {_backend, abstract_code}]}} ->
|
|
35
|
-
{:ok, _module, ast} = :compile.noenv_forms(abstract_code, [:to_core])
|
|
36
|
-
{:ok, ast}
|
|
37
|
-
|
|
38
|
-
_ ->
|
|
39
|
-
err(:unpatchable_beam, beam_path)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
defp compile_patch(patch_path) do
|
|
44
|
-
case :compile.file(to_charlist(patch_path), [:binary, :debug_info, :return_errors, :to_core]) do
|
|
45
|
-
{:ok, _module, ast} ->
|
|
46
|
-
{:ok, ast}
|
|
47
|
-
|
|
48
|
-
{:error, errors, _warnings} ->
|
|
49
|
-
err(:bad_patch, {patch_path, errors})
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
defp serialize(ast) do
|
|
54
|
-
{:ok, _module, beam} = :compile.noenv_forms(ast, [:from_core, debug_info: {:core_v1, ast}])
|
|
55
|
-
beam
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
defp merge_modules(orig_ast, patch_ast) do
|
|
59
|
-
{:c_module, _meta, module_spec, orig_exports, orig_specs, orig_body} = orig_ast
|
|
60
|
-
{:c_module, _meta, _module_spec, patch_exports, patch_specs, patch_body} = patch_ast
|
|
61
|
-
private_overrides = private_overrides(patch_specs)
|
|
62
|
-
orig_exports = MapSet.new(orig_exports, fn {:c_var, _meta, fa} -> fa end)
|
|
63
|
-
|
|
64
|
-
patch_exports =
|
|
65
|
-
patch_exports
|
|
66
|
-
|> MapSet.new(fn {:c_var, _meta, fa} -> fa end)
|
|
67
|
-
|> MapSet.difference(private_overrides)
|
|
68
|
-
|> MapSet.difference(MapSet.new(module_info: 0, module_info: 1))
|
|
69
|
-
|
|
70
|
-
exports = MapSet.union(orig_exports, patch_exports)
|
|
71
|
-
replaced = MapSet.union(patch_exports, private_overrides)
|
|
72
|
-
|
|
73
|
-
# Everything the patch does not replace keeps its name: the module's own
|
|
74
|
-
# `-nifs` declarations and internal calls refer to private functions by name.
|
|
75
|
-
# Replaced originals are renamed rather than dropped so a patch can delegate
|
|
76
|
-
# to them; an original nothing delegates to is dead code and the compiler
|
|
77
|
-
# removes it. Patch-private helpers are renamed so they cannot collide with
|
|
78
|
-
# an original of the same name.
|
|
79
|
-
orig_body = rename_replaced_funs(orig_body, replaced)
|
|
80
|
-
|
|
81
|
-
patch_body =
|
|
82
|
-
patch_body
|
|
83
|
-
|> rename_funs_and_local_calls(%{prefix: @patch_prefix, except: replaced})
|
|
84
|
-
|> inject_original_calls(replaced)
|
|
85
|
-
|
|
86
|
-
exports_ast = exports |> Enum.sort() |> Enum.map(&{:c_var, [], &1})
|
|
87
|
-
|
|
88
|
-
{:c_module, [], module_spec, exports_ast, orig_specs ++ patch_specs, patch_body ++ orig_body}
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
defp private_overrides(patch_specs) do
|
|
92
|
-
patch_specs
|
|
93
|
-
|> Enum.flat_map(fn
|
|
94
|
-
{{:c_literal, _meta1, :compile}, {:c_literal, _meta2, params}} -> params
|
|
95
|
-
_other -> []
|
|
96
|
-
end)
|
|
97
|
-
|> Enum.flat_map(fn
|
|
98
|
-
{:popcorn_patch_private, funs} -> List.wrap(funs)
|
|
99
|
-
_other -> []
|
|
100
|
-
end)
|
|
101
|
-
|> MapSet.new()
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
defp rename_replaced_funs(ast, funs) do
|
|
105
|
-
Enum.map(ast, fn
|
|
106
|
-
{{:c_var, var_meta, {fun, arity}}, definition} ->
|
|
107
|
-
if {fun, arity} in funs do
|
|
108
|
-
{{:c_var, var_meta, {prefixed(@orig_prefix, fun), arity}}, definition}
|
|
109
|
-
else
|
|
110
|
-
{{:c_var, var_meta, {fun, arity}}, definition}
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
form ->
|
|
114
|
-
form
|
|
115
|
-
end)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
defp inject_original_calls(ast, replaced) do
|
|
119
|
-
with {:c_call, call_meta, mod_ast, fun_ast, args} <- ast,
|
|
120
|
-
{:c_literal, _mod_meta, :popcorn_module} <- mod_ast,
|
|
121
|
-
{:c_literal, fun_meta, fun} <- fun_ast do
|
|
122
|
-
arity = length(args)
|
|
123
|
-
original = if {fun, arity} in replaced, do: prefixed(@orig_prefix, fun), else: fun
|
|
124
|
-
|
|
125
|
-
{:c_apply, call_meta, {:c_var, fun_meta, {original, arity}}, args}
|
|
126
|
-
else
|
|
127
|
-
_other -> traverse(ast, &inject_original_calls(&1, replaced))
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
defp rename_funs_and_local_calls({:c_var, meta, {function, arity} = fa} = ast, ctx)
|
|
132
|
-
when is_atom(function) and is_integer(arity) do
|
|
133
|
-
if fa in ctx.except do
|
|
134
|
-
ast
|
|
135
|
-
else
|
|
136
|
-
{:c_var, meta, {prefixed(ctx.prefix, function), arity}}
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
defp rename_funs_and_local_calls({:function, {function, arity} = fa} = ast, ctx)
|
|
141
|
-
when is_atom(function) and is_integer(arity) do
|
|
142
|
-
if fa in ctx.except do
|
|
143
|
-
ast
|
|
144
|
-
else
|
|
145
|
-
{:function, {prefixed(ctx.prefix, function), arity}}
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
defp rename_funs_and_local_calls({:id, {line, col, id}}, ctx) do
|
|
150
|
-
id =
|
|
151
|
-
case Atom.to_string(id) do
|
|
152
|
-
"-" <> id -> id
|
|
153
|
-
id -> id
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
{:id, {line, col, prefixed("-" <> ctx.prefix, id)}}
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
defp rename_funs_and_local_calls(ast, ctx) do
|
|
160
|
-
traverse(ast, &rename_funs_and_local_calls(&1, ctx))
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
defp prefixed(prefix, name), do: String.to_atom("#{prefix}_#{name}")
|
|
164
|
-
|
|
165
|
-
defp traverse(ast, fun) when is_tuple(ast) do
|
|
166
|
-
ast |> Tuple.to_list() |> fun.() |> List.to_tuple()
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
defp traverse([h | t], fun) do
|
|
170
|
-
[fun.(h) | fun.(t)]
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
defp traverse(ast, _fun) do
|
|
174
|
-
ast
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
defp err(:unpatchable_beam, beam_path) do
|
|
178
|
-
{:error, %{code: "unpatchable_beam", beam: beam_path}}
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
defp err(:bad_patch, {patch_path, errors}) do
|
|
182
|
-
{:error, %{code: "bad_patch", patch: patch_path, errors: inspect(errors)}}
|
|
183
|
-
end
|
|
184
|
-
end
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
defmodule Popcorn.BeamTools.CLI do
|
|
2
|
-
alias Popcorn.BeamTools.Packager
|
|
3
|
-
|
|
4
|
-
@options [
|
|
5
|
-
root_dir: :string,
|
|
6
|
-
entrypoint_app: :string,
|
|
7
|
-
extra_app: [:string, :keep],
|
|
8
|
-
out_dir: :string,
|
|
9
|
-
runtimes_dir: :string,
|
|
10
|
-
runtime_variant: :string,
|
|
11
|
-
strip: :boolean
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
@required_options [:root_dir, :out_dir, :runtimes_dir]
|
|
15
|
-
|
|
16
|
-
def main(argv) do
|
|
17
|
-
if String.to_integer(System.otp_release()) < 27 do
|
|
18
|
-
IO.puts(:stderr, "Popcorn requires OTP 27 or later")
|
|
19
|
-
System.halt(1)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
argv
|
|
23
|
-
|> run()
|
|
24
|
-
|> report()
|
|
25
|
-
|> encode_json()
|
|
26
|
-
|> IO.puts()
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def run(argv) do
|
|
30
|
-
with {:ok, options} <- parse_argv(argv) do
|
|
31
|
-
Packager.run(options)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
defp parse_argv(argv) do
|
|
36
|
-
{opts, [], invalid} = OptionParser.parse(argv, strict: @options)
|
|
37
|
-
missing_opts = Enum.reject(@required_options, &Keyword.has_key?(opts, &1))
|
|
38
|
-
|
|
39
|
-
case {invalid, missing_opts} do
|
|
40
|
-
{[], []} ->
|
|
41
|
-
options =
|
|
42
|
-
opts
|
|
43
|
-
|> Keyword.delete(:extra_app)
|
|
44
|
-
|> Map.new()
|
|
45
|
-
|> Map.put(:extra_apps, Keyword.get_values(opts, :extra_app))
|
|
46
|
-
|> Map.put_new(:entrypoint_app, nil)
|
|
47
|
-
|> Map.put_new(:runtime_variant, nil)
|
|
48
|
-
|> Map.put_new(:strip, false)
|
|
49
|
-
|
|
50
|
-
{:ok, options}
|
|
51
|
-
|
|
52
|
-
_ ->
|
|
53
|
-
bad_args(invalid, missing_opts)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
defp bad_args(invalid, missing_opts) do
|
|
58
|
-
invalid =
|
|
59
|
-
Enum.map(invalid, fn {option, value} ->
|
|
60
|
-
%{option: option, value: value}
|
|
61
|
-
end)
|
|
62
|
-
|
|
63
|
-
missing = Enum.map(missing_opts, &to_string/1)
|
|
64
|
-
|
|
65
|
-
{:error, %{code: "bad_args", invalid: invalid, missing: missing}}
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
defp report({:ok, report}), do: report
|
|
69
|
-
defp report({:error, error}), do: %{ok: false, error: error}
|
|
70
|
-
|
|
71
|
-
defp encode_json(term) do
|
|
72
|
-
term |> :json.encode() |> IO.iodata_to_binary()
|
|
73
|
-
end
|
|
74
|
-
end
|