@swmansion/popcorn 0.3.3 → 0.4.0-next.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/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +78 -2
- package/dist/beam.d.ts +10 -0
- package/dist/errors.d.ts +96 -39
- package/dist/etf.d.ts +38 -0
- package/dist/events.d.ts +82 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.mjs +1287 -2
- package/dist/plugins/beam_tools/lib/popcorn/beam_tools/beam_patcher.ex +184 -0
- package/dist/plugins/beam_tools/lib/popcorn/beam_tools/cli.ex +74 -0
- package/dist/plugins/beam_tools/lib/popcorn/beam_tools/packager.ex +540 -0
- package/dist/plugins/beam_tools/mix.exs +16 -0
- package/dist/plugins/beam_tools/patches/kernel/prim_tty.erl +13 -0
- package/dist/plugins/beam_tools/patches/stdlib/beam_lib.erl +27 -0
- package/dist/plugins/esbuild.d.ts +10 -2
- package/dist/plugins/esbuild.mjs +39 -34
- package/dist/plugins/rollup.d.ts +10 -2
- package/dist/plugins/rollup.mjs +46 -25
- package/dist/plugins/shared.d.ts +54 -4
- package/dist/plugins/shared.mjs +207 -0
- package/dist/plugins/vite.d.ts +17 -2
- package/dist/plugins/vite.mjs +201 -75
- package/dist/popcorn.d.ts +237 -110
- package/dist/runtimes/core/beam.emu.mjs +141 -0
- package/dist/runtimes/core/beam.mjs +141 -0
- package/dist/runtimes/core/beam.wasm +0 -0
- package/dist/runtimes/core/manifest.json +1 -0
- package/dist/runtimes/crypto/beam.emu.mjs +520 -0
- package/dist/runtimes/crypto/beam.mjs +520 -0
- package/dist/runtimes/crypto/beam.wasm +0 -0
- package/dist/runtimes/crypto/manifest.json +1 -0
- package/dist/tar.d.ts +4 -0
- package/dist/types.d.ts +108 -63
- package/dist/utils.d.ts +7 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.mjs +765 -0
- package/package.json +20 -28
- package/dist/AtomVM.mjs +0 -7992
- package/dist/AtomVM.wasm +0 -0
- package/dist/bridge.d.ts +0 -22
- package/dist/bridge.mjs +0 -66
- package/dist/errors.mjs +0 -55
- package/dist/iframe.d.ts +0 -1
- package/dist/iframe.mjs +0 -215
- package/dist/popcorn.mjs +0 -381
- package/dist/types.mjs +0 -25
|
@@ -0,0 +1,184 @@
|
|
|
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
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|