Skip to content

Commit

Permalink
Add support for OTP application framework in AtomVM
Browse files Browse the repository at this point in the history
Signed-off-by: Fred Dushin <fred@dushin.net>
  • Loading branch information
fadushin committed Dec 17, 2023
1 parent 7b7a329 commit 92b71f4
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 15 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.7.4] (unreleased)

- Added support for the `--application` (or `-a`) option to support AtomVM OTP applications.

### Changed

- Using the `-s init` option is still supported but deprecated. Use the `--application` (or `-a`) option to generate OTP applications using AtomVM.

## [0.7.3] (2023.11.25)

- Added support for compiling "bootstrap" erlang files that `rebar3` otherwise cannot compile.
Expand Down
6 changes: 6 additions & 0 deletions assets/init_shim.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-module(init_shim).

-export([start/0]).

start() ->
init:start().
76 changes: 61 additions & 15 deletions src/atomvm_packbeam_provider.erl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
{force, $f, "force", boolean, "Force rebuild"},
{prune, $p, "prune", boolean, "Prune unreferenced BEAM files"},
{start, $s, "start", atom, "Start module"},
{application, $a, "application", boolean, "Build a OTP application"},
{remove_lines, $r, "remove_lines", boolean, "Remove line information from generated AVM files (off by default)"},
{list, $l, "list", boolean, "List the contents of AVM files after creation"}
]).
Expand All @@ -38,10 +39,26 @@
force => false,
prune => false,
start => undefined,
application => false,
remove_lines => false,
list => false
}).

%% abstract representation of a simple shim that
%% delegates to `init:start/0`. This form will
%% be compiled and inserted into the AVM if the
%% user has indicated that the project is an (OTP)
%% application.
-define(INIT_FORMS, [
{attribute, 1, file, {"init_shim.erl", 1}},
{attribute, 1, module, init_shim},
{attribute, 3, export, [{start, 0}]},
{function, 5, start, 0, [
{clause, 5, [], [], [{call, 6, {remote, 6, {atom, 6, init}, {atom, 6, start}}, []}]}
]},
{eof, 7}
]).

-record(file_set, {
name, out_dir, beam_files, priv_files, app_file
}).
Expand Down Expand Up @@ -86,6 +103,7 @@ do(State) ->
maps:get(prune, Opts),
maps:get(force, Opts),
get_start_module(Opts),
maps:get(application, Opts),
not maps:get(remove_lines, Opts),
maps:get(list, Opts)
),
Expand Down Expand Up @@ -143,16 +161,16 @@ squash_external_avms(ParsedArgs) ->
).

%% @private
do_packbeam(ProjectApps, Deps, ExternalAVMs, Prune, Force, StartModule, IncludeLines, List) ->
do_packbeam(ProjectApps, Deps, ExternalAVMs, Prune, Force, StartModule, IsApplication, IncludeLines, List) ->
DepFileSets = [get_files(Dep) || Dep <- Deps],
ProjectAppFileSets = [get_files(ProjectApp) || ProjectApp <- ProjectApps],
DepsAvms = [
maybe_create_packbeam(DepFileSet, [], false, Force, undefined, IncludeLines, false)
maybe_create_packbeam(DepFileSet, [], false, Force, undefined, false, IncludeLines, false)
|| DepFileSet <- DepFileSets
],
[
maybe_create_packbeam(
ProjectAppFileSet, DepsAvms ++ ExternalAVMs, Prune, Force, StartModule, IncludeLines, List
ProjectAppFileSet, DepsAvms ++ ExternalAVMs, Prune, Force, StartModule, IsApplication, IncludeLines, List
)
|| ProjectAppFileSet <- ProjectAppFileSets
],
Expand Down Expand Up @@ -224,7 +242,7 @@ get_all_files(Dir) ->
RegularFiles ++ SubFiles.

