src/packbeam.erl

%%
%% Copyright (c) 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.
%%

%%-----------------------------------------------------------------------------
%% @doc An escript and OTP library used to generate an
%% <a href="http://github.com/bettio/AtomVM">AtomVM</a> AVM file from a set of
%% files (beam files, previously built AVM files, or even arbitrary data files).
%% @end
%%-----------------------------------------------------------------------------
-module(packbeam).

%% API exports (for backwards compatibility)
-export([create/2, create/4, list/1, delete/3]).
%% escript
-export([main/1]).

%%
%% Public API
%%

%%-----------------------------------------------------------------------------
%% @doc     Deprecated.  Use the packbeam_api module, instead.
%% @end
%%-----------------------------------------------------------------------------
create(OutputPath, InputPaths) ->
    io:format("WARNING.  packbeam:create/2 is deprecated.  Use packbeam_api::create/2 instead.~n"),
    packbeam_api:create(OutputPath, InputPaths).

%%-----------------------------------------------------------------------------
%% @doc     Deprecated.  Use the packbeam_api module, instead.
%% @end
%%-----------------------------------------------------------------------------
create(OutputPath, InputPaths, Prune, StartModule) ->
    io:format("WARNING.  packbeam:create/4 is deprecated.  Use packbeam_api::create/3 instead.~n"),
    packbeam_api:create(OutputPath, InputPaths, Prune, StartModule).

%%-----------------------------------------------------------------------------
%% @doc     Deprecated.  Use the packbeam_api module, instead.
%% @end
%%-----------------------------------------------------------------------------
list(InputPath) ->
    io:format("WARNING.  packbeam:list/1 is deprecated.  Use packbeam_api::list/1 instead.~n"),
    packbeam_api:list(InputPath).

%%-----------------------------------------------------------------------------
%% @doc     Deprecated.  Use the packbeam_api module, instead.
%% @end
%%-----------------------------------------------------------------------------
delete(OutputPath, InputPath, Names) ->
    io:format("WARNING.  packbeam:delete/3 is deprecated.  Use packbeam_api::delete/3 instead.~n"),
    packbeam_api:delete(OutputPath, InputPath, Names).

%%
%% escript entrypoint
%%

%% @hidden
main(Argv) ->
    {Opts, Args} = parse_args(Argv),
    case length(Args) of
        0 ->
            print_help(),
            erlang:halt(255);
        _ ->
            [Command | ArgsRest] = Args,
            try
                case Command of
                    "create" ->
                        erlang:halt(do_create(Opts, ArgsRest));
                    "list" ->
                        erlang:halt(do_list(Opts, ArgsRest));
                    "extract" ->
                        erlang:halt(do_extract(Opts, ArgsRest));
                    "delete" ->
                        erlang:halt(do_delete(Opts, ArgsRest));
                    "version" ->
                        io:format("~s~n", [get_version()]),
                        erlang:halt(0);
                    "help" ->
                        print_help(),
                        erlang:halt(0);
                    _ ->
                        io:format("packbeam: command must be one of create|list|delete|help~n"),
                        print_help(),
                        erlang:halt(255)
                end
            catch
                _:Exception:S ->
                    io:format("packbeam: caught exception: ~p~n", [Exception]),
                    io:format("Stacktrace: ~n~p~n", [S]),
                    print_help(),
                    erlang:halt(255)
            end
    end.

%%
%% escript internal functions
%%

