diff --git a/doc/src/atomvm-tooling.md b/doc/src/atomvm-tooling.md index 3c2664037a..0fd7a171a3 100644 --- a/doc/src/atomvm-tooling.md +++ b/doc/src/atomvm-tooling.md @@ -71,6 +71,34 @@ Return value: ok For instructions about how to install AtomVM on the `generic_unix` platform, see the [Getting Started Guide](./getting-started-guide.md#getting-started-on-the-generic-unix-platform) +#### Building standalone binaries (macOS and Linux) + +Using [`atomvm_rebar3_plugin`](https://atomvm.github.io/atomvm_rebar3_plugin), it is possible to generate a standalone binary where your application is bundled with AtomVM virtual machine. The binary can be copied to another host and would work without any specific installation. Please note that AtomVM on generic unix currently depends on zlib and mbedtls as shared libraries. + +To proceed, you need to define the main module that should export a `main/1` function. This function is called with parameters like regular Erlang `escript` applications. This is done by adding these two lines to your `rebar.config` file: + +``` +{plugins, [atomvm_rebar3_plugin]}. +{atomvm_rebar3_plugin, [{packbeam, [{start, main_module}]}]}. +``` + +where `main_module` is the name of the module that exports `main/1`. Alternatively, `main_module` can be defined with `escript_name` or `escript_main_app` options. + +Then you can run: +```shell +$ rebar3 atomvm escriptize +... +===> Created packed AVM: ... main_module_packed.avm with start module main_module +===> Created standalone executable: ... main_module/_build/default/bin/main_module +``` + +It is possible to build this standalone binary with a specific build of AtomVM, for example including a particular `nif`. + +```{seealso} +See the [`atomvm_rebar3_plugin`](https://atomvm.github.io/atomvm_rebar3_plugin) page for more detailed instructions +about how to use the `escriptize` target. +``` + ### Flashing your application with `rebar3` The [`atomvm_rebar3_plugin`](https://atomvm.github.io/atomvm_rebar3_plugin) supports flash targets for various device types. These targets are described in more detail below. diff --git a/libs/estdlib/src/init.erl b/libs/estdlib/src/init.erl index 73a8031b32..c62a2e2090 100644 --- a/libs/estdlib/src/init.erl +++ b/libs/estdlib/src/init.erl @@ -39,7 +39,36 @@ %% @doc Entry point. %% @end %%----------------------------------------------------------------------------- --spec boot([binary() | atom()]) -> any(). +-spec boot([binary() | atom() | string()]) -> any(). +boot([<<"-s">>, escript, <<"--">>, _Filename | Args]) -> + % Escript mode: check for start beam and main/1 function before starting kernel + case atomvm:get_start_beam(escript) of + {ok, ModuleNameBinary} -> + % Remove .beam suffix if present (check last 5 bytes) + Size = byte_size(ModuleNameBinary), + ModuleName = + if + Size > 5 -> + case binary:part(ModuleNameBinary, Size - 5, 5) of + <<".beam">> -> binary:part(ModuleNameBinary, 0, Size - 5); + _ -> ModuleNameBinary + end; + true -> + ModuleNameBinary + end, + Module = binary_to_atom(ModuleName, utf8), + case erlang:function_exported(Module, main, 1) of + true -> + {ok, _KernelPid} = kernel:start(boot, []), + Module:main(Args); + false -> + io:format("Function ~s:main/1 is not exported~n", [Module]), + error + end; + _ -> + io:format("start_beam not found~n"), + error + end; boot([<<"-s">>, StartupModule]) when is_atom(StartupModule) -> % Until we have boot scripts, we just start kernel application. {ok, _KernelPid} = kernel:start(boot, []), diff --git a/src/libAtomVM/globalcontext.c b/src/libAtomVM/globalcontext.c index ad1a2d0287..3f317af6ed 100644 --- a/src/libAtomVM/globalcontext.c +++ b/src/libAtomVM/globalcontext.c @@ -29,6 +29,7 @@ #include "context.h" #include "defaultatoms.h" #include "erl_nif_priv.h" +#include "interop.h" #include "list.h" #include "mailbox.h" #include "posix_nifs.h" @@ -720,21 +721,62 @@ Module *globalcontext_get_module_by_index(GlobalContext *global, int index) return result; } -run_result_t globalcontext_run(GlobalContext *glb, Module *startup_module, FILE *out_f) +run_result_t globalcontext_run(GlobalContext *glb, Module *startup_module, FILE *out_f, int argc, char **argv) { Context *ctx = context_new(glb); ctx->leader = 1; Module *init_module = globalcontext_get_module(glb, INIT_ATOM_INDEX); if (IS_NULL_PTR(init_module)) { + if (IS_NULL_PTR(startup_module)) { + fprintf(stderr, "Unable to locate entrypoint.\n"); + return RUN_NO_ENTRY_POINT; + } context_execute_loop(ctx, startup_module, "start", 0); } else { - if (UNLIKELY(memory_ensure_free(ctx, term_binary_heap_size(2) + LIST_SIZE(2, 0)) != MEMORY_GC_OK)) { - fprintf(stderr, "Unable to allocate arguments.\n"); - return RUN_MEMORY_FAILURE; + // Build boot arguments based on whether we're in embedded mode + if (argc > 0 && argv != NULL) { + // Embedded mode: ["-s", escript, "--" | argv_strings] + // Calculate heap size needed + size_t heap_needed = term_binary_heap_size(2) + // "-s" + term_binary_heap_size(2) + // "--" + LIST_SIZE(3, 0); // list for ["-s", escript, "--"] + + // Calculate space for argv strings + for (int i = 0; i < argc; i++) { + size_t arg_len = strlen(argv[i]); + heap_needed += CONS_SIZE * arg_len + LIST_SIZE(1, 0); + } + + if (UNLIKELY(memory_ensure_free(ctx, heap_needed) != MEMORY_GC_OK)) { + fprintf(stderr, "Unable to allocate arguments.\n"); + return RUN_MEMORY_FAILURE; + } + + // Build the argv list in reverse: [argvn, ..., argv0, "--", escript, "-s"] + term args_list = term_nil(); + for (int i = argc - 1; i >= 0; i--) { + term arg = interop_chars_to_list(argv[i], strlen(argv[i]), &ctx->heap); + args_list = term_list_prepend(arg, args_list, &ctx->heap); + } + + term separator = term_from_literal_binary("--", strlen("--"), &ctx->heap, glb); + args_list = term_list_prepend(separator, args_list, &ctx->heap); + + term escript_atom = globalcontext_make_atom(glb, ATOM_STR("\x7", "escript")); + args_list = term_list_prepend(escript_atom, args_list, &ctx->heap); + + term s_opt = term_from_literal_binary("-s", strlen("-s"), &ctx->heap, glb); + ctx->x[0] = term_list_prepend(s_opt, args_list, &ctx->heap); + } else { + // Non-embedded mode: ["-s", startup_module] + if (UNLIKELY(memory_ensure_free(ctx, term_binary_heap_size(2) + LIST_SIZE(2, 0)) != MEMORY_GC_OK)) { + fprintf(stderr, "Unable to allocate arguments.\n"); + return RUN_MEMORY_FAILURE; + } + term s_opt = term_from_literal_binary("-s", strlen("-s"), &ctx->heap, glb); + term list = term_list_prepend(module_get_name(startup_module), term_nil(), &ctx->heap); + ctx->x[0] = term_list_prepend(s_opt, list, &ctx->heap); } - term s_opt = term_from_literal_binary("-s", strlen("-s"), &ctx->heap, glb); - term list = term_list_prepend(module_get_name(startup_module), term_nil(), &ctx->heap); - ctx->x[0] = term_list_prepend(s_opt, list, &ctx->heap); context_execute_loop(ctx, init_module, "boot", 1); } diff --git a/src/libAtomVM/globalcontext.h b/src/libAtomVM/globalcontext.h index 42a756f502..4c4263bc7d 100644 --- a/src/libAtomVM/globalcontext.h +++ b/src/libAtomVM/globalcontext.h @@ -95,6 +95,7 @@ typedef enum run_result_t RUN_SUCCESS = 0, RUN_MEMORY_FAILURE = 1, RUN_RESULT_NOT_OK = 2, + RUN_NO_ENTRY_POINT = 3, } run_result_t; struct GlobalContext @@ -530,9 +531,11 @@ Module *globalcontext_load_module_from_avm(GlobalContext *global, const char *mo * @param global the global context * @param start_module the start module * @param out_f file to print the result to, or NULL + * @param argc number of command-line arguments (0 for non-embedded mode) + * @param argv command-line arguments (NULL for non-embedded mode) * @returns RUN_SUCCESS or an error code */ -run_result_t globalcontext_run(GlobalContext *global, Module *start_module, FILE *out_f); +run_result_t globalcontext_run(GlobalContext *global, Module *start_module, FILE *out_f, int argc, char **argv); #ifndef __cplusplus static inline uint64_t globalcontext_get_ref_ticks(GlobalContext *global) diff --git a/src/platforms/emscripten/src/main.c b/src/platforms/emscripten/src/main.c index c8c00dc07a..756bfb43fa 100644 --- a/src/platforms/emscripten/src/main.c +++ b/src/platforms/emscripten/src/main.c @@ -96,7 +96,7 @@ static int start(void) return EXIT_FAILURE; } - run_result_t ret_value = globalcontext_run(global, main_module, stdout); + run_result_t ret_value = globalcontext_run(global, main_module, stdout, 0, NULL); int status; if (ret_value == RUN_SUCCESS) { diff --git a/src/platforms/esp32/main/main.c b/src/platforms/esp32/main/main.c index ca6d02e2f3..412e5b1fcf 100644 --- a/src/platforms/esp32/main/main.c +++ b/src/platforms/esp32/main/main.c @@ -123,7 +123,7 @@ void app_main() ESP_LOGI(TAG, "Starting %s...", startup_module_name); fprintf(stdout, "---\n"); - run_result_t result = globalcontext_run(glb, mod, stdout); + run_result_t result = globalcontext_run(glb, mod, stdout, 0, NULL); bool reboot_on_not_ok = #if defined(CONFIG_REBOOT_ON_NOT_OK) diff --git a/src/platforms/generic_unix/main.c b/src/platforms/generic_unix/main.c index 6908aa50f8..c6025d3386 100644 --- a/src/platforms/generic_unix/main.c +++ b/src/platforms/generic_unix/main.c @@ -26,6 +26,16 @@ #include #include +#ifdef __APPLE__ +#include +#include +#elif defined(__linux__) +#include +#include +#include +#include +#endif + #include "atom.h" #include "avm_version.h" #include "avmpack.h" @@ -40,6 +50,28 @@ #include "term.h" #include "utils.h" +#ifdef __linux__ +// On Linux (using ELF), the embedded avm binary is added as a section +// and the following symbols are patched. +uint64_t __atomvm_avm_offset __attribute__((section(".atomvm_avm_info"))); +uint64_t __atomvm_avm_length __attribute__((section(".atomvm_avm_info"))); +#endif + +/** + * @brief Destructor for embedded AVM pack data + * @details Embedded AVM data is part of the executable and should not be freed + */ +static void embedded_avm_pack_destructor(struct AVMPackData *obj, GlobalContext *global) +{ + UNUSED(global); + // Embedded data is part of the executable, so we only free the pack structure itself + free(obj); +} + +static const struct AVMPackInfo embedded_avm_pack_info = { + .destructor = embedded_avm_pack_destructor +}; + void print_help(const char *program_name) { printf( @@ -62,92 +94,178 @@ void print_help(const char *program_name) program_name, program_name); } -int main(int argc, char **argv) +/** + * @brief Try to extract embedded AVM data from the executable itself + * @param data pointer to store the embedded AVM data (if found) + * @param size pointer to store the size of the embedded AVM data + * @return true if embedded AVM data was found, false otherwise + */ +bool get_embedded_avm(const void **data, size_t *size) { - int c; - while ((c = getopt(argc, argv, "hv")) != -1) { - switch (c) { - case 'h': - print_help(argv[0]); - return EXIT_SUCCESS; - - case 'v': - printf(ATOMVM_VERSION "\n"); - return EXIT_SUCCESS; - - default: - break; +#ifdef __APPLE__ + // On macOS, look for the __ATOMVM,__avm_data section + unsigned long section_size = 0; + const void *section_data = getsectiondata(&_mh_execute_header, "__ATOMVM", "__avm_data", §ion_size); + + if (section_data && section_size > 0) { + *data = section_data; + *size = (size_t) section_size; + return true; + } +#elif defined(__linux__) + // On Linux, check if symbols were added by objcopy during escriptize + if (__atomvm_avm_offset != 0 && __atomvm_avm_length != 0) { + int fd = open("/proc/self/exe", O_RDONLY); + if (fd == -1) { + fprintf(stderr, "Cannot open /proc/self/exe (errno = %d)\n", (int) errno); + return false; } + long page_size = sysconf(_SC_PAGESIZE); + + // Round down the offset to the nearest page boundary + off_t page_aligned_offset = (__atomvm_avm_offset / page_size) * page_size; + + // Calculate the extra bytes needed to cover the target region + size_t extra = __atomvm_avm_offset - page_aligned_offset; + size_t map_length = __atomvm_avm_length + extra; + + void *map = mmap(NULL, map_length, PROT_READ, MAP_PRIVATE, fd, page_aligned_offset); + if (map == MAP_FAILED) { + fprintf(stderr, "Failed to mmap current executable (errno = %d)\n", (int) errno); + close(fd); + return false; + } + *data = (const void *) ((const char *) map + extra); + *size = __atomvm_avm_length; + return true; } +#else + // embedded avm not supported yet on this target (e.g. FreeBSD) + UNUSED(data); + UNUSED(size); +#endif + + return false; +} + +int main(int argc, char **argv) +{ + // Check for embedded AVM data first + const void *embedded_data = NULL; + size_t embedded_size = 0; + bool has_embedded_avm = get_embedded_avm(&embedded_data, &embedded_size); + + // Only process getopt if not in embedded mode + // In embedded mode, all arguments (including -h, -v) are passed to the script + if (!has_embedded_avm) { + int c; + while ((c = getopt(argc, argv, "hv")) != -1) { + switch (c) { + case 'h': + print_help(argv[0]); + return EXIT_SUCCESS; + + case 'v': + printf(ATOMVM_VERSION "\n"); + return EXIT_SUCCESS; + + default: + break; + } + } - if (argc < 2) { - printf("Syntax Error! Missing .beam or .avm files.\n"); - print_help(argv[0]); - return EXIT_FAILURE; + if (argc < 2) { + printf("Syntax Error! Missing .beam or .avm files.\n"); + print_help(argv[0]); + return EXIT_FAILURE; + } } GlobalContext *glb = globalcontext_new(); Module *startup_module = NULL; - for (int i = 1; i < argc; ++i) { - const char *ext = strrchr(argv[i], '.'); - if (ext && strcmp(ext, ".avm") == 0) { - struct AVMPackData *avmpack_data; - if (UNLIKELY(sys_open_avm_from_file(glb, argv[i], &avmpack_data) != AVM_OPEN_OK)) { - fprintf(stderr, "Failed opening %s.\n", argv[i]); - return EXIT_FAILURE; - } - synclist_append(&glb->avmpack_data, &avmpack_data->avmpack_head); - - if (IS_NULL_PTR(startup_module)) { - const void *startup_beam = NULL; - const char *startup_module_name; - uint32_t startup_beam_size; - avmpack_find_section_by_flag(avmpack_data->data, BEAM_START_FLAG, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name); - - if (startup_beam) { - avmpack_data->in_use = true; - startup_module = module_new_from_iff_binary(glb, startup_beam, startup_beam_size); - if (IS_NULL_PTR(startup_module)) { - fprintf(stderr, "Cannot load startup module: %s\n", startup_module_name); - return EXIT_FAILURE; + if (has_embedded_avm) { + struct AVMPackData *avmpack_data = malloc(sizeof(struct AVMPackData)); + if (!avmpack_data) { + fprintf(stderr, "Failed to allocate memory for embedded AVM pack.\n"); + globalcontext_destroy(glb); + return EXIT_FAILURE; + } + + avmpack_data_init(avmpack_data, &embedded_avm_pack_info); + avmpack_data->data = embedded_data; + // Set the name for the embedded AVM pack so it can be found by atomvm:get_start_beam/1 + term escript_atom = globalcontext_make_atom(glb, ATOM_STR("\x7", "escript")); + avmpack_data->name_atom_id = term_to_atom_index(escript_atom); + + synclist_append(&glb->avmpack_data, &avmpack_data->avmpack_head); + } + + // Process command-line AVM/BEAM files only if not in embedded mode + // In embedded mode, all remaining arguments are passed to the script + if (!has_embedded_avm) { + for (int i = 1; i < argc; ++i) { + const char *ext = strrchr(argv[i], '.'); + if (ext && strcmp(ext, ".avm") == 0) { + struct AVMPackData *avmpack_data; + if (UNLIKELY(sys_open_avm_from_file(glb, argv[i], &avmpack_data) != AVM_OPEN_OK)) { + fprintf(stderr, "Failed opening %s.\n", argv[i]); + return EXIT_FAILURE; + } + synclist_append(&glb->avmpack_data, &avmpack_data->avmpack_head); + + if (IS_NULL_PTR(startup_module)) { + const void *startup_beam = NULL; + const char *startup_module_name; + uint32_t startup_beam_size; + avmpack_find_section_by_flag(avmpack_data->data, BEAM_START_FLAG, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name); + + if (startup_beam) { + avmpack_data->in_use = true; + startup_module = module_new_from_iff_binary(glb, startup_beam, startup_beam_size); + if (IS_NULL_PTR(startup_module)) { + fprintf(stderr, "Cannot load startup module: %s\n", startup_module_name); + return EXIT_FAILURE; + } + globalcontext_insert_module(glb, startup_module); + startup_module->module_platform_data = NULL; } - globalcontext_insert_module(glb, startup_module); - startup_module->module_platform_data = NULL; } - } - } else if (ext && (strcmp(ext, ".beam") == 0)) { - MappedFile *mapped_file = mapped_file_open_beam(argv[i]); - if (!iff_is_valid_beam(mapped_file->mapped)) { - fprintf(stderr, "%s has invalid beam format.\n", argv[i]); - return EXIT_FAILURE; - } - Module *mod = module_new_from_iff_binary(glb, mapped_file->mapped, mapped_file->size); - if (IS_NULL_PTR(mod)) { - fprintf(stderr, "Cannot load module: %s\n", argv[i]); + } else if (ext && (strcmp(ext, ".beam") == 0)) { + MappedFile *mapped_file = mapped_file_open_beam(argv[i]); + if (!iff_is_valid_beam(mapped_file->mapped)) { + fprintf(stderr, "%s has invalid beam format.\n", argv[i]); + return EXIT_FAILURE; + } + Module *mod = module_new_from_iff_binary(glb, mapped_file->mapped, mapped_file->size); + if (IS_NULL_PTR(mod)) { + fprintf(stderr, "Cannot load module: %s\n", argv[i]); + return EXIT_FAILURE; + } + globalcontext_insert_module(glb, mod); + mod->module_platform_data = NULL; + if (IS_NULL_PTR(startup_module) && module_search_exported_function(mod, START_ATOM_INDEX, 0) != 0) { + startup_module = mod; + } + + } else { + fprintf(stderr, "%s is not an AVM or a BEAM file.\n", argv[i]); return EXIT_FAILURE; } - globalcontext_insert_module(glb, mod); - mod->module_platform_data = NULL; - if (IS_NULL_PTR(startup_module) && module_search_exported_function(mod, START_ATOM_INDEX, 0) != 0) { - startup_module = mod; - } - - } else { - fprintf(stderr, "%s is not an AVM or a BEAM file.\n", argv[i]); - return EXIT_FAILURE; } } - if (IS_NULL_PTR(startup_module)) { - fprintf(stderr, "Unable to locate entrypoint.\n"); - return EXIT_FAILURE; + // Pass command-line arguments to escript in embedded mode + run_result_t result; + if (has_embedded_avm) { + // Don't print return value in embedded mode (pass NULL instead of stderr) + result = globalcontext_run(glb, startup_module, NULL, argc, argv); + } else { + result = globalcontext_run(glb, startup_module, stderr, 0, NULL); } - run_result_t result = globalcontext_run(glb, startup_module, stderr); - int status; if (result == RUN_SUCCESS) { status = EXIT_SUCCESS; diff --git a/src/platforms/rp2/src/main.c b/src/platforms/rp2/src/main.c index 0733c85e63..71c4ce9c22 100644 --- a/src/platforms/rp2/src/main.c +++ b/src/platforms/rp2/src/main.c @@ -136,7 +136,7 @@ static int app_main() globalcontext_insert_module(glb, mod); mod->module_platform_data = NULL; - run_result_t result = globalcontext_run(glb, mod, stdout); + run_result_t result = globalcontext_run(glb, mod, stdout, 0, NULL); nif_collection_destroy_all(glb); globalcontext_destroy(glb); diff --git a/src/platforms/stm32/src/main.c b/src/platforms/stm32/src/main.c index 689aa925f1..cc2fe7009e 100644 --- a/src/platforms/stm32/src/main.c +++ b/src/platforms/stm32/src/main.c @@ -271,7 +271,7 @@ int main() AVM_LOGI(TAG, "Starting: %s...\n", startup_module_name); fprintf(stdout, "---\n"); - run_result_t result = globalcontext_run(glb, mod, stdout); + run_result_t result = globalcontext_run(glb, mod, stdout, 0, NULL); bool reboot_on_not_ok = #if defined(CONFIG_REBOOT_ON_NOT_OK)