@swmansion/popcorn 0.4.0-next.0 → 0.4.0-next.3
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 +7 -7
- package/dist/index.mjs +37 -7
- 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.emu.mjs +139 -139
- package/dist/runtimes/core/beam.mjs +139 -139
- package/dist/runtimes/core/beam.wasm +0 -0
- package/dist/runtimes/crypto/beam.emu.mjs +518 -517
- package/dist/runtimes/crypto/beam.mjs +518 -517
- package/dist/runtimes/crypto/beam.wasm +0 -0
- package/dist/types.d.ts +7 -0
- package/dist/worker.mjs +93 -16
- 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
|
Binary file
|
package/dist/types.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export type BeamBootOptions = {
|
|
|
45
45
|
/** VM environment variables. */
|
|
46
46
|
env?: Record<string, string>;
|
|
47
47
|
ttySize: TtySize;
|
|
48
|
+
noshell: boolean;
|
|
48
49
|
createModule: CreateModuleFn<EmscriptenModule>;
|
|
49
50
|
emit: (event: BeamEvent) => void;
|
|
50
51
|
};
|
|
@@ -66,6 +67,9 @@ export type BeamEvent = {
|
|
|
66
67
|
} | {
|
|
67
68
|
type: "otp:run_js";
|
|
68
69
|
payload: RunJsRequest;
|
|
70
|
+
} | {
|
|
71
|
+
type: "otp:await-ready";
|
|
72
|
+
payload: ReadyRequest;
|
|
69
73
|
} | {
|
|
70
74
|
type: "otp:tracked-value-delete";
|
|
71
75
|
payload: number;
|
|
@@ -76,6 +80,9 @@ export type RunJsRequest = {
|
|
|
76
80
|
replyTo: Uint8Array;
|
|
77
81
|
return: "value" | "ref";
|
|
78
82
|
};
|
|
83
|
+
export type ReadyRequest = {
|
|
84
|
+
replyTo: Uint8Array;
|
|
85
|
+
};
|
|
79
86
|
/**
|
|
80
87
|
* VM shutdown notification.
|
|
81
88
|
*
|
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++) {
|
|
@@ -405,7 +466,7 @@ function start(options) {
|
|
|
405
466
|
};
|
|
406
467
|
}
|
|
407
468
|
async function boot(opts, state, vm) {
|
|
408
|
-
const { otpAssetsRoot, emulatorArgs, extraArgs, env, ttySize, createModule, emit, } = opts;
|
|
469
|
+
const { otpAssetsRoot, emulatorArgs, extraArgs, env, ttySize, noshell, createModule, emit, } = opts;
|
|
409
470
|
const loadedFsData = await loadFsData(otpAssetsRoot);
|
|
410
471
|
if (!loadedFsData.ok) {
|
|
411
472
|
return { ok: false, error: loadedFsData.error };
|
|
@@ -424,22 +485,34 @@ async function boot(opts, state, vm) {
|
|
|
424
485
|
LINES: String(ttySize.rows),
|
|
425
486
|
ERL_INETRC: INETRC_PATH,
|
|
426
487
|
};
|
|
488
|
+
let emitError = function (payload) {
|
|
489
|
+
emitError = null;
|
|
490
|
+
state.isVmReady = false;
|
|
491
|
+
emit({ type: "otp:error", payload });
|
|
492
|
+
};
|
|
427
493
|
const moduleConfig = {
|
|
428
494
|
print: (text) => emit({ type: "otp:stdout", payload: UTF8.encode(text) }),
|
|
429
495
|
printErr: (text) => emit({ type: "otp:stderr", payload: UTF8.encode(text) }),
|
|
430
|
-
onExit: (code) =>
|
|
431
|
-
onAbort: (text) =>
|
|
496
|
+
onExit: (code) => emitError?.({ kind: "exit", data: code }),
|
|
497
|
+
onAbort: (text) => emitError?.({ kind: "abort", data: text }),
|
|
432
498
|
onBeamMessage: (text) => {
|
|
499
|
+
if (emitError === null) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
433
502
|
const event = deserializeBridgeMessage(text);
|
|
434
503
|
if (event === null)
|
|
435
504
|
return;
|
|
505
|
+
if (event.type === "otp:error") {
|
|
506
|
+
emitError(event.payload);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
436
509
|
if (handleVmReady(event))
|
|
437
510
|
return;
|
|
438
511
|
if (handleAppReady(event))
|
|
439
512
|
return;
|
|
440
513
|
emit(event);
|
|
441
514
|
},
|
|
442
|
-
onError: (text) =>
|
|
515
|
+
onError: (text) => emitError?.({ kind: "error", data: text }),
|
|
443
516
|
onStdinConsumed: (size) => emit({ type: "otp:stdin-consumed", payload: size }),
|
|
444
517
|
onTrackedValueDelete: (key) => emit({ type: "otp:tracked-value-delete", payload: key }),
|
|
445
518
|
onTtyChunk: (fd, chunk) => emit({
|
|
@@ -449,6 +522,7 @@ async function boot(opts, state, vm) {
|
|
|
449
522
|
arguments: buildArgs({
|
|
450
523
|
appNames: fsData.appNames,
|
|
451
524
|
entrypoint: fsData.entrypoint,
|
|
525
|
+
noshell,
|
|
452
526
|
emulator: emulatorArgs ?? [],
|
|
453
527
|
extra: extraArgs ?? [],
|
|
454
528
|
}),
|
|
@@ -516,8 +590,10 @@ function toPopcornError(error) {
|
|
|
516
590
|
const message = error instanceof Error ? error.message : String(error);
|
|
517
591
|
return err("worker:load", { message });
|
|
518
592
|
}
|
|
519
|
-
function buildArgs({ appNames, entrypoint, emulator, extra, }) {
|
|
593
|
+
function buildArgs({ appNames, entrypoint, noshell, emulator, extra, }) {
|
|
520
594
|
const args = [...emulator, "--", ...BASE_ARGS, "-boot", BOOT_NAME];
|
|
595
|
+
if (noshell)
|
|
596
|
+
args.push("-noshell");
|
|
521
597
|
for (const app of CORE_APPS) {
|
|
522
598
|
args.push("-pa", `/lib/${app}/ebin`);
|
|
523
599
|
}
|
|
@@ -714,6 +790,7 @@ self.onmessage = async (event) => {
|
|
|
714
790
|
extraArgs: data.payload.extraArgs,
|
|
715
791
|
env: data.payload.env,
|
|
716
792
|
ttySize: data.payload.ttySize,
|
|
793
|
+
noshell: data.payload.noshell,
|
|
717
794
|
createModule,
|
|
718
795
|
emit: toMain,
|
|
719
796
|
});
|
|
@@ -743,7 +820,7 @@ self.onmessage = async (event) => {
|
|
|
743
820
|
});
|
|
744
821
|
break;
|
|
745
822
|
}
|
|
746
|
-
case "popcorn:
|
|
823
|
+
case "popcorn:bridge-reply": {
|
|
747
824
|
// ignore the `send()` result, process could've died
|
|
748
825
|
check(instance !== null);
|
|
749
826
|
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.3",
|
|
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
|