%% @private
print_help() ->
    io:format(
        "~n"
        "packbeam version ~s~n"
        "~n"
        "Syntax:~n"
        "    packbeam <sub-command> <options> <args>~n"
        "~n"
        "The following sub-commands are supported:~n"
        "~n"
        "    create <options> <output-avm-file> [<input-file>]+~n"
        "        where:~n"
        "           <output-avm-file> is the output AVM file,~n"
        "           [<input-file>]+ is a list of one or more input files,~n"
        "           and <options> are among the following:~n"
        "              [--prune|-p]           Prune dependencies~n"
        "              [--start|-s <module>]  Start module~n"
        "              [--remove_lines|-r]    Remove line number information from AVM files~n"
        "~n"
        "    list <options> <avm-file>~n"
        "        where:~n"
        "           <avm-file> is an AVM file,~n"
        "           and <options> are among the following:~n"
        "               [--format|-f csv|bare|default]  Format output~n"
        "~n"
        "    extract <options> <avm-file> [<element>]*~n"
        "        where:~n"
        "           <avm-file> is an AVM file,~n"
        "           [<element>]+ is a list of one or more elements to extract~n"
        "               (if empty, then extract all elements)~n"
        "           and <options> are among the following:~n"
        "               [--out|-o <output-directory>]   Output directory into which to write elements~n"
        "               (if unspecified, use the current working directory)~n"
        "~n"
        "    delete <options> <avm-file> [<element>]+~n"
        "        where:~n"
        "           <avm-file> is an AVM file,~n"
        "           [<element>]+ is a list of one or more elements to delete,~n"
        "           and <options> are among the following:~n"
        "               [--out|-o <output-avm-file>]    Output AVM file~n"
        "~n"
        "    version~n"
        "        Print version and exit~n"
        "~n"
        "    help~n"
        "        Print this help~n"
        "~n",
        [get_version()]
    ).

%% @private
get_version() ->
    case application:load(atomvm_packbeam) of
        ok ->
            case lists:keyfind(atomvm_packbeam, 1, application:loaded_applications()) of
                {_, _, Version} ->
                    Version;
                false ->
                    "Error!  Unable to find atomvm_packbeam in loaded applications"
            end;
        {error, _Reason} ->
            "Error!  Unable to load atomvm_packbeam application"
    end.

%% @private
do_create(Opts, Args) ->
    validate_args(create, Opts, Args),
    [OutputFile | InputFiles] = Args,
    ok = packbeam_api:create(
        OutputFile, InputFiles,
        #{
            prune => maps:get(prune, Opts, false),
            start => maps:get(start, Opts, undefined),
            include_lines => not maps:get(remove_lines, Opts, false)
        }
    ),
    0.

%% @private
do_list(Opts, Args) ->
    validate_args(list, Opts, Args),
    [InputFile | _] = Args,
    Modules = packbeam_api:list(InputFile),
    print_modules(Modules, maps:get(format, Opts, undefined)),
    0.

%% @private
do_extract(Opts, Args) ->
    validate_args(extract, Opts, Args),
    [InputFile | Rest] = Args,
    OutputDir = maps:get(output, Opts, "."),
    ok = packbeam_api:extract(InputFile, Rest, OutputDir),
    0.

%% @private
do_delete(Opts, Args) ->
    validate_args(delete, Opts, Args),
    [InputFile | _] = Args,
    OutputFile = maps:get(output, Opts, InputFile),
    packbeam_api:delete(OutputFile, InputFile, Args),
    0.

%% @private
validate_args(create, _Opts, [OutputPath | _Rest] = _Args) ->
    case filelib:is_dir(OutputPath) of
        true ->
            throw(io_lib:format("Output file (~p) is a directory", [OutputPath]));
        _ ->
            ok
    end;
validate_args(create, _Opts, [] = _Args) ->
    throw("Missing output file option");
%%
validate_args(list, _Opts, [InputPath | _Rest] = _Args) ->
    case not filelib:is_file(InputPath) of
        true ->
            throw(io_lib:format("Input file (~p) does not exist", [InputPath]));
        _ ->
            ok
    end;
validate_args(list, _Opts, [] = _Args) ->
    throw("Missing input option");
%%
validate_args(extract, _Opts, [InputPath | _Rest] = _Args) ->
    case not filelib:is_file(InputPath) of
        true ->
            throw(io_lib:format("Input file (~p) does not exist", [InputPath]));
        _ ->
            ok
    end;
validate_args(extract, _Opts, [] = _Args) ->
    throw("Missing input option");
%%
validate_args(delete, _Opts, [InputPath | _Rest] = _Args) ->
    case not filelib:is_file(InputPath) of
        true ->
            throw(io_lib:format("Input file (~p) does not exist", [InputPath]));
        _ ->
            ok
    end;
