From 8f82541a2936519eb67ba355aaf54ae90a096a06 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 1 Nov 2025 09:18:10 +0100 Subject: [PATCH 1/4] Add escriptize provider Requires support for AtomVM: https://github.com/atomvm/AtomVM/pull/1948 NB: it's currently incompatible with JIT Signed-off-by: Paul Guyot --- CHANGELOG.md | 1 + README.md | 23 + src/atomvm_escriptize_provider.erl | 456 ++++++++++++++++++ src/atomvm_packbeam_provider.erl | 13 +- src/atomvm_rebar3_plugin.erl | 1 + test/driver/apps/myscript/rebar.config | 28 ++ .../driver/apps/myscript/src/myscript.app.src | 32 ++ test/driver/apps/myscript/src/myscript.erl | 30 ++ test/driver/src/escriptize_tests.erl | 53 ++ test/driver/src/test.erl | 4 + 10 files changed, 638 insertions(+), 3 deletions(-) create mode 100644 src/atomvm_escriptize_provider.erl create mode 100644 test/driver/apps/myscript/rebar.config create mode 100644 test/driver/apps/myscript/src/myscript.app.src create mode 100644 test/driver/apps/myscript/src/myscript.erl create mode 100644 test/driver/src/escriptize_tests.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index ee5e997..44a9e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added dialyzer task to simplify running dialyzer on AtomVM applications. - Added support for rp2350 devices to allow for default detection of the device mount path. - Added configuration paramenter for setting the path to picotool for the pico_flash task. +- Added escriptize task to build escriptize-like bundled binaries with AtomVM. ### Changed - The `uf2create` task now creates `universal` format uf2 files by default, suitable for both diff --git a/README.md b/README.md index 8763e38..2f90aa1 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ The [`rebar3`](https://rebar3.org) plugin provides the following tasks under the * `version` Print the version of the [`atomvm_rebar3_plugin`](https://atomvm.github.io/atomvm_rebar3_plugin) to the console. * `bootstrap` Compile Erlang files that `rebar3` otherwise cannot compile. Typically, such files include modules from the OTP `kernel` or `stdlib` application that `rebar3` uses internally for its own implementation. * `dialyzer` Use dialyzer for static analysis of AtomVM applications. +* `escriptize` Generate a standalone binary for the current host, using AtomVM. > IMPORTANT! Some of the above tasks were previously located under the default [`rebar3`](https://rebar3.org) namespace; however, the commands under the default namespace have been DEPRECATED. Users will get a warning message on the console when using deprecated tasks, and any deprecated tasks may be removed in the future without warning. Be sure to migrate any scripts or code you have to use the `atomvm` namespace. @@ -615,7 +616,29 @@ Example: Any setting specified on the command line take precedence over entries in `rebar.config`, which in turn take precedence over the default values specified above. +### The `escriptize` task +Use the `escriptize` task to generate a standalone binary combining AtomVM virtual machine and the application. The binary can then be copied to another host with the same architecture and would work provided that mbedtls and zlib are installed. + +To use this task, you need to define a module that exports a `main/1` function, following `rebar3` `escriptize` command. This function is invoked with the command line parameters, as strings. + +`-spec main(Args :: [string()]) -> ok | 0 | any().` + +If the function returns ok or 0, the `main` entry point will return 0 as its exit code. Otherwise, it will return 1. Please note that AtomVM does not implement `erlang:halt/1` as of this writing. + +The module that exports this `main/1` function should be declared in `rebar.config` file with either: +- `start` option of `packbeam` task (see above): +``` +{atomvm_rebar3_plugin, [{packbeam, [{start, main_module}]}]}.` +``` +- `rebar3` standard `escript_name`: +``` +{escript_name, main_module}. +``` +- `rebar3` standard `escript_main_app` which `escript_name` defaults to: +``` +{escript_main_app, main_module}. +``` ## AtomVM App Template diff --git a/src/atomvm_escriptize_provider.erl b/src/atomvm_escriptize_provider.erl new file mode 100644 index 0000000..fab74ad --- /dev/null +++ b/src/atomvm_escriptize_provider.erl @@ -0,0 +1,456 @@ +%% +%% Copyright (c) 2025 Paul Guyot +%% 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. +%% +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(atomvm_escriptize_provider). + +-behaviour(provider). + +-export([init/1, do/1, format_error/1]). + +-include_lib("kernel/include/file.hrl"). + +-define(PROVIDER, escriptize). +-define(DEPS, [packbeam]). +-define(OPTS, [ + {atomvm_binary, $b, "atomvm_binary", string, "Path to AtomVM binary (default: which AtomVM)"}, + {atomvmlib, $l, "atomvmlib", string, "Path to atomvmlib.avm"}, + {output, $o, "output", string, "Output executable name (default: app name)"}, + {objcopy, $c, "objcopy", string, "Path to objcopy tool (auto-detected if not specified)"}, + {start, $s, "start", atom, "Start module (default: app name)"} +]). + +-define(DEFAULT_OPTS, #{ + atomvm_binary => undefined, + atomvmlib => undefined, + output => undefined, + objcopy => undefined, + start => undefined +}). + +%% +%% provider implementation +%% +-spec init(rebar_state:t()) -> {ok, rebar_state:t()}. +init(State) -> + Provider = providers:create([ + % The atomvm namespace + {namespace, atomvm}, + % The 'user friendly' name of the task + {name, ?PROVIDER}, + % The module implementation of the task + {module, ?MODULE}, + % The task can be run by the user, always true + {bare, true}, + % The list of dependencies + {deps, ?DEPS}, + % How to use the plugin + {example, "rebar3 atomvm escriptize"}, + % list of options understood by the plugin + {opts, ?OPTS}, + {short_desc, "Create a standalone executable with embedded AVM"}, + {desc, + "~n" + "Use this plugin to create a standalone executable by embedding an AVM file " + "into the AtomVM binary using objcopy.~n"} + ]), + {ok, rebar_state:add_provider(State, Provider)}. + +-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. +do(State) -> + try + Opts = get_opts(State), + rebar_api:debug("Effective opts for ~p: ~p", [?PROVIDER, Opts]), + + % Get app info + [App] = [ProjectApp || ProjectApp <- rebar_state:project_apps(State)], + OutDir = rebar_app_info:out_dir(App), + Name = binary_to_list(rebar_app_info:name(App)), + DirName = filename:dirname(OutDir), + + % Get paths + TargetAVM = filename:join(DirName, Name ++ ".avm"), + AtomVMLib = get_atomvmlib_path(Opts), + AtomVMBinary = get_atomvm_binary(Opts), + ObjCopyTool = get_objcopy_tool(Opts), + OutputExe = get_output_path(Opts, DirName, Name), + + % Get start module (default to app name) + StartModule = + case maps:get(start, Opts) of + undefined -> list_to_atom(Name); + Module -> Module + end, + + % Create packed AVM with atomvmlib + PackedAVM = create_packed_avm(TargetAVM, AtomVMLib, DirName, Name, StartModule), + + % Copy AtomVM binary + ok = copy_atomvm_binary(AtomVMBinary, OutputExe), + + % Embed AVM into executable + ok = embed_avm(ObjCopyTool, OutputExe, PackedAVM), + + % Make executable + ok = make_executable(OutputExe), + + rebar_api:info("Created standalone executable: ~s", [OutputExe]), + {ok, State} + catch + C:E:S -> + rebar_api:error( + "An error occurred in the ~p task. Class=~p Error=~p Stacktrace=~p~n", [ + ?PROVIDER, C, E, S + ] + ), + {error, E} + end. + +-spec format_error(any()) -> iolist(). +format_error(Reason) -> + io_lib:format("~p", [Reason]). + +%% +%% internal functions +%% + +%% @private +get_opts(State) -> + {ParsedArgs, _} = rebar_state:command_parsed_args(State), + RebarOpts = atomvm_rebar3_plugin:get_atomvm_rebar_provider_config(State, ?PROVIDER), + ParsedOpts = atomvm_rebar3_plugin:proplist_to_map(ParsedArgs), + maps:merge(?DEFAULT_OPTS, maps:merge(RebarOpts, ParsedOpts)). + +%% @private +get_atomvmlib_path(Opts) -> + case maps:get(atomvmlib, Opts) of + undefined -> + % Try to find atomvmlib.avm in common locations + case find_atomvmlib() of + {ok, Path} -> Path; + {error, Reason} -> throw({atomvmlib_not_found, Reason}) + end; + Path -> + case filelib:is_file(Path) of + true -> Path; + false -> throw({atomvmlib_not_found, Path}) + end + end. + +%% @private +find_atomvmlib() -> + % First try to infer from AtomVM wrapper script + case os:find_executable("AtomVM") of + false -> + find_atomvmlib_fallback(); + WrapperPath -> + case infer_atomvmlib_from_wrapper(WrapperPath) of + {ok, Path} -> {ok, Path}; + {error, _} -> find_atomvmlib_fallback() + end + end. + +%% @private +find_atomvmlib_fallback() -> + % Try to find atomvmlib.avm in common locations + PossiblePaths = [ + "/opt/local/lib/atomvm/atomvmlib.avm", + "/usr/local/lib/atomvm/atomvmlib.avm", + "/usr/lib/atomvm/atomvmlib.avm", + filename:join([os:getenv("HOME", "/tmp"), ".atomvm", "lib", "atomvmlib.avm"]) + ], + case lists:filter(fun filelib:is_file/1, PossiblePaths) of + [Path | _] -> {ok, Path}; + [] -> {error, "Could not find atomvmlib.avm. Please specify with --atomvmlib option"} + end. + +%% @private +infer_atomvmlib_from_wrapper(WrapperPath) -> + % The wrapper script references atomvmlib.avm at ${avm_lib}/atomvm/atomvmlib.avm + % where avm_lib="${avm_root}/lib" and avm_root is the prefix + Dir = filename:dirname(WrapperPath), + Prefix = filename:dirname(Dir), + AtomVMLibPath = filename:join([Prefix, "lib", "atomvm", "atomvmlib.avm"]), + case filelib:is_file(AtomVMLibPath) of + true -> {ok, AtomVMLibPath}; + false -> {error, not_found} + end. + +%% @private +get_atomvm_binary(Opts) -> + case maps:get(atomvm_binary, Opts) of + undefined -> + % Use which to find AtomVM + case find_atomvm_binary() of + {ok, Path} -> Path; + {error, Reason} -> throw({atomvm_binary_not_found, Reason}) + end; + Path -> + case filelib:is_file(Path) of + true -> Path; + false -> throw({atomvm_binary_not_found, Path}) + end + end. + +%% @private +find_atomvm_binary() -> + case os:find_executable("AtomVM") of + false -> + {error, "AtomVM binary not found in PATH. Please specify with --atomvm_binary option"}; + Path -> + % Check if it's a shell script wrapper and find the actual binary + case resolve_atomvm_binary(Path) of + {ok, BinaryPath} -> {ok, BinaryPath}; + % Fall back to original path + {error, _} -> {ok, Path} + end + end. + +%% @private +resolve_atomvm_binary(Path) -> + % Try to read the file to see if it's a shell script + case file:read_file(Path) of + {ok, Content} -> + case binary:match(Content, <<"#!/bin/sh">>) of + {0, _} -> + % It's a shell script, parse it to find the actual binary + % The standard wrapper is at /prefix/bin/AtomVM + % The actual binary is at /prefix/lib/atomvm/AtomVM + Dir = filename:dirname(Path), + Prefix = filename:dirname(Dir), + ActualBinary = filename:join([Prefix, "lib", "atomvm", "AtomVM"]), + case filelib:is_file(ActualBinary) of + true -> {ok, ActualBinary}; + false -> {error, not_found} + end; + _ -> + {error, not_a_script} + end; + {error, Reason} -> + {error, Reason} + end. + +%% @private +get_objcopy_tool(Opts) -> + case maps:get(objcopy, Opts) of + undefined -> + case find_objcopy() of + {ok, Path} -> Path; + {error, Reason} -> throw({objcopy_not_found, Reason}) + end; + Path -> + Path + end. + +%% @private +find_objcopy() -> + % Try different objcopy variants + Tools = + case os:type() of + {unix, darwin} -> + % On macOS, prefer llvm-objcopy and try MacPorts variants + [ + "llvm-objcopy", + "llvm-objcopy-mp-21", + "llvm-objcopy-mp-20", + "llvm-objcopy-mp-19", + "objcopy" + ]; + {unix, linux} -> + % On Linux, prefer objcopy, then llvm-objcopy + ["objcopy", "llvm-objcopy"]; + _ -> + ["objcopy", "llvm-objcopy"] + end, + case find_first_executable(Tools) of + {ok, Path} -> {ok, Path}; + error -> {error, "No objcopy tool found. Please install llvm or binutils"} + end. + +%% @private +find_first_executable([]) -> + error; +find_first_executable([Tool | Rest]) -> + case os:find_executable(Tool) of + false -> find_first_executable(Rest); + Path -> {ok, Path} + end. + +%% @private +get_output_path(Opts, DirName, Name) -> + case maps:get(output, Opts) of + undefined -> + % Place executable in _build/default/bin/ like standard rebar3 escriptize + BuildDir = filename:dirname(DirName), + BinDir = filename:join(BuildDir, "bin"), + ok = filelib:ensure_dir(filename:join(BinDir, "dummy")), + filename:join(BinDir, Name); + OutputName -> + case filename:dirname(OutputName) of + "." -> + BuildDir = filename:dirname(DirName), + BinDir = filename:join(BuildDir, "bin"), + ok = filelib:ensure_dir(filename:join(BinDir, "dummy")), + filename:join(BinDir, OutputName); + _ -> + OutputName + end + end. + +%% @private +create_packed_avm(TargetAVM, AtomVMLib, DirName, Name, StartModule) -> + PackedAVM = filename:join(DirName, Name ++ "_packed.avm"), + + % Use packbeam_api to create a new AVM with atomvmlib and the app AVM + rebar_api:debug("Creating packed AVM with atomvmlib: ~s (start: ~p)", [PackedAVM, StartModule]), + + % Read both AVM files + case {filelib:is_file(AtomVMLib), filelib:is_file(TargetAVM)} of + {true, true} -> + packbeam_api:create(PackedAVM, [AtomVMLib, TargetAVM], #{start => StartModule}), + rebar_api:info("Created packed AVM: ~s with start module ~p", [PackedAVM, StartModule]), + PackedAVM; + {false, _} -> + throw({file_not_found, AtomVMLib}); + {_, false} -> + throw({file_not_found, TargetAVM}) + end. + +%% @private +copy_atomvm_binary(Source, Dest) -> + rebar_api:debug("Copying AtomVM binary from ~s to ~s", [Source, Dest]), + case file:copy(Source, Dest) of + {ok, _} -> + ok; + {error, Reason} -> + throw({copy_failed, Source, Dest, Reason}) + end. + +%% @private +embed_avm(ObjCopyTool, Executable, AVMFile) -> + % Use section name without dot on Linux for automatic symbol generation + % Use segment/section syntax on macOS + SectionName = + case os:type() of + {unix, linux} -> "atomvm_avm"; + _ -> ".atomvm_avm" + end, + + % Determine the objcopy command based on OS + case os:type() of + {unix, darwin} -> + % On macOS, use segment/section syntax + Cmd = lists:flatten( + io_lib:format( + "~s --add-section __ATOMVM,__avm_data=~s ~s", + [ObjCopyTool, AVMFile, Executable] + ) + ), + rebar_api:debug("Embedding AVM with command: ~s", [Cmd]), + run_objcopy_cmd(Cmd); + {unix, linux} -> + % On Linux: Step 1 - Add the section + {ok, AVMInfo} = file:read_file_info(AVMFile), + AVMSize = AVMInfo#file_info.size, + + Cmd1 = lists:flatten( + io_lib:format( + "~s --add-section ~s=~s --set-section-flags ~s=alloc,load,readonly,data ~s", + [ObjCopyTool, SectionName, AVMFile, SectionName, Executable] + ) + ), + rebar_api:debug("Step 1 - Adding section: ~s", [Cmd1]), + ok = run_objcopy_cmd(Cmd1), + + % Step 2 - Add symbols at section boundaries + set_atomvm_avm_info(ObjCopyTool, Executable, SectionName, AVMSize); + _ -> + % Default to Linux syntax + Cmd = lists:flatten( + io_lib:format( + "~s --add-section ~s=~s --set-section-flags ~s=alloc,readonly,data ~s", + [ObjCopyTool, SectionName, AVMFile, SectionName, Executable] + ) + ), + rebar_api:debug("Embedding AVM with command: ~s", [Cmd]), + run_objcopy_cmd(Cmd) + end. + +%% @private +run_objcopy_cmd(Cmd) -> + case os:cmd(Cmd ++ " 2>&1") of + "" -> + ok; + Output -> + % Check if it's just a warning or an actual error + case string:str(Output, "error") of + 0 -> + rebar_api:debug("objcopy output: ~s", [Output]), + ok; + _ -> + throw({embed_failed, Output}) + end + end. + +%% @private +set_atomvm_avm_info(ObjCopyTool, Executable, SectionName, SectionSize) -> + % Parse objdump to get the section's offset and length + ObjdumpCmd = lists:flatten( + io_lib:format("objdump -h ~s | grep '~s' | grep -v atomvm_avm_info", [ + Executable, SectionName + ]) + ), + rebar_api:debug("Step 2 - Get section offset and size: ~s", [ObjdumpCmd]), + Output = os:cmd(ObjdumpCmd), + % Parse output: " 18 atomvm_avm 000394ec 000000000014e8e5 ..." + % Fields are: Idx Name Size VMA LMA Offset Alignment + Fields = string:tokens(string:trim(Output), " \t"), + SizeHex = lists:nth(3, Fields), + % Ensure we got it right + SectionSize = list_to_integer(SizeHex, 16), + OffsetHex = lists:nth(6, Fields), + Offset = list_to_integer(OffsetHex, 16), + + % Write new info size + AVMInfoTempFile = Executable ++ ".atomvm_avm_info", + ok = file:write_file(AVMInfoTempFile, <>), + + ObjCopyCmd = lists:flatten( + io_lib:format( + "~s --update-section .atomvm_avm_info=~s --set-section-flags .atomvm_avm_info=alloc,load,readonly,data ~s", + [ObjCopyTool, AVMInfoTempFile, Executable] + ) + ), + rebar_api:debug("Step 3 - Replace info section: ~s", [ObjCopyCmd]), + ok = run_objcopy_cmd(ObjCopyCmd), + ok = file:delete(AVMInfoTempFile). + +%% @private +make_executable(Path) -> + rebar_api:debug("Making ~s executable", [Path]), + case file:read_file_info(Path) of + {ok, FileInfo} -> + NewMode = FileInfo#file_info.mode bor 8#00111, + case file:write_file_info(Path, FileInfo#file_info{mode = NewMode}) of + ok -> ok; + {error, Reason} -> throw({chmod_failed, Path, Reason}) + end; + {error, Reason} -> + throw({stat_failed, Path, Reason}) + end. diff --git a/src/atomvm_packbeam_provider.erl b/src/atomvm_packbeam_provider.erl index 58b9adc..5fd2280 100644 --- a/src/atomvm_packbeam_provider.erl +++ b/src/atomvm_packbeam_provider.erl @@ -111,7 +111,7 @@ do(State) -> maps:get(external, Opts), maps:get(prune, Opts), maps:get(force, Opts), - get_start_module(Opts), + get_start_module(State, Opts), maps:get(application, Opts), not maps:get(remove_lines, Opts), maps:get(list, Opts) @@ -127,9 +127,16 @@ do(State) -> {error, E} end. -get_start_module(Opts) -> +get_start_module(State, Opts) -> case maps:get(start, Opts, undefined) of - undefined -> undefined; + undefined -> + % Default to escript_name or escript_main_app + case rebar_state:get(State, escript_name, undefined) of + undefined -> + rebar_state:get(State, escript_main_app, undefined); + EscriptName -> + EscriptName + end; StartModule -> StartModule end. diff --git a/src/atomvm_rebar3_plugin.erl b/src/atomvm_rebar3_plugin.erl index c27f132..1bcb95f 100644 --- a/src/atomvm_rebar3_plugin.erl +++ b/src/atomvm_rebar3_plugin.erl @@ -28,6 +28,7 @@ atomvm_bootstrap_provider, atomvm_packbeam_provider, atomvm_dialyzer_provider, + atomvm_escriptize_provider, atomvm_esp32_flash_provider, atomvm_pico_flash_provider, atomvm_stm32_flash_provider, diff --git a/test/driver/apps/myscript/rebar.config b/test/driver/apps/myscript/rebar.config new file mode 100644 index 0000000..4dd6c5b --- /dev/null +++ b/test/driver/apps/myscript/rebar.config @@ -0,0 +1,28 @@ +%% +%% Copyright (c) 2023 +%% 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. +%% +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +{erl_opts, [debug_info]}. +{deps, []}. +{plugins, [ + atomvm_rebar3_plugin +]}. +{atomvm_rebar3_plugin, [ + {packbeam, [{start, myscript}, prune]} +]}. diff --git a/test/driver/apps/myscript/src/myscript.app.src b/test/driver/apps/myscript/src/myscript.app.src new file mode 100644 index 0000000..6887edd --- /dev/null +++ b/test/driver/apps/myscript/src/myscript.app.src @@ -0,0 +1,32 @@ +%% +%% Copyright (c) 2025 Paul Guyot +%% 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. +%% +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +{application, myscript, [ + {description, "An AtomVM-powered script"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, [ + kernel, stdlib + ]}, + {env, []}, + {modules, []}, + {licenses, ["Apache-2.0"]}, + {links, []} +]}. diff --git a/test/driver/apps/myscript/src/myscript.erl b/test/driver/apps/myscript/src/myscript.erl new file mode 100644 index 0000000..9326a93 --- /dev/null +++ b/test/driver/apps/myscript/src/myscript.erl @@ -0,0 +1,30 @@ +%% +%% Copyright (c) 2025 Paul Guyot +%% 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. +%% +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% +-module(myscript). + +-export([main/1]). + +-spec main(Args :: [string()]) -> 0 | ok | any(). +main(Args) -> + case Args of + [] -> 0; + ["error"] -> 1; + Other -> io:format("Args = ~p\n", [Other]) + end. diff --git a/test/driver/src/escriptize_tests.erl b/test/driver/src/escriptize_tests.erl new file mode 100644 index 0000000..713a305 --- /dev/null +++ b/test/driver/src/escriptize_tests.erl @@ -0,0 +1,53 @@ +%% +%% Copyright (c) 2025 Paul Guyot +%% 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. +%% +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% +-module(escriptize_tests). + +-export([run/1]). + +run(Opts) -> + ok = test_defaults(Opts), + ok. + +%% @private +test_defaults(Opts) -> + AppsDir = maps:get(apps_dir, Opts), + AppDir = test:make_path([AppsDir, "myscript"]), + + Cmd = create_escriptize_cmd(AppDir, [], []), + Output = test:execute_cmd(Cmd, Opts), + test:debug(Output, Opts), + + ok = test:expect_contains("Created packed AVM:", Output), + ok = test:expect_contains("_build/default/lib/myscript_packed.avm", Output), + ok = test:expect_contains("with start module myscript", Output), + + ok = test:expect_contains("Created standalone executable:", Output), + ok = test:expect_contains("_build/default/bin/myscript", Output), + + ExecPath = test:make_path([AppDir, "_build/default/bin/myscript"]), + ok = test:file_exists(ExecPath), + + [] = test:execute_cmd(ExecPath), + + test:tick(). + +%% @private +create_escriptize_cmd(AppDir, Opts, Env) -> + test:create_rebar3_cmd(AppDir, escriptize, Opts, Env). diff --git a/test/driver/src/test.erl b/test/driver/src/test.erl index e6cde06..8a388a3 100644 --- a/test/driver/src/test.erl +++ b/test/driver/src/test.erl @@ -67,6 +67,10 @@ run_tests(Opts) -> ok = bootstrap_tests:run(Opts), io:put_chars("\n"), + io:put_chars("escriptize_tests: "), + ok = escriptize_tests:run(Opts), + io:put_chars("\n"), + ok. make_path(Elements) -> From 6fc8beb25df04827b51bab9581d4bf80ac2f4c9d Mon Sep 17 00:00:00 2001 From: Winford Date: Mon, 8 Dec 2025 22:55:25 -0800 Subject: [PATCH 2/4] Small adjustments to escriptize provider and test (#1) This makes a few small adjustments to the escriptize provider as well as the tests, most of these are necessary changes. A few additional changes have heen made, such as adding the users $HOME/.local, to the default AtomVV install search paths, and supporting matching on the AtomVM elf executable, as well as the shell launcher script. Signed-off-by: Winford --- .github/workflows/build-and-test.yaml | 54 ++++++++++++++++++--------- src/atomvm_escriptize_provider.erl | 37 +++++++++++++----- src/atomvm_packbeam_provider.erl | 3 +- test/driver/src/escriptize_tests.erl | 4 +- test/driver/src/test.erl | 6 +-- 5 files changed, 71 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 30b3484..664dc6a 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -10,7 +10,7 @@ on: [push, pull_request] jobs: build-and-test: - runs-on: "ubuntu-22.04" + runs-on: "ubuntu-24.04" strategy: matrix: otp: ["25", "26", "27", "28"] @@ -18,24 +18,18 @@ jobs: include: - otp: "25" make_jobs: "compile etest" + rebar3_version: "3.24.0 " - otp: "26" make_jobs: "all" + rebar3_version: "3.25.1" - otp: "27" make_jobs: "all" + rebar3_version: "3.25.1" - otp: "28" make_jobs: "all" + rebar3_version: "3.25.1" steps: - # Setup - - name: "Checkout repo" - uses: actions/checkout@v2 - with: - submodules: 'recursive' - - - uses: erlef/setup-beam@v1 - with: - otp-version: ${{ matrix.otp }} - # Builder info - name: "System info" run: | @@ -44,17 +38,43 @@ jobs: echo "**OTP version:**" cat $(dirname $(which erlc))/../releases/RELEASES || true + # Setup OTP + - name: "Setup OTP" + uses: erlef/setup-beam@v1 + with: + otp-version: ${{ matrix.otp }} + rebar3-version: ${{ matrix.rebar3_version }} + + # Checkout AtomVM + - name: "Checkout AtomVM" + uses: actions/checkout@v5 + with: + repository: 'atomvm/AtomVM' + path: 'atomvm' + submodules: 'recursive' + - name: "Install deps" run: | - sudo apt install -y make git + sudo apt install -y make git gcc-14 g++-14 cmake gperf zlib1g-dev libmbedtls-dev ninja-build - - name: "Build rebar3" + - name: "Install AtomVM" + working-directory: 'atomvm' run: | - cd /tmp - git clone https://github.com/erlang/rebar3.git - cd rebar3 - ./bootstrap + mkdir build + cd build + cmake -DAVM_BUILD_RUNTIME_ONLY=ON -G Ninja .. + ninja + sudo ninja install + atomvm -v + + # Setup + - name: "Checkout repo" + uses: actions/checkout@v5 + with: + path: 'atomvm_packbeam' + submodules: 'recursive' # Build - name: "Make" + working-directory: 'atomvm_packbeam' run: PATH="/tmp/rebar3:${PATH}" make ${{ matrix.make_jobs }} diff --git a/src/atomvm_escriptize_provider.erl b/src/atomvm_escriptize_provider.erl index fab74ad..844ba37 100644 --- a/src/atomvm_escriptize_provider.erl +++ b/src/atomvm_escriptize_provider.erl @@ -29,8 +29,8 @@ -define(PROVIDER, escriptize). -define(DEPS, [packbeam]). -define(OPTS, [ - {atomvm_binary, $b, "atomvm_binary", string, "Path to AtomVM binary (default: which AtomVM)"}, - {atomvmlib, $l, "atomvmlib", string, "Path to atomvmlib.avm"}, + {atomvm_binary, $b, "atomvm_binary", string, "Path to AtomVM binary (default: which atomvm)"}, + {atomvmlib, $l, "atomvmlib", string, "Path to atomvmlib.avm (default: auto discovered)"}, {output, $o, "output", string, "Output executable name (default: app name)"}, {objcopy, $c, "objcopy", string, "Path to objcopy tool (auto-detected if not specified)"}, {start, $s, "start", atom, "Start module (default: app name)"} @@ -115,10 +115,11 @@ do(State) -> catch C:E:S -> rebar_api:error( - "An error occurred in the ~p task. Class=~p Error=~p Stacktrace=~p~n", [ - ?PROVIDER, C, E, S + "An error occurred in the ~p task. Class=~p Error=~p~n", [ + ?PROVIDER, C, E ] ), + rebar_api:debug("===== Stacktrace ==~n~p~n", [S]), {error, E} end. @@ -156,7 +157,7 @@ get_atomvmlib_path(Opts) -> %% @private find_atomvmlib() -> % First try to infer from AtomVM wrapper script - case os:find_executable("AtomVM") of + case os:find_executable("atomvm") of false -> find_atomvmlib_fallback(); WrapperPath -> @@ -173,6 +174,7 @@ find_atomvmlib_fallback() -> "/opt/local/lib/atomvm/atomvmlib.avm", "/usr/local/lib/atomvm/atomvmlib.avm", "/usr/lib/atomvm/atomvmlib.avm", + filename:join([os:getenv("HOME", "/.local"), "lib", "atomvm", "atomvmlib.avm"]), filename:join([os:getenv("HOME", "/tmp"), ".atomvm", "lib", "atomvmlib.avm"]) ], case lists:filter(fun filelib:is_file/1, PossiblePaths) of @@ -210,9 +212,18 @@ get_atomvm_binary(Opts) -> %% @private find_atomvm_binary() -> - case os:find_executable("AtomVM") of + case os:find_executable("atomvm") of false -> - {error, "AtomVM binary not found in PATH. Please specify with --atomvm_binary option"}; + case os:find_executable("AtomVM") of + Path -> + case resolve_atomvm_binary(Path) of + {ok, BinaryPath} -> + {ok, BinaryPath}; + _ -> + {error, + "AtomVM binary not found in PATH. Please specify with --atomvm_binary option"} + end + end; Path -> % Check if it's a shell script wrapper and find the actual binary case resolve_atomvm_binary(Path) of @@ -227,10 +238,10 @@ resolve_atomvm_binary(Path) -> % Try to read the file to see if it's a shell script case file:read_file(Path) of {ok, Content} -> - case binary:match(Content, <<"#!/bin/sh">>) of - {0, _} -> + case binary:part(Content, 0, 9) of + <<"#!/bin/sh">> -> % It's a shell script, parse it to find the actual binary - % The standard wrapper is at /prefix/bin/AtomVM + % The standard wrapper is at /prefix/bin/atomvm % The actual binary is at /prefix/lib/atomvm/AtomVM Dir = filename:dirname(Path), Prefix = filename:dirname(Dir), @@ -239,6 +250,12 @@ resolve_atomvm_binary(Path) -> true -> {ok, ActualBinary}; false -> {error, not_found} end; + <<_:8, $e, $l, $f, _:40>> -> + % It's the actual binary + case filelib:is_file(Path) of + true -> {ok, Path}; + false -> {error, not_found} + end; _ -> {error, not_a_script} end; diff --git a/src/atomvm_packbeam_provider.erl b/src/atomvm_packbeam_provider.erl index 5fd2280..c796158 100644 --- a/src/atomvm_packbeam_provider.erl +++ b/src/atomvm_packbeam_provider.erl @@ -137,7 +137,8 @@ get_start_module(State, Opts) -> EscriptName -> EscriptName end; - StartModule -> StartModule + StartModule -> + StartModule end. -spec format_error(any()) -> iolist(). diff --git a/test/driver/src/escriptize_tests.erl b/test/driver/src/escriptize_tests.erl index 713a305..2833f36 100644 --- a/test/driver/src/escriptize_tests.erl +++ b/test/driver/src/escriptize_tests.erl @@ -35,7 +35,7 @@ test_defaults(Opts) -> test:debug(Output, Opts), ok = test:expect_contains("Created packed AVM:", Output), - ok = test:expect_contains("_build/default/lib/myscript_packed.avm", Output), + ok = test:expect_contains("_build/default/lib/myscript.avm", Output), ok = test:expect_contains("with start module myscript", Output), ok = test:expect_contains("Created standalone executable:", Output), @@ -44,7 +44,7 @@ test_defaults(Opts) -> ExecPath = test:make_path([AppDir, "_build/default/bin/myscript"]), ok = test:file_exists(ExecPath), - [] = test:execute_cmd(ExecPath), + [] = test:execute_cmd(ExecPath, Opts), test:tick(). diff --git a/test/driver/src/test.erl b/test/driver/src/test.erl index 8a388a3..22d8845 100644 --- a/test/driver/src/test.erl +++ b/test/driver/src/test.erl @@ -128,10 +128,10 @@ make_opts(Opts) -> ). execute_cmd(Cmd) -> - execute_cmd(Cmd, false). + execute_cmd(Cmd, #{}). execute_cmd(Cmd, Opts) -> - case maps:get(verbose, Opts) orelse maps:get(debug, Opts) of + case maps:get(verbose, Opts, false) orelse maps:get(debug, Opts, false) of true -> io:format("#executing> ~s~n", [Cmd]); _ -> @@ -141,7 +141,7 @@ execute_cmd(Cmd, Opts) -> os:cmd(Cmd). debug(Msg, Opts) -> - case maps:get(debug, Opts) of + case maps:get(debug, Opts, false) of true -> io:format("~s~n", [Msg]); _ -> From 15b3e59e461b2e7c2b8af2ed2721da906e022a1b Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Tue, 9 Dec 2025 08:18:41 +0100 Subject: [PATCH 3/4] Detect executable with file(1) Signed-off-by: Paul Guyot --- .github/workflows/build-and-test.yaml | 50 +++++++++++++++++++------- src/atomvm_escriptize_provider.erl | 51 ++++++++++++--------------- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 664dc6a..ff420cd 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -10,15 +10,17 @@ on: [push, pull_request] jobs: build-and-test: - runs-on: "ubuntu-24.04" + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: otp: ["25", "26", "27", "28"] + os: ["ubuntu-24.04", "ubuntu-24.04-arm", "macos-26", "macos-15-intel"] include: - otp: "25" make_jobs: "compile etest" - rebar3_version: "3.24.0 " + rebar3_version: "3.24.0" - otp: "26" make_jobs: "all" rebar3_version: "3.25.1" @@ -31,20 +33,46 @@ jobs: steps: # Builder info - - name: "System info" + - name: "Install deps" + if: startsWith(matrix.os, 'macos-') && matrix.otp != '25' run: | - echo "**uname:**" - uname -a - echo "**OTP version:**" - cat $(dirname $(which erlc))/../releases/RELEASES || true + brew update && HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install gperf ninja llvm erlang@${{ matrix.otp }} rebar3 + echo PATH="/usr/local/opt/llvm/bin:/opt/homebrew/opt/llvm/bin:$PATH" >> $GITHUB_ENV + + - name: "Install deps" + if: startsWith(matrix.os, 'macos-') && matrix.otp == '25' + run: | + brew update + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install gperf ninja llvm erlang@${{ matrix.otp }} + wget https://github.com/erlang/rebar3/releases/download/${{ matrix.rebar3_version }}/rebar3 + chmod +x rebar3 + for bin_dir in {/usr/local,/opt/homebrew}/opt/erlang@25/bin/ ; do + if [ -e ${bin_dir} ]; then + sudo cp rebar3 ${bin_dir} + fi + done + echo PATH="/usr/local/opt/llvm/bin:/opt/homebrew/opt/llvm/bin:/usr/local/opt/erlang@${{ matrix.otp }}/bin:/opt/homebrew/opt/erlang@${{ matrix.otp }}/bin:$PATH" >> $GITHUB_ENV + + - name: "Install deps" + if: startsWith(matrix.os, 'ubuntu-') + run: | + sudo apt install -y make git gcc-14 g++-14 cmake gperf zlib1g-dev libmbedtls-dev ninja-build # Setup OTP - name: "Setup OTP" + if: startsWith(matrix.os, 'ubuntu-') uses: erlef/setup-beam@v1 with: otp-version: ${{ matrix.otp }} rebar3-version: ${{ matrix.rebar3_version }} + - name: "System info" + run: | + echo "**uname:**" + uname -a + echo "**OTP version:**" + cat $(dirname $(which erlc))/../releases/RELEASES || true + # Checkout AtomVM - name: "Checkout AtomVM" uses: actions/checkout@v5 @@ -53,18 +81,14 @@ jobs: path: 'atomvm' submodules: 'recursive' - - name: "Install deps" - run: | - sudo apt install -y make git gcc-14 g++-14 cmake gperf zlib1g-dev libmbedtls-dev ninja-build - - name: "Install AtomVM" working-directory: 'atomvm' run: | mkdir build cd build cmake -DAVM_BUILD_RUNTIME_ONLY=ON -G Ninja .. - ninja - sudo ninja install + cmake --build . + sudo cmake --build . -t install atomvm -v # Setup diff --git a/src/atomvm_escriptize_provider.erl b/src/atomvm_escriptize_provider.erl index 844ba37..581d01e 100644 --- a/src/atomvm_escriptize_provider.erl +++ b/src/atomvm_escriptize_provider.erl @@ -235,32 +235,24 @@ find_atomvm_binary() -> %% @private resolve_atomvm_binary(Path) -> - % Try to read the file to see if it's a shell script - case file:read_file(Path) of - {ok, Content} -> - case binary:part(Content, 0, 9) of - <<"#!/bin/sh">> -> - % It's a shell script, parse it to find the actual binary - % The standard wrapper is at /prefix/bin/atomvm - % The actual binary is at /prefix/lib/atomvm/AtomVM - Dir = filename:dirname(Path), - Prefix = filename:dirname(Dir), - ActualBinary = filename:join([Prefix, "lib", "atomvm", "AtomVM"]), - case filelib:is_file(ActualBinary) of - true -> {ok, ActualBinary}; - false -> {error, not_found} - end; - <<_:8, $e, $l, $f, _:40>> -> - % It's the actual binary - case filelib:is_file(Path) of - true -> {ok, Path}; - false -> {error, not_found} - end; - _ -> - {error, not_a_script} + case string:trim(os:cmd("file -b --mime-type " ++ Path)) of + "text/x-shellscript" -> + % It's a shell script, suppose + % The standard wrapper is at /prefix/bin/atomvm + % The actual binary is at /prefix/lib/atomvm/AtomVM + Dir = filename:dirname(Path), + Prefix = filename:dirname(Dir), + ActualBinary = filename:join([Prefix, "lib", "atomvm", "AtomVM"]), + case filelib:is_file(ActualBinary) of + true -> {ok, ActualBinary}; + false -> {error, not_found} end; - {error, Reason} -> - {error, Reason} + "application/x-mach-binary" -> + {ok, Path}; + "application/x-elf" -> + {ok, Path}; + Other -> + {error, {unexpected, Other}} end. %% @private @@ -281,7 +273,7 @@ find_objcopy() -> Tools = case os:type() of {unix, darwin} -> - % On macOS, prefer llvm-objcopy and try MacPorts variants + % On macOS, prefer llvm-objcopy (Homebrew) and try MacPorts variants [ "llvm-objcopy", "llvm-objcopy-mp-21", @@ -296,8 +288,11 @@ find_objcopy() -> ["objcopy", "llvm-objcopy"] end, case find_first_executable(Tools) of - {ok, Path} -> {ok, Path}; - error -> {error, "No objcopy tool found. Please install llvm or binutils"} + {ok, Path} -> + {ok, Path}; + error -> + {error, + "No objcopy tool found. Please install llvm or binutils and ensure objcopy or llvm-objcopy is on the PATH"} end. %% @private From 95225c98675ce741a32318c05322832ce67ba12c Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Wed, 10 Dec 2025 08:51:11 +0100 Subject: [PATCH 4/4] Do not update flags of .atomvm_avm_info section This will prevent objcopy from dammaging the binary on linux/arm64 Signed-off-by: Paul Guyot --- src/atomvm_escriptize_provider.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/atomvm_escriptize_provider.erl b/src/atomvm_escriptize_provider.erl index 581d01e..94c0853 100644 --- a/src/atomvm_escriptize_provider.erl +++ b/src/atomvm_escriptize_provider.erl @@ -445,7 +445,7 @@ set_atomvm_avm_info(ObjCopyTool, Executable, SectionName, SectionSize) -> ObjCopyCmd = lists:flatten( io_lib:format( - "~s --update-section .atomvm_avm_info=~s --set-section-flags .atomvm_avm_info=alloc,load,readonly,data ~s", + "~s --update-section .atomvm_avm_info=~s ~s", [ObjCopyTool, AVMInfoTempFile, Executable] ) ),