@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.
- package/LICENSE +1 -1
- 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,540 @@
|
|
|
1
|
+
defmodule Popcorn.BeamTools.Packager do
|
|
2
|
+
alias Popcorn.BeamTools.BeamPatcher
|
|
3
|
+
|
|
4
|
+
@static_nif_beams MapSet.new(["wasm.beam", "prim_tty.beam", "zstd.beam"])
|
|
5
|
+
|
|
6
|
+
# :beam_lib.significant_chunks/0 is undocumented.
|
|
7
|
+
@retained_chunks :beam_lib.significant_chunks() -- [~c"Type"]
|
|
8
|
+
# Applications that only work when the emulator was built with the matching
|
|
9
|
+
# native support. The runtime manifest declares what the build provides.
|
|
10
|
+
@app_capabilities %{
|
|
11
|
+
"asn1" => "crypto",
|
|
12
|
+
"crypto" => "crypto",
|
|
13
|
+
"public_key" => "crypto",
|
|
14
|
+
"ssl" => "crypto"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@boot_name "bin/vm.boot"
|
|
18
|
+
|
|
19
|
+
# Using `__DIR__` is safe – the plugin is compiled on user's machine
|
|
20
|
+
@patches_dir Path.expand("../../../patches", __DIR__)
|
|
21
|
+
|
|
22
|
+
# To run, Beam needs following apps:
|
|
23
|
+
# - kernel
|
|
24
|
+
# - stdlib (kernel dep)
|
|
25
|
+
#
|
|
26
|
+
# We also add elixir support out of the box and need:
|
|
27
|
+
# - compiler (elixir dep)
|
|
28
|
+
# - elixir
|
|
29
|
+
#
|
|
30
|
+
# All of them should be in elixir's transitive dependency closure
|
|
31
|
+
@base_apps ["elixir"]
|
|
32
|
+
|
|
33
|
+
@type options :: %{
|
|
34
|
+
root_dir: Path.t(),
|
|
35
|
+
entrypoint_app: String.t() | nil,
|
|
36
|
+
extra_apps: [String.t()],
|
|
37
|
+
out_dir: Path.t(),
|
|
38
|
+
runtimes_dir: Path.t(),
|
|
39
|
+
runtime_variant: String.t() | nil,
|
|
40
|
+
strip: boolean()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
defp strip_tarball(path, out_dir) do
|
|
44
|
+
{:ok, entries} = :erl_tar.extract(to_charlist(path), [:memory])
|
|
45
|
+
|
|
46
|
+
stripped =
|
|
47
|
+
entries
|
|
48
|
+
|> Enum.sort_by(fn {name, _content} -> to_string(name) end)
|
|
49
|
+
|> Enum.map(&strip_beam/1)
|
|
50
|
+
|
|
51
|
+
output = Path.join(out_dir, Path.basename(path))
|
|
52
|
+
opts = [mtime: 0, atime: 0, ctime: 0, uid: 0, gid: 0]
|
|
53
|
+
|
|
54
|
+
:ok = :erl_tar.create(to_charlist(output), stripped, opts)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
defp beam?(path), do: Path.extname(path) == ".beam"
|
|
58
|
+
|
|
59
|
+
# Reimplementation of :beam_lib.strip_files/2.
|
|
60
|
+
# Original also gzip compresses modules.
|
|
61
|
+
defp strip_beam({name, content}) do
|
|
62
|
+
if beam?(to_string(name)) do
|
|
63
|
+
{:ok, _module, chunks} = :beam_lib.all_chunks(content)
|
|
64
|
+
chunks_by_name = Map.new(chunks)
|
|
65
|
+
|
|
66
|
+
chunks =
|
|
67
|
+
Enum.flat_map(@retained_chunks, fn name ->
|
|
68
|
+
case Map.fetch(chunks_by_name, name) do
|
|
69
|
+
{:ok, data} -> [{name, data}]
|
|
70
|
+
:error -> []
|
|
71
|
+
end
|
|
72
|
+
end)
|
|
73
|
+
|
|
74
|
+
{:ok, stripped} = :beam_lib.build_module(chunks)
|
|
75
|
+
|
|
76
|
+
{name, stripped}
|
|
77
|
+
else
|
|
78
|
+
{name, content}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@spec run(options()) :: {:ok, map()} | {:error, map()}
|
|
83
|
+
def run(args) do
|
|
84
|
+
%{
|
|
85
|
+
root_dir: root_dir,
|
|
86
|
+
entrypoint_app: entrypoint_app,
|
|
87
|
+
extra_apps: extra_apps,
|
|
88
|
+
out_dir: out_dir,
|
|
89
|
+
runtimes_dir: runtimes_dir,
|
|
90
|
+
runtime_variant: runtime_variant,
|
|
91
|
+
strip: strip
|
|
92
|
+
} = args
|
|
93
|
+
|
|
94
|
+
toolchain = fetch_toolchain_info()
|
|
95
|
+
|
|
96
|
+
with {:ok, project_apps} <- root_dir |> project_build_dir() |> get_apps_info(),
|
|
97
|
+
{:ok, builtin_apps} <- get_builtin_apps(toolchain),
|
|
98
|
+
{:ok, apps_info} <- apps_to_pack(project_apps, builtin_apps, extra_apps, entrypoint_app),
|
|
99
|
+
variant = runtime_variant || required_runtime(apps_info),
|
|
100
|
+
{:ok, manifest} <- read_manifest(Path.join([runtimes_dir, variant, "manifest.json"])),
|
|
101
|
+
:ok <- check_otp_version(toolchain.otp, manifest.version),
|
|
102
|
+
:ok <- check_capabilities(apps_info, manifest.capabilities),
|
|
103
|
+
{:ok, boot_path} <- create_boot(out_dir, toolchain.otp_root, manifest.preloaded),
|
|
104
|
+
staged_apps = stage_apps(Path.join(out_dir, "staging"), apps_info),
|
|
105
|
+
:ok <- patch_apps(staged_apps) do
|
|
106
|
+
vm_version = manifest.version
|
|
107
|
+
toolchain = Map.take(toolchain, ~w(otp elixir)a)
|
|
108
|
+
|
|
109
|
+
File.mkdir_p!(out_dir)
|
|
110
|
+
|
|
111
|
+
packed_apps =
|
|
112
|
+
staged_apps
|
|
113
|
+
|> async_stream(fn {app, info} ->
|
|
114
|
+
version = Keyword.get(info.props, :vsn, ~c"") |> to_string()
|
|
115
|
+
tar_path = create_tarball(out_dir, app, info.ebin_dir)
|
|
116
|
+
|
|
117
|
+
{app, %{tar: tar_path, version: version}}
|
|
118
|
+
end)
|
|
119
|
+
|> Map.new()
|
|
120
|
+
|
|
121
|
+
diagnostics =
|
|
122
|
+
staged_apps
|
|
123
|
+
|> async_stream(fn {app, info} ->
|
|
124
|
+
case loaded_dynamic_nifs(app, info.ebin_dir) do
|
|
125
|
+
[] ->
|
|
126
|
+
[]
|
|
127
|
+
|
|
128
|
+
beams ->
|
|
129
|
+
{:error, context} = err(:dynamic_nifs_loading, {app, beams})
|
|
130
|
+
[context]
|
|
131
|
+
end
|
|
132
|
+
end)
|
|
133
|
+
|> Enum.concat()
|
|
134
|
+
|
|
135
|
+
manifest_path = Path.join(out_dir, "manifest.json")
|
|
136
|
+
|
|
137
|
+
packed_tar_paths =
|
|
138
|
+
packed_apps
|
|
139
|
+
|> Map.values()
|
|
140
|
+
|> Enum.map(&Path.expand(Path.join(out_dir, &1.tar)))
|
|
141
|
+
|
|
142
|
+
tar_paths = maybe_strip_tarballs(packed_tar_paths, strip, out_dir)
|
|
143
|
+
|
|
144
|
+
manifest = %{
|
|
145
|
+
entrypoint: entrypoint_app,
|
|
146
|
+
apps: packed_apps,
|
|
147
|
+
notes: diagnostics,
|
|
148
|
+
toolchain: toolchain,
|
|
149
|
+
vm: %{boot: @boot_name, version: vm_version}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
File.write!(manifest_path, encode_json(manifest))
|
|
153
|
+
|
|
154
|
+
result = %{
|
|
155
|
+
ok: true,
|
|
156
|
+
runtimeVariant: variant,
|
|
157
|
+
entrypoint: entrypoint_app,
|
|
158
|
+
manifestPath: Path.expand(manifest_path),
|
|
159
|
+
bootPath: Path.expand(boot_path),
|
|
160
|
+
tarPaths: tar_paths,
|
|
161
|
+
apps: packed_apps,
|
|
162
|
+
notes: diagnostics,
|
|
163
|
+
toolchain: toolchain
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
{:ok, result}
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
defp create_boot(out_dir, otp_root, runtime_preloaded) do
|
|
171
|
+
boot_path = Path.join([Path.dirname(otp_root), "bin", "no_dot_erlang.boot"])
|
|
172
|
+
{:script, id, commands} = boot_path |> File.read!() |> :erlang.binary_to_term()
|
|
173
|
+
|
|
174
|
+
preloaded =
|
|
175
|
+
Enum.flat_map(commands, fn
|
|
176
|
+
{:preLoaded, modules} -> Enum.map(modules, &to_string/1)
|
|
177
|
+
_ -> []
|
|
178
|
+
end)
|
|
179
|
+
|
|
180
|
+
case preloaded -- runtime_preloaded do
|
|
181
|
+
[] ->
|
|
182
|
+
boot = {:script, id, Enum.map(commands, &drop_app_versions/1)}
|
|
183
|
+
path = Path.join(out_dir, @boot_name)
|
|
184
|
+
|
|
185
|
+
File.mkdir_p!(Path.dirname(path))
|
|
186
|
+
|
|
187
|
+
File.write!(path, :erlang.term_to_binary(boot))
|
|
188
|
+
|
|
189
|
+
{:ok, path}
|
|
190
|
+
|
|
191
|
+
missing ->
|
|
192
|
+
err(:unsupported_boot, {boot_path, missing})
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# `$ROOT/lib/kernel-10.5/ebin` -> `$ROOT/lib/kernel/ebin`
|
|
197
|
+
defp drop_app_versions({:path, dirs}) do
|
|
198
|
+
{:path, Enum.map(dirs, &drop_version_fragment/1)}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
defp drop_app_versions(command), do: command
|
|
202
|
+
|
|
203
|
+
defp drop_version_fragment(dir) do
|
|
204
|
+
[root, "lib", app_version, "ebin"] = dir |> to_string() |> Path.split()
|
|
205
|
+
[app, _version] = String.split(app_version, "-", parts: 2)
|
|
206
|
+
|
|
207
|
+
to_charlist(Path.join([root, "lib", app, "ebin"]))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Apps are copied out of the host installation so packing can modify them.
|
|
211
|
+
defp stage_apps(staging_dir, apps_info) do
|
|
212
|
+
apps_info
|
|
213
|
+
|> async_stream(fn {app, info} ->
|
|
214
|
+
ebin_dir = Path.join([staging_dir, app, "ebin"])
|
|
215
|
+
|
|
216
|
+
File.mkdir_p!(Path.dirname(ebin_dir))
|
|
217
|
+
File.cp_r!(info.ebin_dir, ebin_dir)
|
|
218
|
+
|
|
219
|
+
{app, %{info | ebin_dir: ebin_dir}}
|
|
220
|
+
end)
|
|
221
|
+
|> Map.new()
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# we patch only selected modules from OTP, see patches/
|
|
225
|
+
defp patch_apps(staged_apps) do
|
|
226
|
+
[@patches_dir, "*", "*.erl"]
|
|
227
|
+
|> Path.join()
|
|
228
|
+
|> Path.wildcard()
|
|
229
|
+
|> reduce_while_ok(:ok, fn patch_path, :ok ->
|
|
230
|
+
app = patch_path |> Path.dirname() |> Path.basename()
|
|
231
|
+
name = Path.basename(patch_path, ".erl") <> ".beam"
|
|
232
|
+
ebin_dir = Map.fetch!(staged_apps, app).ebin_dir
|
|
233
|
+
beam_path = Path.join(ebin_dir, name)
|
|
234
|
+
|
|
235
|
+
BeamPatcher.patch_beam(beam_path, patch_path)
|
|
236
|
+
end)
|
|
237
|
+
|> case do
|
|
238
|
+
{:ok, _} -> :ok
|
|
239
|
+
{:error, _} = error -> error
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
defp async_stream(enumerable, fun) do
|
|
244
|
+
enumerable
|
|
245
|
+
|> Task.async_stream(fun, timeout: :infinity)
|
|
246
|
+
|> Enum.map(fn {:ok, result} -> result end)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
defp maybe_strip_tarballs(paths, false, _out_dir), do: paths
|
|
250
|
+
|
|
251
|
+
defp maybe_strip_tarballs(paths, true, out_dir) do
|
|
252
|
+
Enum.map(paths, fn path ->
|
|
253
|
+
:ok = strip_tarball(path, out_dir)
|
|
254
|
+
Path.expand(Path.join(out_dir, Path.basename(path)))
|
|
255
|
+
end)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
defp project_build_dir(root_dir) do
|
|
259
|
+
build_env = System.get_env("MIX_ENV", "dev")
|
|
260
|
+
build_lib_dir = Path.join([root_dir, "_build", build_env, "lib"])
|
|
261
|
+
|
|
262
|
+
build_lib_dir
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
defp get_apps_info(root_dir) do
|
|
266
|
+
extract_info = fn app_path ->
|
|
267
|
+
{:ok, [{:application, name, props}]} = :file.consult(app_path)
|
|
268
|
+
dir = Path.dirname(app_path)
|
|
269
|
+
|
|
270
|
+
{to_string(name), %{props: props, ebin_dir: dir, app_path: app_path}}
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
get_name = fn {name, _info} -> name end
|
|
274
|
+
get_app_path = fn {_name, info} -> info.app_path end
|
|
275
|
+
duplicated? = fn {_name, paths} -> match?([_, _ | _], paths) end
|
|
276
|
+
|
|
277
|
+
apps =
|
|
278
|
+
root_dir
|
|
279
|
+
|> Path.join("*/ebin/*.app")
|
|
280
|
+
|> Path.wildcard()
|
|
281
|
+
|> Enum.map(extract_info)
|
|
282
|
+
|
|
283
|
+
duplicates =
|
|
284
|
+
apps
|
|
285
|
+
|> Enum.group_by(get_name, get_app_path)
|
|
286
|
+
|> Enum.filter(duplicated?)
|
|
287
|
+
|> Map.new()
|
|
288
|
+
|
|
289
|
+
if Enum.empty?(duplicates) do
|
|
290
|
+
{:ok, Map.new(apps)}
|
|
291
|
+
else
|
|
292
|
+
err(:duplicated_apps, {root_dir, duplicates})
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
defp get_builtin_apps(toolchain) do
|
|
297
|
+
with {:ok, elixir_apps} <- get_apps_info(toolchain.elixir_root),
|
|
298
|
+
{:ok, otp_apps} <- get_apps_info(toolchain.otp_root) do
|
|
299
|
+
{:ok, Map.merge(elixir_apps, otp_apps)}
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
defp read_manifest(manifest_path) do
|
|
304
|
+
with {:ok, json} <- File.read(manifest_path),
|
|
305
|
+
{:ok, %{"vm" => vm}} <- decode_json(json),
|
|
306
|
+
%{"version" => version, "preloaded" => preloaded, "capabilities" => capabilities} <- vm do
|
|
307
|
+
{:ok, %{version: version, preloaded: preloaded, capabilities: capabilities}}
|
|
308
|
+
else
|
|
309
|
+
_ -> err(:bad_manifest, manifest_path)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
defp apps_to_pack(project_apps, builtin_apps, extra_apps, entrypoint) do
|
|
314
|
+
all_apps_info = Map.merge(builtin_apps, project_apps)
|
|
315
|
+
|
|
316
|
+
gather_from_root = fn app, selected ->
|
|
317
|
+
gather_required_apps(all_apps_info, project_apps, app, selected)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
with {:ok, roots} <- root_apps(all_apps_info, extra_apps, entrypoint),
|
|
321
|
+
{:ok, selected_apps} <- reduce_while_ok(roots, MapSet.new(), gather_from_root) do
|
|
322
|
+
all_apps_info
|
|
323
|
+
|> Map.filter(fn {app, _info} -> MapSet.member?(selected_apps, app) end)
|
|
324
|
+
|> Enum.sort()
|
|
325
|
+
|> then(&{:ok, &1})
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
defp required_runtime(apps_info) do
|
|
330
|
+
needs_crypto = Enum.any?(apps_info, fn {app, _info} -> @app_capabilities[app] == "crypto" end)
|
|
331
|
+
if needs_crypto, do: "crypto", else: "core"
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
defp check_capabilities(apps_info, capabilities) do
|
|
335
|
+
unsupported =
|
|
336
|
+
apps_info
|
|
337
|
+
|> Enum.flat_map(fn {app, _info} ->
|
|
338
|
+
case Map.fetch(@app_capabilities, app) do
|
|
339
|
+
{:ok, capability} -> [%{app: app, capability: capability}]
|
|
340
|
+
:error -> []
|
|
341
|
+
end
|
|
342
|
+
end)
|
|
343
|
+
|> Enum.reject(&Map.fetch!(capabilities, &1.capability))
|
|
344
|
+
|
|
345
|
+
if unsupported == [], do: :ok, else: err(:unsupported_apps, unsupported)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
defp root_apps(all_apps_info, extra_apps, entrypoint) do
|
|
349
|
+
with {:ok, roots} <- entrypoint_roots(all_apps_info, entrypoint),
|
|
350
|
+
{:ok, extra} <- extra_roots(all_apps_info, extra_apps) do
|
|
351
|
+
{:ok, extra ++ roots}
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
defp entrypoint_roots(_all_apps_info, nil), do: {:ok, @base_apps}
|
|
356
|
+
|
|
357
|
+
# The entrypoint may be a builtin app, so a project with no code of its own
|
|
358
|
+
# doesn't need a stub application just to depend on one.
|
|
359
|
+
defp entrypoint_roots(all_apps_info, entrypoint)
|
|
360
|
+
when is_map_key(all_apps_info, entrypoint) do
|
|
361
|
+
{:ok, [entrypoint | @base_apps]}
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
defp entrypoint_roots(_all_apps_info, entrypoint),
|
|
365
|
+
do: err(:missing_entrypoint, entrypoint)
|
|
366
|
+
|
|
367
|
+
defp extra_roots(all_apps_info, extra_apps) do
|
|
368
|
+
case Enum.reject(extra_apps, &is_map_key(all_apps_info, &1)) do
|
|
369
|
+
[] -> {:ok, extra_apps}
|
|
370
|
+
missing -> err(:missing_extra_apps, Enum.sort(missing))
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
defp gather_required_apps(all_apps_info, project_apps, app, selected) do
|
|
375
|
+
if MapSet.member?(selected, app) do
|
|
376
|
+
{:ok, selected}
|
|
377
|
+
else
|
|
378
|
+
info = Map.fetch!(all_apps_info, app)
|
|
379
|
+
selected = MapSet.put(selected, app)
|
|
380
|
+
|
|
381
|
+
info.props
|
|
382
|
+
|> get_required_apps()
|
|
383
|
+
|> reduce_while_ok(selected, fn dep, acc ->
|
|
384
|
+
if Map.has_key?(all_apps_info, dep) do
|
|
385
|
+
gather_required_apps(all_apps_info, project_apps, dep, acc)
|
|
386
|
+
else
|
|
387
|
+
project_app_names = Enum.sort(Map.keys(project_apps))
|
|
388
|
+
err(:missing_dep, {app, dep, project_app_names})
|
|
389
|
+
end
|
|
390
|
+
end)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
defp reduce_while_ok(enumerable, acc, f) do
|
|
395
|
+
Enum.reduce_while(enumerable, {:ok, acc}, fn value, {:ok, acc} ->
|
|
396
|
+
case f.(value, acc) do
|
|
397
|
+
:ok -> {:cont, {:ok, acc}}
|
|
398
|
+
{:ok, new_acc} -> {:cont, {:ok, new_acc}}
|
|
399
|
+
{:error, _} = error -> {:halt, error}
|
|
400
|
+
end
|
|
401
|
+
end)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
defp create_tarball(outdir, app, ebin_dir) do
|
|
405
|
+
tar = "lib/#{app}.tar"
|
|
406
|
+
tar_path = Path.join(outdir, tar)
|
|
407
|
+
tar_path_c = to_charlist(tar_path)
|
|
408
|
+
arc_name = ~c"lib/#{app}/ebin"
|
|
409
|
+
ebin_dir_c = to_charlist(ebin_dir)
|
|
410
|
+
|
|
411
|
+
File.mkdir_p!(Path.dirname(tar_path))
|
|
412
|
+
:ok = :erl_tar.create(tar_path_c, [{arc_name, ebin_dir_c}], [])
|
|
413
|
+
|
|
414
|
+
tar
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
defp loaded_dynamic_nifs(app, ebin_dir) do
|
|
418
|
+
Path.join(ebin_dir, "*.beam")
|
|
419
|
+
|> Path.wildcard()
|
|
420
|
+
|> Enum.filter(&imports_load_nif?/1)
|
|
421
|
+
|> Enum.map(&Path.basename/1)
|
|
422
|
+
|> Enum.reject(&MapSet.member?(@static_nif_beams, &1))
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
defp imports_load_nif?(beam_path) do
|
|
426
|
+
case :beam_lib.chunks(to_charlist(beam_path), [:imports]) do
|
|
427
|
+
{:ok, {_mod, [imports: imports]}} -> {:erlang, :load_nif, 2} in imports
|
|
428
|
+
_ -> false
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
defp fetch_toolchain_info() do
|
|
433
|
+
%{
|
|
434
|
+
otp: host_otp_version(),
|
|
435
|
+
elixir: System.version(),
|
|
436
|
+
otp_root: Path.join(to_string(:code.root_dir()), "lib"),
|
|
437
|
+
elixir_root: :elixir |> :code.lib_dir() |> to_string() |> Path.dirname()
|
|
438
|
+
}
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
defp check_otp_version(host_version, runtime_version) do
|
|
442
|
+
# host: computer this runs on
|
|
443
|
+
# runtime: vm compiled to wasm
|
|
444
|
+
host = otp_version(host_version)
|
|
445
|
+
runtime = otp_version(runtime_version)
|
|
446
|
+
|
|
447
|
+
[host_major | _] = host
|
|
448
|
+
[runtime_major | _] = runtime
|
|
449
|
+
|
|
450
|
+
compatible = runtime_major - 2 <= host_major and version_lte?(host, runtime)
|
|
451
|
+
|
|
452
|
+
if compatible do
|
|
453
|
+
:ok
|
|
454
|
+
else
|
|
455
|
+
err(:unsupported_otp, {host_version, runtime_version})
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
defp host_otp_version do
|
|
460
|
+
path =
|
|
461
|
+
Path.join([to_string(:code.root_dir()), "releases", System.otp_release(), "OTP_VERSION"])
|
|
462
|
+
|
|
463
|
+
path
|
|
464
|
+
|> File.read!()
|
|
465
|
+
|> String.trim()
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
defp otp_version(version) do
|
|
469
|
+
version
|
|
470
|
+
|> to_string()
|
|
471
|
+
|> String.split("-", parts: 2)
|
|
472
|
+
|> hd()
|
|
473
|
+
|> String.split(".")
|
|
474
|
+
|> Enum.map(&String.to_integer/1)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
defp version_lte?(left, right) do
|
|
478
|
+
width = max(length(left), length(right))
|
|
479
|
+
pad = fn version -> version ++ List.duplicate(0, width - length(version)) end
|
|
480
|
+
pad.(left) <= pad.(right)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
defp get_required_apps(props) do
|
|
484
|
+
prop = &Keyword.get/3
|
|
485
|
+
optional = prop.(props, :optional_applications, []) |> MapSet.new()
|
|
486
|
+
applications = prop.(props, :applications, []) |> MapSet.new()
|
|
487
|
+
included = prop.(props, :included_applications, []) |> MapSet.new()
|
|
488
|
+
|
|
489
|
+
MapSet.union(applications, included)
|
|
490
|
+
|> MapSet.difference(optional)
|
|
491
|
+
|> Enum.map(&to_string/1)
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
defp err(:missing_entrypoint, app) do
|
|
495
|
+
{:error, %{code: "missing_entrypoint", app: app}}
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
defp err(:missing_extra_apps, apps) do
|
|
499
|
+
{:error, %{code: "missing_extra_apps", apps: apps}}
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
defp err(:bad_manifest, path) do
|
|
503
|
+
{:error, %{code: "bad_manifest", path: path}}
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
defp err(:unsupported_boot, {boot_path, missing_preloaded}) do
|
|
507
|
+
missing = Enum.sort(missing_preloaded)
|
|
508
|
+
{:error, %{code: "unsupported_boot", boot: boot_path, missing_preloaded: missing}}
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
defp err(:unsupported_otp, {host, runtime}) do
|
|
512
|
+
{:error, %{code: "unsupported_otp", host: host, runtime: runtime}}
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
defp err(:duplicated_apps, {root_dir, duplicates}) do
|
|
516
|
+
{:error, %{code: "duplicated_apps", root_dir: root_dir, duplicates: duplicates}}
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
defp err(:unsupported_apps, unsupported) do
|
|
520
|
+
{:error, %{code: "unsupported_apps", apps: Enum.sort_by(unsupported, & &1.app)}}
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
defp err(:missing_dep, {app, dep, project_apps}) do
|
|
524
|
+
{:error, %{code: "missing_dep", app: app, dep: dep, available_apps: project_apps}}
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
defp err(:dynamic_nifs_loading, {app, beams}) do
|
|
528
|
+
{:error, %{code: "dynamic_nifs_loading", app: app, beams: beams}}
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
defp encode_json(term) do
|
|
532
|
+
term |> :json.encode() |> IO.iodata_to_binary()
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
defp decode_json(json) do
|
|
536
|
+
{:ok, :json.decode(json)}
|
|
537
|
+
rescue
|
|
538
|
+
_ -> :error
|
|
539
|
+
end
|
|
540
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
%% The emscripten target has no terminal to query, so terminal capability
|
|
2
|
+
%% detection is skipped and the state is returned unchanged.
|
|
3
|
+
-module(prim_tty).
|
|
4
|
+
-compile([{popcorn_patch_private, [{init, 2}]}]).
|
|
5
|
+
-export([init/2]).
|
|
6
|
+
|
|
7
|
+
init(State, {unix, _} = OsType) ->
|
|
8
|
+
case erlang:system_info(system_architecture) of
|
|
9
|
+
"wasm32-unknown-emscripten" -> State;
|
|
10
|
+
_ -> popcorn_module:init(State, OsType)
|
|
11
|
+
end;
|
|
12
|
+
init(State, OsType) ->
|
|
13
|
+
popcorn_module:init(State, OsType).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-module(beam_lib).
|
|
2
|
+
-export([checksum/1]).
|
|
3
|
+
|
|
4
|
+
checksum(File0) ->
|
|
5
|
+
case catch checksum_1(File0) of
|
|
6
|
+
{ok, _} = Result -> Result;
|
|
7
|
+
Error -> Error
|
|
8
|
+
end.
|
|
9
|
+
|
|
10
|
+
checksum_1(File0) ->
|
|
11
|
+
File = popcorn_module:beam_filename(File0),
|
|
12
|
+
Beam = read_beam(File),
|
|
13
|
+
{ok, Module, _} = popcorn_module:scan_beam(Beam, []),
|
|
14
|
+
case erts_internal:beamfile_module_checksum(Beam) of
|
|
15
|
+
Digest when is_binary(Digest) ->
|
|
16
|
+
{ok, {Module, {xxh3_128, Digest}}};
|
|
17
|
+
undefined ->
|
|
18
|
+
popcorn_module:error({not_a_beam_file, File})
|
|
19
|
+
end.
|
|
20
|
+
|
|
21
|
+
read_beam(Beam) when is_binary(Beam) ->
|
|
22
|
+
popcorn_module:maybe_uncompress(Beam);
|
|
23
|
+
read_beam(File) ->
|
|
24
|
+
case file:read_file(File) of
|
|
25
|
+
{ok, Beam} -> popcorn_module:maybe_uncompress(Beam);
|
|
26
|
+
Error -> popcorn_module:file_error(File, Error)
|
|
27
|
+
end.
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { Plugin } from "esbuild";
|
|
2
|
-
import { type
|
|
3
|
-
|
|
2
|
+
import { type Options } from "./shared";
|
|
3
|
+
/**
|
|
4
|
+
* Copies the worker, VM runtime, and `otp/` assets into the output directory after a successful esbuild build.
|
|
5
|
+
*
|
|
6
|
+
* Requires `format: "esm"` and either `outdir` or `outfile`.
|
|
7
|
+
* If the application bundle is elsewhere, set `PopcornOpts.workerUrl` to the copied `worker.mjs`.
|
|
8
|
+
*
|
|
9
|
+
* @see {@link Options} for application packaging and server requirements.
|
|
10
|
+
*/
|
|
11
|
+
export declare function popcorn(options: Options): Plugin;
|