%% @private
maybe_create_packbeam(FileSet, AvmFiles, Prune, Force, StartModule, IncludeLines, List) ->
maybe_create_packbeam(FileSet, AvmFiles, Prune, Force, StartModule, IsApplication, IncludeLines, List) ->
#file_set{
name = Name,
out_dir = OutDir,
Expand All @@ -237,7 +255,7 @@ maybe_create_packbeam(FileSet, AvmFiles, Prune, Force, StartModule, IncludeLines
AppFiles = case AppFile of undefined -> []; _ -> [AppFile] end,
case Force orelse needs_build(TargetAVM, BeamFiles ++ PrivFiles ++ AvmFiles ++ AppFiles) of
true ->
create_packbeam(FileSet, AvmFiles, Prune, StartModule, IncludeLines, List);
create_packbeam(FileSet, AvmFiles, Prune, StartModule, IsApplication, IncludeLines, List);
_ ->
rebar_api:debug("No packbeam build needed.", []),
TargetAVM
Expand All @@ -258,7 +276,7 @@ latest_modified_time(PathList) ->
lists:max([modified_time(Path) || Path <- PathList]).

%% @private
create_packbeam(FileSet, AvmFiles, Prune, StartModule, IncludeLines, List) ->
create_packbeam(FileSet, AvmFiles, Prune, StartModule, IsApplication, IncludeLines, List) ->
#file_set{
name = Name,
out_dir = OutDir,
Expand All @@ -269,21 +287,31 @@ create_packbeam(FileSet, AvmFiles, Prune, StartModule, IncludeLines, List) ->
N = length(OutDir) + 1,
PrivFilesRelative = [filename:join(Name, string:slice(PrivFile, N)) || PrivFile <- PrivFiles],
{ApplicationModule, AppFileBinFiles} = create_app_file_bin_files(Name, OutDir, AppFile),
BootFile = case StartModule of
init ->
[create_boot_file(OutDir, ApplicationModule)];
_ ->
[]
end,
BootFiles =
case IsApplication of
true ->
[create_boot_file(OutDir, ApplicationModule), create_init_shim(OutDir)];
_ ->
case StartModule of
init ->
rebar_api:warn(
"Specifying `init` as the start module to generate an OTP application is deprecated. "
"Use the `--application` (or `-a`) option, instead.", []
),
[create_boot_file(OutDir, ApplicationModule)];
_ ->
[]
end
end,
Cwd = rebar_dir:get_cwd(),
try
DirName = filename:dirname(OutDir),
ok = file:set_cwd(DirName),
AvmFilename = Name ++ ".avm",
FileList = reorder_beamfiles(BeamFiles) ++ AppFileBinFiles ++ BootFile ++ PrivFilesRelative ++ AvmFiles,
FileList = reorder_beamfiles(BeamFiles) ++ AppFileBinFiles ++ BootFiles ++ PrivFilesRelative ++ AvmFiles,
Opts = #{
prune => Prune,
start_module => StartModule,
start_module => effective_start_module(StartModule, IsApplication),
application_module => ApplicationModule,
include_lines => IncludeLines
},
Expand All @@ -301,6 +329,13 @@ create_packbeam(FileSet, AvmFiles, Prune, StartModule, IncludeLines, List) ->
ok = file:set_cwd(Cwd)
end.

%% @private
effective_start_module(_StartModule, true) ->
init_shim;
effective_start_module(StartModule, _) ->
StartModule.


%% @private
maybe_list(_, false) ->
ok;
Expand Down Expand Up @@ -355,7 +390,18 @@ create_boot_file(OutDir, ApplicationModule) ->
WritePath = filename:join([OutDir, "start.boot"]),
Bin = erlang:term_to_binary(BootSpec),
ok = file:write_file(WritePath, Bin),
{WritePath, filename:join(["init", "priv", "start.boot"])}.
StartBootPath = filename:join(["init", "priv", "start.boot"]),
rebar_api:debug("Created boot file ~s in ~s", [StartBootPath, WritePath]),
{WritePath, StartBootPath}.

