Skip to content

Commit cab1ccd

Browse files
committed
plugin: use host builtin apps
Manifest combined apps from host system and Wasm BEAM build. We only passed apps from host to the packager script, which created a blindspot for the treeshaker. Packager script now uses host system apps with unconditional inclusion of :elixir app (which pulls :kernel & co., needed for VM to start). This allows treeshaker to see full dependency tree.
1 parent 12d19ce commit cab1ccd

3 files changed

Lines changed: 46 additions & 42 deletions

File tree

popcorn/js/plugins/beam_tools/lib/popcorn/beam_tools/cli.ex

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ defmodule Popcorn.BeamTools.CLI do
1313

1414
def main(argv) do
1515
if String.to_integer(System.otp_release()) < 27 do
16-
IO.puts(:stderr, "tarballs.exs requires host OTP >= 27 for the built-in :json module")
16+
IO.puts(:stderr, "Popcorn requires OTP 27 or later")
1717
System.halt(1)
1818
end
1919

@@ -31,7 +31,7 @@ defmodule Popcorn.BeamTools.CLI do
3131
end
3232

3333
defp parse_argv(argv) do
34-
{opts, tar_paths, invalid} = OptionParser.parse(argv, strict: @options)
34+
{opts, [], invalid} = OptionParser.parse(argv, strict: @options)
3535
missing_opts = Enum.reject(@required_options, &Keyword.has_key?(opts, &1))
3636

3737
case {invalid, missing_opts} do
@@ -41,7 +41,6 @@ defmodule Popcorn.BeamTools.CLI do
4141
|> Map.new()
4242
|> Map.put_new(:entrypoint_app, nil)
4343
|> Map.put_new(:strip, false)
44-
|> Map.put(:tar_paths, tar_paths)
4544

4645
{:ok, options}
4746

popcorn/js/plugins/beam_tools/lib/popcorn/beam_tools/packager.ex

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
defmodule Popcorn.BeamTools.Packager do
22
@static_nif_beams MapSet.new(["wasm.beam"])
33

4+
# To run, Beam needs following apps:
5+
# - kernel
6+
# - stdlib (kernel dep)
7+
#
8+
# We also add elixir support out of the box and need:
9+
# - compiler (elixir dep)
10+
# - elixir
11+
#
12+
# All of them should be in elixir's transitive dependency closure
13+
@base_apps ["elixir"]
14+
415
@type options :: %{
516
root_dir: Path.t(),
617
entrypoint_app: String.t() | nil,
718
out_dir: Path.t(),
819
manifest_path: Path.t(),
9-
strip: boolean(),
10-
tar_paths: [Path.t()]
20+
strip: boolean()
1121
}
1222

1323
defp strip_tarball(path, out_dir) do
@@ -43,8 +53,7 @@ defmodule Popcorn.BeamTools.Packager do
4353
entrypoint_app: entrypoint_app,
4454
out_dir: out_dir,
4555
manifest_path: manifest_path,
46-
strip: strip,
47-
tar_paths: input_tar_paths
56+
strip: strip
4857
} = args
4958

5059
with {:ok, manifest} <- read_manifest(manifest_path),
@@ -57,20 +66,19 @@ defmodule Popcorn.BeamTools.Packager do
5766

5867
File.mkdir_p!(out_dir)
5968

