@swmansion/popcorn 0.3.2 → 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.
Files changed (47) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +12 -0
  3. package/README.md +78 -2
  4. package/dist/beam.d.ts +10 -0
  5. package/dist/errors.d.ts +96 -39
  6. package/dist/etf.d.ts +38 -0
  7. package/dist/events.d.ts +82 -0
  8. package/dist/index.d.ts +7 -3
  9. package/dist/index.mjs +1287 -2
  10. package/dist/plugins/beam_tools/lib/popcorn/beam_tools/beam_patcher.ex +184 -0
  11. package/dist/plugins/beam_tools/lib/popcorn/beam_tools/cli.ex +74 -0
  12. package/dist/plugins/beam_tools/lib/popcorn/beam_tools/packager.ex +540 -0
  13. package/dist/plugins/beam_tools/mix.exs +16 -0
  14. package/dist/plugins/beam_tools/patches/kernel/prim_tty.erl +13 -0
  15. package/dist/plugins/beam_tools/patches/stdlib/beam_lib.erl +27 -0
  16. package/dist/plugins/esbuild.d.ts +10 -2
  17. package/dist/plugins/esbuild.mjs +39 -34
  18. package/dist/plugins/rollup.d.ts +10 -2
  19. package/dist/plugins/rollup.mjs +46 -25
  20. package/dist/plugins/shared.d.ts +54 -4
  21. package/dist/plugins/shared.mjs +207 -0
  22. package/dist/plugins/vite.d.ts +17 -2
  23. package/dist/plugins/vite.mjs +201 -75
  24. package/dist/popcorn.d.ts +237 -110
  25. package/dist/runtimes/core/beam.emu.mjs +141 -0
  26. package/dist/runtimes/core/beam.mjs +141 -0
  27. package/dist/runtimes/core/beam.wasm +0 -0
  28. package/dist/runtimes/core/manifest.json +1 -0
  29. package/dist/runtimes/crypto/beam.emu.mjs +520 -0
  30. package/dist/runtimes/crypto/beam.mjs +520 -0
  31. package/dist/runtimes/crypto/beam.wasm +0 -0
  32. package/dist/runtimes/crypto/manifest.json +1 -0
  33. package/dist/tar.d.ts +4 -0
  34. package/dist/types.d.ts +108 -63
  35. package/dist/utils.d.ts +7 -0
  36. package/dist/worker.d.ts +1 -0
  37. package/dist/worker.mjs +765 -0
  38. package/package.json +20 -28
  39. package/dist/AtomVM.mjs +0 -7992
  40. package/dist/AtomVM.wasm +0 -0
  41. package/dist/bridge.d.ts +0 -22
  42. package/dist/bridge.mjs +0 -66
  43. package/dist/errors.mjs +0 -55
  44. package/dist/iframe.d.ts +0 -1
  45. package/dist/iframe.mjs +0 -215
  46. package/dist/popcorn.mjs +0 -381
  47. 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