Skip to content

Commit 4ca9f8a

Browse files
committed
Fix prune of callback modules from AVM libs
When using prune with modules sourced from a .avm library, modules referenced only in literals (e.g., supervisor callback modules in child specs) were incorrectly pruned. The root cause was that parse_beam (the AVM parsing path) did not extract the literals chunk, so get_atoms/1 could not find atoms that only appear in the literals table — such as a worker module name inside a supervisor child spec map. The fix extends the existing beam_lib:chunks/2 call in parse_beam to also fetch the raw "LitT" and "LitU" chunks (compressed and uncompressed literals, respectively), and extracts uncompressed_literals inline. This keeps it to a single beam_lib call and handles both OTP < 28 (compressed LitT) and OTP >= 28 (uncompressed LitT, or LitU from repackaged AVMs). Signed-off-by: Peter M <petermm@gmail.com>
1 parent 1673545 commit 4ca9f8a

5 files changed

Lines changed: 148 additions & 2 deletions

File tree

src/packbeam_api.erl

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -746,8 +746,14 @@ parse_beam(Data, _Tmp, in_data, _Pos, Accum) ->
746746
case is_beam(Accum) orelse is_entrypoint(Accum) of
747747
true ->
748748
StrippedData = strip_padding(Data),
749-
{ok, {Module, ChunkRefs}} = beam_lib:chunks(StrippedData, [imports, exports, atoms]),
750-
[{module, Module}, {chunk_refs, ChunkRefs}, {data, StrippedData} | Accum];
749+
{ok, {Module, ChunkRefs}} = beam_lib:chunks(StrippedData, [imports, exports, atoms, "LitT", "LitU"], [allow_missing_chunks]),
750+
[
751+
{module, Module},
752+
{chunk_refs, ChunkRefs},
753+
{uncompressed_literals, get_uncompressed_literals(ChunkRefs)},
754+
{data, StrippedData}
755+
| Accum
756+
];
751757
_ ->
752758
[{data, Data} | Accum]
753759
end.
@@ -757,6 +763,18 @@ strip_padding(<<0:8, Rest/binary>>) ->
757763
strip_padding(Data) ->
758764
Data.
759765

766+
%% @private
767+
get_uncompressed_literals(ChunkRefs) ->
768+
case proplists:get_value("LitT", ChunkRefs) of
769+
missing_chunk ->
770+
case proplists:get_value("LitU", ChunkRefs) of
771+
missing_chunk -> undefined;
772+
LitU -> LitU
773+
end;
774+
<<0:32, Data/binary>> -> Data;
775+
<<_Size:4/binary, Data/binary>> -> zlib:uncompress(Data)
776+
end.
777+
760778
%% @private
761779
maybe_uncompress_literals(Chunks) ->
762780
case proplists:get_value("LitT", Chunks) of

test/packbeam_api_tests.erl

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,82 @@ packbeam_extract_test() ->
553553

554554
ok.
555555

556+
packbeam_create_prune_supervisor_callback_test() ->
557+
%% Test that prune does not remove a module referenced only in a
558+
%% supervisor child spec literal. In this scenario:
559+
%% start_mod (entrypoint) -> my_sup (via atom) -> my_worker (only in literal)
560+
%% The my_worker atom only appears in the literals table of my_sup,
561+
%% not in its atoms chunk or imports. Prune must still keep it.
562+
AVMFile = dest_dir("packbeam_prune_sup_callback_test.avm"),
563+
?assertMatch(
564+
ok,
565+
packbeam_api:create(
566+
AVMFile,
567+
[
568+
test_beam_path("start_mod.beam"),
569+
test_beam_path("my_sup.beam"),
570+
test_beam_path("my_worker.beam"),
571+
test_beam_path("d.beam")
572+
],
573+
#{prune => true}
574+
)
575+
),
576+
577+
ParsedFiles = packbeam_api:list(AVMFile),
578+
579+
?assert(is_list(ParsedFiles)),
580+
581+
%% start_mod calls my_sup, my_sup has my_worker only in literal child specs.
582+
%% d is not referenced by anyone.
583+
?assert(parsed_file_contains_module(start_mod, ParsedFiles)),
584+
?assert(parsed_file_contains_module(my_sup, ParsedFiles)),
585+
?assert(parsed_file_contains_module(my_worker, ParsedFiles)),
586+
?assertNot(parsed_file_contains_module(d, ParsedFiles)),
587+
588+
ok.
589+
590+
packbeam_create_prune_supervisor_callback_from_avm_test() ->
591+
%% Same as above, but with my_worker coming from a lib AVM file.
592+
%% This tests the AVM parsing path where uncompressed_literals
593+
%% may not be available.
594+
LibAVMFile = dest_dir("packbeam_prune_sup_callback_lib.avm"),
595+
?assertMatch(
596+
ok,
597+
packbeam_api:create(
598+
LibAVMFile,
599+
[
600+
test_beam_path("my_sup.beam"),
601+
test_beam_path("my_worker.beam"),
602+
test_beam_path("d.beam")
603+
],
604+
#{lib => true}
605+
)
606+
),
607+
608+
AVMFile = dest_dir("packbeam_prune_sup_callback_from_avm_test.avm"),
609+
?assertMatch(
610+
ok,
611+
packbeam_api:create(
612+
AVMFile,
613+
[
614+
test_beam_path("start_mod.beam"),
615+
LibAVMFile
616+
],
617+
#{prune => true}
618+
)
619+
),
620+
621+
ParsedFiles = packbeam_api:list(AVMFile),
622+
623+
?assert(is_list(ParsedFiles)),
624+
625+
?assert(parsed_file_contains_module(start_mod, ParsedFiles)),
626+
?assert(parsed_file_contains_module(my_sup, ParsedFiles)),
627+
?assert(parsed_file_contains_module(my_worker, ParsedFiles)),
628+
?assertNot(parsed_file_contains_module(d, ParsedFiles)),
629+
630+
ok.
631+
556632
file_exists(Path) ->
557633
filelib:is_file(Path).
558634

test/prune_sup/my_sup.erl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
%%
2+
%% Copyright (c) 2026 AtomVM
3+
%% All rights reserved.
4+
%%
5+
%% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
6+
7+
%% @doc A supervisor-like module whose init returns child specs.
8+
%% The child module (my_worker) is only referenced as an atom.
9+
-module(my_sup).
10+
11+
-export([start_link/0, init/1]).
12+
13+
start_link() ->
14+
init([]).
15+
16+
init(_Args) ->
17+
ChildSpecs = [
18+
#{
19+
id => my_worker,
20+
start => {my_worker, start_link, []}
21+
}
22+
],
23+
{ok, {#{}, ChildSpecs}}.

test/prune_sup/my_worker.erl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
%%
2+
%% Copyright (c) 2026 AtomVM
3+
%% All rights reserved.
4+
%%
5+
%% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
6+
7+
%% @doc A worker module only referenced as a callback in a supervisor
8+
%% child spec. No module directly imports this module.
9+
-module(my_worker).
10+
11+
-export([start_link/0]).
12+
13+
start_link() ->
14+
ok.

test/prune_sup/start_mod.erl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
%%
2+
%% Copyright (c) 2026 AtomVM
3+
%% All rights reserved.
4+
%%
5+
%% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
6+
7+
%% @doc A start module that starts a supervisor.
8+
%% The supervisor module (my_sup) is referenced via atoms and imports.
9+
-module(start_mod).
10+
11+
-export([start/0]).
12+
13+
start() ->
14+
Sup = my_sup,
15+
Sup:start_link().

0 commit comments

Comments
 (0)