%% @private
create_init_shim(OutDir) ->
EBinDir = filename:join([OutDir, "ebin"]),
{ok, init_shim, Data} = compile:forms(?INIT_FORMS, []),
WritePath = filename:join([EBinDir, "init_shim.beam"]),
ok = file:write_file(WritePath, Data),
rebar_api:debug("Created init_shim.beam in ~s", [WritePath]),
{WritePath, "init_shim.beam"}.

%% @private
reorder_beamfiles(BeamFiles) ->
Expand Down
25 changes: 25 additions & 0 deletions test/driver/apps/otp_application/rebar.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
%%
%% Copyright (c) 2023 <fred@dushin.net>
%% All rights reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%

{erl_opts, [debug_info]}.
{deps, []}.
{plugins, [
atomvm_rebar3_plugin
]}.
{atomvm_rebar3_plugin, [
{packbeam, [application, prune]}
]}.
30 changes: 30 additions & 0 deletions test/driver/apps/otp_application/src/my_app.app.src
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
%%
%% Copyright (c) 2023 <fred@dushin.net>
%% All rights reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%

{application, my_app, [
{description, "An AtomVM application"},
{vsn, "0.1.0"},
{registered, []},
{applications, [
kernel, stdlib
]},
{mod, {my_app, #{foo => bar}}},
{env,[]},
{modules, []},
{licenses, ["Apache-2.0"]},
{links, []}
]}.
25 changes: 25 additions & 0 deletions test/driver/apps/otp_application/src/my_app.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
%%
%% Copyright (c) 2023 <fred@dushin.net>
%% All rights reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
-module(my_app).

-export([start/2, stop/1]).

start(_StartType, _StartArgs) ->
{ok, dummy_pid}.

stop(_State) ->
ok.
34 changes: 34 additions & 0 deletions test/driver/src/packbeam_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ run(Opts) ->
ok = test_start(Opts),
ok = test_prune(Opts),
ok = test_rebar_overrides(Opts),
ok = test_otp_application(Opts),
ok.

%% @private
Expand Down Expand Up @@ -178,6 +179,39 @@ test_rebar_overrides(Opts) ->

test:tick().

%% @private
test_otp_application(Opts) ->

AppsDir = maps:get(apps_dir, Opts),
AppDir = test:make_path([AppsDir, "otp_application"]),

Cmd = create_packbeam_cmd(AppDir, ["-f"], []), %% -f temporary during dev
Output = test:execute_cmd(Cmd, Opts),
test:debug(Output, Opts),

ok = test:expect_contains("AVM file written to", Output),
ok = test:expect_contains("_build/default/lib/my_app.avm", Output),
AVMPath = test:make_path([AppDir, "_build/default/lib/my_app.avm"]),
ok = test:file_exists(AVMPath),
AVMElements = test:get_avm_elements(AVMPath),

[InitShimBeam | _Rest] = AVMElements,
true = packbeam_api:is_beam(InitShimBeam),
true = packbeam_api:is_entrypoint(InitShimBeam),

{value, StartBoot} = test:find_avm_element_by_name("init/priv/start.boot", AVMElements),
false = packbeam_api:is_beam(StartBoot),

{value, MyAppBeam} = test:find_avm_element_by_name("my_app.beam", AVMElements),
true = packbeam_api:is_beam(MyAppBeam),
false = packbeam_api:is_entrypoint(MyAppBeam),

{value, MyAppApplicationBin} = test:find_avm_element_by_name("my_app/priv/application.bin", AVMElements),
false = packbeam_api:is_beam(MyAppApplicationBin),
false = packbeam_api:is_entrypoint(MyAppApplicationBin),

test:tick().

%% @private
create_packbeam_cmd(AppDir, Opts, Env) ->
test:create_rebar3_cmd(AppDir, packbeam, Opts, Env).

0 comments on commit 92b71f4

Please sign in to comment.