validate_args(delete, _Opts, [] = _Args) ->
    throw("Missing input option").

%% @private
print_modules(Modules, "csv" = Format) ->
    io:format("MODULE_NAME,IS_BEAM,IS_ENTRYPOINT,SIZE_BYTES~n"),
    lists:foreach(
        fun(Module) -> print_module(Module, Format) end,
        Modules
    );
print_modules(Modules, Format) ->
    lists:foreach(
        fun(Module) -> print_module(Module, Format) end,
        Modules
    ).

%% @private
print_module(ParsedFile, undefined) ->
    print_module(ParsedFile, "default");
print_module(ParsedFile, "default") ->
    Name = packbeam_api:get_element_name(ParsedFile),
    Data = packbeam_api:get_element_data(ParsedFile),
    io:format(
        "~s~s [~p]~n", [
            Name,
            case packbeam_api:is_entrypoint(ParsedFile) of
                true -> " *";
                _ -> ""
            end,
            byte_size(Data)
        ]
    );
print_module(ParsedFile, "csv") ->
    Name = packbeam_api:get_element_name(ParsedFile),
    Data = packbeam_api:get_element_data(ParsedFile),
    io:format(
        "~s,~p,~p,~p~n", [
            Name,
            packbeam_api:is_beam(ParsedFile),
            packbeam_api:is_entrypoint(ParsedFile),
            byte_size(Data)
        ]
    );
print_module(ParsedFile, "bare") ->
    Name = packbeam_api:get_element_name(ParsedFile),
    io:format(
        "~s~n", [
            Name
        ]
    );
print_module(_ParsedFile, Format) ->
    throw({error, {unsupported_format, Format}}).

%% @private
parse_args(Argv) ->
    parse_args(Argv, {#{}, []}).

%% @private
parse_args([], {Opts, Args}) ->
    {Opts, lists:reverse(Args)};

parse_args(["-out", Path | T], {Opts, Args}) ->
    io:format("WARNING.  Deprecated option.  Use --out instead.~n"),
    parse_args(["--out", Path | T], {Opts, Args});
parse_args(["-o", Path | T], {Opts, Args}) ->
    parse_args(["--out", Path | T], {Opts, Args});
parse_args(["--out", Path | T], {Opts, Args}) ->
    parse_args(T, {Opts#{output => Path}, Args});

parse_args(["-prune" | T], {Opts, Args}) ->
    io:format("WARNING.  Deprecated option.  Use --prune instead.~n"),
    parse_args(["--prune" | T], {Opts, Args});
parse_args(["-p" | T], {Opts, Args}) ->
    parse_args(["--prune" | T], {Opts, Args});
parse_args(["--prune" | T], {Opts, Args}) ->
    parse_args(T, {Opts#{prune => true}, Args});

parse_args(["-start", Module | T], {Opts, Args}) ->
    io:format("WARNING.  Deprecated option.  Use --start instead.~n"),
    parse_args(["--start", Module | T], {Opts, Args});
parse_args(["-s", Module | T], {Opts, Args}) ->
    parse_args(["--start", Module | T], {Opts, Args});
parse_args(["--start", Module | T], {Opts, Args}) ->
    parse_args(T, {Opts#{start_module => list_to_atom(Module)}, Args});

parse_args(["-r" | T], {Opts, Args}) ->
    parse_args(["--remove_lines" | T], {Opts, Args});
parse_args(["--remove_lines" | T], {Opts, Args}) ->
    parse_args(T, {Opts#{remove_lines => true}, Args});

parse_args(["-format", Format | T], {Opts, Args}) ->
    io:format("WARNING.  Deprecated option.  Use --format instead.~n"),
    parse_args(["--format", Format | T], {Opts, Args});
parse_args(["-f", Format | T], {Opts, Args}) ->
    parse_args(["--format", Format | T], {Opts, Args});
parse_args(["--format", Format | T], {Opts, Args}) ->
    parse_args(T, {Opts#{format => Format}, Args});
parse_args([H | T], {Opts, Args}) ->
    parse_args(T, {Opts, [H | Args]}).