From da31bd3575b7869f9e00d70fd97cb3370055f9a7 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 1 Nov 2025 09:18:10 +0100 Subject: [PATCH] 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 1ecb04a..55cde74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,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) ->