60-
# TODO: We still use builtin apps, needs to change
6169
packed_apps =
6270
apps_info
63-
|> Task.async_stream(fn {app, info} ->
71+
|> async_stream(fn {app, info} ->
6472
version = Keyword.get(info.props, :vsn, ~c"") |> to_string()
6573
tar_path = create_tarball(out_dir, app, info.ebin_dir)
6674

6775
{app, %{tar: tar_path, version: version}}
6876
end)
69-
|> Map.new(fn {:ok, app} -> app end)
77+
|> Map.new()
7078

7179
diagnostics =
7280
apps_info
73-
|> Task.async_stream(fn {app, info} ->
81+
|> async_stream(fn {app, info} ->
7482
case loaded_dynamic_nifs(app, info.ebin_dir) do
7583
[] ->
7684
[]
@@ -80,21 +88,20 @@ defmodule Popcorn.BeamTools.Packager do
8088
[context]
8189
end
8290
end)
83-
|> Enum.flat_map(fn {:ok, diagnostics} -> diagnostics end)
91+
|> Enum.concat()
8492

8593
manifest_path = Path.join(out_dir, "manifest.json")
86-
manifest_apps = Map.merge(packed_apps, manifest.apps)
8794

8895
packed_tar_paths =
8996
packed_apps
9097
|> Map.values()
9198
|> Enum.map(&Path.expand(Path.join(out_dir, &1.tar)))
9299

93-
tar_paths = maybe_strip_tarballs(packed_tar_paths ++ input_tar_paths, strip, out_dir)
100+
tar_paths = maybe_strip_tarballs(packed_tar_paths, strip, out_dir)
94101

95102
manifest = %{
96103
entrypoint: entrypoint_app,
97-
apps: manifest_apps,
104+
apps: packed_apps,
98105
notes: diagnostics,
99106
toolchain: toolchain,
100107
vm: %{boot: "bin/vm.boot", version: vm_version}
@@ -107,7 +114,7 @@ defmodule Popcorn.BeamTools.Packager do
107114
entrypoint: entrypoint_app,
108115
manifestPath: Path.expand(manifest_path),
109116
tarPaths: tar_paths,
110-
apps: manifest_apps,
117+
apps: packed_apps,
111118
notes: diagnostics,
112119
toolchain: toolchain
113120
}
@@ -116,6 +123,12 @@ defmodule Popcorn.BeamTools.Packager do
116123
end
117124
end
118125

126+
defp async_stream(enumerable, fun) do
127+
enumerable
128+
|> Task.async_stream(fun, timeout: :infinity)
129+
|> Enum.map(fn {:ok, result} -> result end)
130+
end
131+
119132
defp maybe_strip_tarballs(paths, false, _out_dir), do: paths
120133

121134
defp maybe_strip_tarballs(paths, true, out_dir) do
@@ -155,34 +168,37 @@ defmodule Popcorn.BeamTools.Packager do
155168

156169
defp read_manifest(manifest_path) do
157170
with {:ok, json} <- File.read(manifest_path),
158-
{:ok, %{"apps" => apps, "vm" => %{"version" => version}}} <- decode_json(json) do
159-
{:ok, %{version: version, apps: apps}}
171+
{:ok, %{"vm" => %{"version" => version}}} <- decode_json(json) do
172+
{:ok, %{version: version}}
160173
else
161174
_ -> err(:bad_manifest, manifest_path)
162175
end
163176
end
164177

165-
defp apps_to_pack(project_apps, _builtin_apps, nil) do
166-
{:ok, Enum.sort_by(project_apps, fn {app, _info} -> app end)}
167-
end
168-
169-
defp apps_to_pack(project_apps, builtin_apps, entrypoint)
170-
when is_map_key(project_apps, entrypoint) do
178+
defp apps_to_pack(project_apps, builtin_apps, entrypoint) do
171179
all_apps_info = Map.merge(builtin_apps, project_apps)
172180

173-
with {:ok, selected_apps} <-
174-
gather_required_apps(all_apps_info, project_apps, entrypoint, MapSet.new()) do
175-
project_apps
181+
gather_from_root = fn app, selected ->
182+
gather_required_apps(all_apps_info, project_apps, app, selected)
183+
end
184+
185+
with {:ok, roots} <- root_apps(project_apps, entrypoint),
186+
{:ok, selected_apps} <- reduce_while_ok(roots, MapSet.new(), gather_from_root) do
187+
all_apps_info
176188
|> Map.filter(fn {app, _info} -> MapSet.member?(selected_apps, app) end)
177189
|> Enum.sort()
178190
|> then(&{:ok, &1})
179191
end
180192
end
181193

182-
defp apps_to_pack(_project_apps, _builtin_apps, entrypoint) do
183-
err(:missing_entrypoint, entrypoint)
194+
defp root_apps(_project_apps, nil), do: {:ok, @base_apps}
195+
196+
defp root_apps(project_apps, entrypoint) when is_map_key(project_apps, entrypoint) do
197+
{:ok, [entrypoint | @base_apps]}
184198
end
185199

200+
defp root_apps(_project_apps, entrypoint), do: err(:missing_entrypoint, entrypoint)
201+
186202
defp gather_required_apps(all_apps_info, project_apps, app, selected) do
187203
if MapSet.member?(selected, app) do
188204
{:ok, selected}

popcorn/js/plugins/shared.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
mkdir,
55
mkdtemp,
66
readFile,
7-
readdir,
87
rm,
98
writeFile,
109
} from "node:fs/promises";
@@ -54,7 +53,6 @@ export async function popcorn(opts: Options): Promise<Prepared> {
5453
];
5554
const distDir = p`${dirname(fileURLToPath(import.meta.url))}/..`;
5655
const preparedDir = await mkdtemp(p`${tmpdir()}/popcorn-otp-`);
57-
const coreTarballs = await pathsIn(p`${distDir}/assets/lib`, ".tar");
5856

5957
try {
6058
await Promise.all([
@@ -83,7 +81,6 @@ export async function popcorn(opts: Options): Promise<Prepared> {
8381
outDir: packedDir,
8482
manifestPath: p`${distDir}/assets/manifest.json`,
8583
app: opts.app,
86-
tarPaths: coreTarballs,
8784
strip,
8885
});
8986

@@ -115,11 +112,10 @@ type PackTarballsParams = {
115112
outDir: string;
116113
manifestPath: string;
117114
app: string | null;
118-
tarPaths: string[];
119115
strip: boolean;
120116
};
121117
async function packTarballs(opts: PackTarballsParams): Promise<Report> {
122-
const { rootDir, outDir, manifestPath, app, tarPaths, strip } = opts;
118+
const { rootDir, outDir, manifestPath, app, strip } = opts;
123119
const toolDir = p`${dirname(fileURLToPath(import.meta.url))}/beam_tools`;
124120

125121
const packerArgs = [
@@ -142,7 +138,6 @@ async function packTarballs(opts: PackTarballsParams): Promise<Report> {
142138
if (strip) {
143139
packerArgs.push("--strip");
144140
}
145-
packerArgs.push(...tarPaths);
146141

147142
const env = {
148143
...process.env,
@@ -181,7 +176,7 @@ function formatPackError(error: unknown): string {
181176
].join("\n ");
182177
}
183178

184-
return `tarballs.exs failed: ${JSON.stringify(error)}`;
179+
return `packaging failed: ${JSON.stringify(error)}`;
185180
}
186181

187182
async function copy(
@@ -240,12 +235,6 @@ function p(
240235
return normalize(String.raw(strings, ...values));
241236
}
242237

243-
async function pathsIn(dir: string, suffix: string): Promise<string[]> {
244-
return (await readdir(dir))
245-
.filter((name) => name.endsWith(suffix))
246-
.map((name) => p`${dir}/${name}`);
247-
}
248-
249238
async function withTmp<T>(f: (dir: string) => Promise<T>): Promise<T> {
250239
const dir = await mkdtemp(p`${tmpdir()}/popcorn-otp-`);
251240
try {

0 commit comments

Comments
 (0)