diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/getopt.erl | 621 | ||||
| -rw-r--r-- | src/rebar.erl | 157 | ||||
| -rw-r--r-- | src/rebar_app_utils.erl | 9 | ||||
| -rw-r--r-- | src/rebar_base_compiler.erl | 44 | ||||
| -rw-r--r-- | src/rebar_config.erl | 5 | ||||
| -rw-r--r-- | src/rebar_core.erl | 69 | ||||
| -rw-r--r-- | src/rebar_ct.erl | 61 | ||||
| -rw-r--r-- | src/rebar_deps.erl | 237 | ||||
| -rw-r--r-- | src/rebar_dia_compiler.erl | 4 | ||||
| -rw-r--r-- | src/rebar_erlc_compiler.erl | 483 | ||||
| -rw-r--r-- | src/rebar_erlydtl_compiler.erl | 63 | ||||
| -rw-r--r-- | src/rebar_eunit.erl | 88 | ||||
| -rw-r--r-- | src/rebar_getopt.erl | 914 | ||||
| -rw-r--r-- | src/rebar_lfe_compiler.erl | 4 | ||||
| -rw-r--r-- | src/rebar_log.erl | 37 | ||||
| -rw-r--r-- | src/rebar_mustache.erl (renamed from src/mustache.erl) | 10 | ||||
| -rw-r--r-- | src/rebar_neotoma_compiler.erl | 12 | ||||
| -rw-r--r-- | src/rebar_otp_app.erl | 15 | ||||
| -rw-r--r-- | src/rebar_qc.erl | 7 | ||||
| -rw-r--r-- | src/rebar_reltool.erl | 26 | ||||
| -rw-r--r-- | src/rebar_templater.erl | 34 | ||||
| -rw-r--r-- | src/rebar_upgrade.erl | 33 | ||||
| -rw-r--r-- | src/rebar_utils.erl | 25 | ||||
| -rw-r--r-- | src/rebar_xref.erl | 193 | 
24 files changed, 2057 insertions, 1094 deletions
| diff --git a/src/getopt.erl b/src/getopt.erl deleted file mode 100644 index 175b7a5..0000000 --- a/src/getopt.erl +++ /dev/null @@ -1,621 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author Juan Jose Comellas <juanjo@comellas.org> -%%% @copyright (C) 2009 Juan Jose Comellas -%%% @doc Parses command line options with a format similar to that of GNU getopt. -%%% @end -%%% -%%% This source file is subject to the New BSD License. You should have received -%%% a copy of the New BSD license with this software. If not, it can be -%%% retrieved from: http://www.opensource.org/licenses/bsd-license.php -%%%------------------------------------------------------------------- --module(getopt). --author('juanjo@comellas.org'). - --export([parse/2, usage/2, usage/3, usage/4]). - --export_type([arg_type/0, -	      arg_value/0, -	      arg_spec/0, -	      simple_option/0, -	      compound_option/0, -	      option/0, -	      option_spec/0]). - --define(TAB_LENGTH, 8). -%% Indentation of the help messages in number of tabs. --define(INDENTATION, 3). - -%% Position of each field in the option specification tuple. --define(OPT_NAME, 1). --define(OPT_SHORT, 2). --define(OPT_LONG, 3). --define(OPT_ARG, 4). --define(OPT_HELP, 5). - --define(IS_OPT_SPEC(Opt), (tuple_size(Opt) =:= ?OPT_HELP)). - - -%% Atom indicating the data type that an argument can be converted to. --type arg_type() :: 'atom' | 'binary' | 'boolean' | 'float' | 'integer' | 'string'. -%% Data type that an argument can be converted to. --type arg_value() :: atom() | binary() | boolean() | float() | integer() | string(). -%% Argument specification. --type arg_spec() :: arg_type() | {arg_type(), arg_value()} | undefined. -%% Option type and optional default argument. --type simple_option() :: atom(). --type compound_option() :: {atom(), arg_value()}. --type option() :: simple_option() | compound_option(). -%% Command line option specification. --type option_spec() :: { -                   Name    :: atom(), -                   Short   :: char() | undefined, -                   Long    :: string() | undefined, -                   ArgSpec :: arg_spec(), -                   Help    :: string() | undefined -                  }. -%% Output streams --type output_stream() :: 'standard_io' | 'standard_error'. - - -%% @doc  Parse the command line options and arguments returning a list of tuples -%%       and/or atoms using the Erlang convention for sending options to a -%%       function. --spec parse([option_spec()], string() | [string()]) -> -    {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: any()}}. -parse(OptSpecList, CmdLine) -> -    try -        Args = if -                   is_integer(hd(CmdLine)) -> -                       string:tokens(CmdLine, " \t\n"); -                   true -> -                       CmdLine -               end, -        parse(OptSpecList, [], [], 0, Args) -    catch -        throw: {error, {_Reason, _Data}} = Error -> -            Error -    end. - - --spec parse([option_spec()], [option()], [string()], integer(), [string()]) -> -    {ok, {[option()], [string()]}}. -%% Process the option terminator. -parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, ["--" | Tail]) -> -    %% Any argument present after the terminator is not considered an option. -    {ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc, Tail)}}; -%% Process long options. -parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["--" ++ OptArg = OptStr | Tail]) -> -    parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg); -%% Process short options. -parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["-" ++ ([_Char | _] = OptArg) = OptStr | Tail]) -> -    parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg); -%% Process non-option arguments. -parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail]) -> -    case find_non_option_arg(OptSpecList, ArgPos) of -        {value, OptSpec} when ?IS_OPT_SPEC(OptSpec) -> -            parse(OptSpecList, add_option_with_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos + 1, Tail); -        false -> -            parse(OptSpecList, OptAcc, [Arg | ArgAcc], ArgPos, Tail) -    end; -parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, []) -> -    %% Once we have completed gathering the options we add the ones that were -    %% not present but had default arguments in the specification. -    {ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc)}}. - - -%% @doc Parse a long option, add it to the option accumulator and continue -%%      parsing the rest of the arguments recursively. -%%      A long option can have the following syntax: -%%        --foo      Single option 'foo', no argument -%%        --foo=bar  Single option 'foo', argument "bar" -%%        --foo bar  Single option 'foo', argument "bar" --spec parse_long_option([option_spec()], [option()], [string()], integer(), [string()], string(), string()) -> -          {ok, {[option()], [string()]}}. -parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) -> -    case split_assigned_arg(OptArg) of -        {Long, Arg} -> -            %% Get option that has its argument within the same string -            %% separated by an equal ('=') character (e.g. "--port=1000"). -            parse_long_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg); - -        Long -> -            case lists:keyfind(Long, ?OPT_LONG, OptSpecList) of -                {Name, _Short, Long, undefined, _Help} -> -                    parse(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args); - -                {_Name, _Short, Long, _ArgSpec, _Help} = OptSpec -> -                    %% The option argument string is empty, but the option requires -                    %% an argument, so we look into the next string in the list. -                    %% e.g ["--port", "1000"] -                    parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec); -                false -> -                    throw({error, {invalid_option, OptStr}}) -            end -    end. - - -%% @doc Parse an option where the argument is 'assigned' in the same string using -%%      the '=' character, add it to the option accumulator and continue parsing the -%%      rest of the arguments recursively. This syntax is only valid for long options. --spec parse_long_option_assigned_arg([option_spec()], [option()], [string()], integer(), -                                     [string()], string(), string(), string()) -> -                                            {ok, {[option()], [string()]}}. -parse_long_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg) -> -    case lists:keyfind(Long, ?OPT_LONG, OptSpecList) of -        {_Name, _Short, Long, ArgSpec, _Help} = OptSpec -> -            case ArgSpec of -                undefined -> -                    throw({error, {invalid_option_arg, OptStr}}); -                _ -> -                    parse(OptSpecList, add_option_with_assigned_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args) -            end; -        false -> -            throw({error, {invalid_option, OptStr}}) -    end. - - -%% @doc Split an option string that may contain an option with its argument -%%      separated by an equal ('=') character (e.g. "port=1000"). --spec split_assigned_arg(string()) -> {Name :: string(), Arg :: string()} | string(). -split_assigned_arg(OptStr) -> -    split_assigned_arg(OptStr, OptStr, []). - -split_assigned_arg(_OptStr, "=" ++ Tail, Acc) -> -    {lists:reverse(Acc), Tail}; -split_assigned_arg(OptStr, [Char | Tail], Acc) -> -    split_assigned_arg(OptStr, Tail, [Char | Acc]); -split_assigned_arg(OptStr, [], _Acc) -> -    OptStr. - - -%% @doc Retrieve the argument for an option from the next string in the list of -%%      command-line parameters or set the value of the argument from the argument -%%      specification (for boolean and integer arguments), if possible. -parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec) -> -    ArgSpecType = arg_spec_type(ArgSpec), -    case Args =:= [] orelse is_implicit_arg(ArgSpecType, hd(Args)) of -        true -> -            parse(OptSpecList, add_option_with_implicit_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args); -        false -> -            [Arg | Tail] = Args, -            try -                parse(OptSpecList, [{Name, to_type(ArgSpecType, Arg)} | OptAcc], ArgAcc, ArgPos, Tail) -            catch -                error:_ -> -                    throw({error, {invalid_option_arg, {Name, Arg}}}) -            end -    end. - - -%% @doc Parse a short option, add it to the option accumulator and continue -%%      parsing the rest of the arguments recursively. -%%      A short option can have the following syntax: -%%        -a       Single option 'a', no argument or implicit boolean argument -%%        -a foo   Single option 'a', argument "foo" -%%        -afoo    Single option 'a', argument "foo" -%%        -abc     Multiple options: 'a'; 'b'; 'c' -%%        -bcafoo  Multiple options: 'b'; 'c'; 'a' with argument "foo" -%%        -aaa     Multiple repetitions of option 'a' (only valid for options with integer arguments) --spec parse_short_option([option_spec()], [option()], [string()], integer(), [string()], string(), string()) -> -    {ok, {[option()], [string()]}}. -parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) -> -    parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, first, OptArg). - -parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptPos, [Short | Arg]) -> -    case lists:keyfind(Short, ?OPT_SHORT, OptSpecList) of -        {Name, Short, _Long, undefined, _Help} -> -            parse_short_option(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args, OptStr, first, Arg); - -        {_Name, Short, _Long, ArgSpec, _Help} = OptSpec -> -            %% The option has a specification, so it requires an argument. -            case Arg of -                [] -> -                    %% The option argument string is empty, but the option requires -                    %% an argument, so we look into the next string in the list. -                    parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec, OptPos); - -                _ -> -                    case is_valid_arg(ArgSpec, Arg) of -                        true -> -                            parse(OptSpecList, add_option_with_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args); -                        _ -> -                            NewOptAcc = case OptPos of -                                            first -> add_option_with_implicit_arg(OptSpec, OptAcc); -                                            _     -> add_option_with_implicit_incrementable_arg(OptSpec, OptAcc) -                                        end, -                            parse_short_option(OptSpecList, NewOptAcc, ArgAcc, ArgPos, Args, OptStr, next, Arg) -                    end -            end; - -        false -> -            throw({error, {invalid_option, OptStr}}) -    end; -parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, _OptStr, _OptPos, []) -> -    parse(OptSpecList, OptAcc, ArgAcc, ArgPos, Args). - - -%% @doc Retrieve the argument for an option from the next string in the list of -%%      command-line parameters or set the value of the argument from the argument -%%      specification (for boolean and integer arguments), if possible. -parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec, OptPos) -> -    case Args =:= [] orelse is_implicit_arg(ArgSpec, hd(Args)) of -        true when OptPos =:= first -> -            parse(OptSpecList, add_option_with_implicit_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args); -        true -> -            parse(OptSpecList, add_option_with_implicit_incrementable_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args); -        false -> -            [Arg | Tail] = Args, -            try -                parse(OptSpecList, [{Name, to_type(ArgSpec, Arg)} | OptAcc], ArgAcc, ArgPos, Tail) -            catch -                error:_ -> -                    throw({error, {invalid_option_arg, {Name, Arg}}}) -            end -    end. - - -%% @doc Find the option for the discrete argument in position specified in the -%%      Pos argument. --spec find_non_option_arg([option_spec()], integer()) -> {value, option_spec()} | false. -find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} = OptSpec | _Tail], 0) -> -     {value, OptSpec}; -find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} | Tail], Pos) -> -    find_non_option_arg(Tail, Pos - 1); -find_non_option_arg([_Head | Tail], Pos) -> -    find_non_option_arg(Tail, Pos); -find_non_option_arg([], _Pos) -> -    false. - - -%% @doc Append options that were not present in the command line arguments with -%%      their default arguments. --spec append_default_options([option_spec()], [option()]) -> [option()]. -append_default_options([{Name, _Short, _Long, {_Type, DefaultArg}, _Help} | Tail], OptAcc) -> -    append_default_options(Tail, -               case lists:keymember(Name, 1, OptAcc) of -                   false -> -                       [{Name, DefaultArg} | OptAcc]; -                   _ -> -                       OptAcc -               end); -%% For options with no default argument. -append_default_options([_Head | Tail], OptAcc) -> -    append_default_options(Tail, OptAcc); -append_default_options([], OptAcc) -> -    OptAcc. - - -%% @doc Add an option with argument converting it to the data type indicated by the -%%      argument specification. --spec add_option_with_arg(option_spec(), string(), [option()]) -> [option()]. -add_option_with_arg({Name, _Short, _Long, ArgSpec, _Help} = OptSpec, Arg, OptAcc) -> -    case is_valid_arg(ArgSpec, Arg) of -        true -> -            try -                [{Name, to_type(ArgSpec, Arg)} | OptAcc] -            catch -                error:_ -> -                    throw({error, {invalid_option_arg, {Name, Arg}}}) -            end; -        false -> -            add_option_with_implicit_arg(OptSpec, OptAcc) -    end. - - -%% @doc Add an option with argument that was part of an assignment expression -%%      (e.g. "--verbose=3") converting it to the data type indicated by the -%%      argument specification. --spec add_option_with_assigned_arg(option_spec(), string(), [option()]) -> [option()]. -add_option_with_assigned_arg({Name, _Short, _Long, ArgSpec, _Help}, Arg, OptAcc) -> -    try -        [{Name, to_type(ArgSpec, Arg)} | OptAcc] -    catch -        error:_ -> -            throw({error, {invalid_option_arg, {Name, Arg}}}) -    end. - - -%% @doc Add an option that required an argument but did not have one. Some data -%%      types (boolean, integer) allow implicit or assumed arguments. --spec add_option_with_implicit_arg(option_spec(), [option()]) -> [option()]. -add_option_with_implicit_arg({Name, _Short, _Long, ArgSpec, _Help}, OptAcc) -> -    case arg_spec_type(ArgSpec) of -        boolean -> -            %% Special case for boolean arguments: if there is no argument we -            %% set the value to 'true'. -            [{Name, true} | OptAcc]; -        integer -> -            %% Special case for integer arguments: if the option had not been set -            %% before we set the value to 1. This is needed to support options like -            %% "-v" to return something like {verbose, 1}. -            [{Name, 1} | OptAcc]; -        _ -> -            throw({error, {missing_option_arg, Name}}) -    end. - - -%% @doc Add an option with an implicit or assumed argument. --spec add_option_with_implicit_incrementable_arg(option_spec() | arg_spec(), [option()]) -> [option()]. -add_option_with_implicit_incrementable_arg({Name, _Short, _Long, ArgSpec, _Help}, OptAcc) -> -    case arg_spec_type(ArgSpec) of -        boolean -> -            %% Special case for boolean arguments: if there is no argument we -            %% set the value to 'true'. -            [{Name, true} | OptAcc]; -        integer -> -            %% Special case for integer arguments: if the option had not been set -            %% before we set the value to 1; if not we increment the previous value -            %% the option had. This is needed to support options like "-vvv" to -            %% return something like {verbose, 3}. -            case OptAcc of -                [{Name, Count} | Tail] -> -                    [{Name, Count + 1} | Tail]; -                _ -> -                    [{Name, 1} | OptAcc] -            end; -        _ -> -            throw({error, {missing_option_arg, Name}}) -    end. - - -%% @doc Retrieve the data type form an argument specification. --spec arg_spec_type(arg_spec()) -> arg_type() | undefined. -arg_spec_type({Type, _DefaultArg}) -> -    Type; -arg_spec_type(Type) when is_atom(Type) -> -    Type. - - -%% @doc Convert an argument string to its corresponding data type. --spec to_type(arg_spec() | arg_type(), string()) -> arg_value(). -to_type({Type, _DefaultArg}, Arg) -> -    to_type(Type, Arg); -to_type(binary, Arg) -> -    list_to_binary(Arg); -to_type(atom, Arg) -> -    list_to_atom(Arg); -to_type(integer, Arg) -> -    list_to_integer(Arg); -to_type(float, Arg) -> -    list_to_float(Arg); -to_type(boolean, Arg) -> -    LowerArg = string:to_lower(Arg), -    case is_arg_true(LowerArg) of -        true -> -            true; -        _ -> -            case is_arg_false(LowerArg) of -                true -> -                    false; -                false -> -                    erlang:error(badarg) -            end -    end; -to_type(_Type, Arg) -> -    Arg. - - --spec is_arg_true(string()) -> boolean(). -is_arg_true(Arg) -> -    (Arg =:= "true") orelse (Arg =:= "t") orelse -    (Arg =:= "yes") orelse (Arg =:= "y") orelse -    (Arg =:= "on") orelse (Arg =:= "enabled") orelse -    (Arg =:= "1"). - - --spec is_arg_false(string()) -> boolean(). -is_arg_false(Arg) -> -    (Arg =:= "false") orelse (Arg =:= "f") orelse -    (Arg =:= "no") orelse (Arg =:= "n") orelse -    (Arg =:= "off") orelse (Arg =:= "disabled") orelse -    (Arg =:= "0"). - - --spec is_valid_arg(arg_spec(), nonempty_string()) -> boolean(). -is_valid_arg({Type, _DefaultArg}, Arg) -> -    is_valid_arg(Type, Arg); -is_valid_arg(boolean, Arg) -> -    is_boolean_arg(Arg); -is_valid_arg(integer, Arg) -> -    is_non_neg_integer_arg(Arg); -is_valid_arg(float, Arg) -> -    is_non_neg_float_arg(Arg); -is_valid_arg(_Type, _Arg) -> -    true. - - --spec is_implicit_arg(arg_spec(), nonempty_string()) -> boolean(). -is_implicit_arg({Type, _DefaultArg}, Arg) -> -    is_implicit_arg(Type, Arg); -is_implicit_arg(boolean, Arg) -> -    not is_boolean_arg(Arg); -is_implicit_arg(integer, Arg) -> -    not is_integer_arg(Arg); -is_implicit_arg(_Type, _Arg) -> -    false. - - --spec is_boolean_arg(string()) -> boolean(). -is_boolean_arg(Arg) -> -    LowerArg = string:to_lower(Arg), -    is_arg_true(LowerArg) orelse is_arg_false(LowerArg). - - --spec is_integer_arg(string()) -> boolean(). -is_integer_arg("-" ++ Tail) -> -    is_non_neg_integer_arg(Tail); -is_integer_arg(Arg) -> -    is_non_neg_integer_arg(Arg). - - --spec is_non_neg_integer_arg(string()) -> boolean(). -is_non_neg_integer_arg([Head | Tail]) when Head >= $0, Head =< $9 -> -    is_non_neg_integer_arg(Tail); -is_non_neg_integer_arg([_Head | _Tail]) -> -    false; -is_non_neg_integer_arg([]) -> -    true. - - --spec is_non_neg_float_arg(string()) -> boolean(). -is_non_neg_float_arg([Head | Tail]) when (Head >= $0 andalso Head =< $9) orelse Head =:= $. -> -    is_non_neg_float_arg(Tail); -is_non_neg_float_arg([_Head | _Tail]) -> -    false; -is_non_neg_float_arg([]) -> -    true. - - -%% @doc  Show a message on standard_error indicating the command line options and -%%       arguments that are supported by the program. --spec usage([option_spec()], string()) -> ok. -usage(OptSpecList, ProgramName) -> -	usage(OptSpecList, ProgramName, standard_error). - - -%% @doc  Show a message on standard_error or standard_io indicating the command line options and -%%       arguments that are supported by the program. --spec usage([option_spec()], string(), output_stream() | string()) -> ok. -usage(OptSpecList, ProgramName, OutputStream) when is_atom(OutputStream) -> -    io:format(OutputStream, "Usage: ~s~s~n~n~s~n", -              [ProgramName, usage_cmd_line(OptSpecList), usage_options(OptSpecList)]); -%% @doc  Show a message on standard_error indicating the command line options and -%%       arguments that are supported by the program. The CmdLineTail argument -%%       is a string that is added to the end of the usage command line. -usage(OptSpecList, ProgramName, CmdLineTail) -> -	usage(OptSpecList, ProgramName, CmdLineTail, standard_error). - - -%% @doc  Show a message on standard_error or standard_io indicating the command line options and -%%       arguments that are supported by the program. The CmdLineTail argument -%%       is a string that is added to the end of the usage command line. --spec usage([option_spec()], string(), string(), output_stream() | [{string(), string()}]) -> ok. -usage(OptSpecList, ProgramName, CmdLineTail, OutputStream) when is_atom(OutputStream) -> -	io:format(OutputStream, "Usage: ~s~s ~s~n~n~s~n", -              [ProgramName, usage_cmd_line(OptSpecList), CmdLineTail, usage_options(OptSpecList)]); -%% @doc  Show a message on standard_error indicating the command line options and -%%       arguments that are supported by the program. The CmdLineTail and OptionsTail -%%       arguments are a string that is added to the end of the usage command line -%%       and a list of tuples that are added to the end of the options' help lines. -usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail) -> -	usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail, standard_error). - - -%% @doc  Show a message on standard_error or standard_io indicating the command line options and -%%       arguments that are supported by the program. The CmdLineTail and OptionsTail -%%       arguments are a string that is added to the end of the usage command line -%%       and a list of tuples that are added to the end of the options' help lines. --spec usage([option_spec()], string(), string(), [{string(), string()}], output_stream()) -> ok. -usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail, OutputStream) -> -    UsageOptions = lists:foldl( -                     fun ({Prefix, Help}, Acc) -> -                             add_option_help(Prefix, Help, Acc) -                     end, usage_options_reverse(OptSpecList, []), OptionsTail), -    io:format(OutputStream, "Usage: ~s~s ~s~n~n~s~n", -              [ProgramName, usage_cmd_line(OptSpecList), CmdLineTail, -               lists:flatten(lists:reverse(UsageOptions))]). - - -%% @doc Return a string with the syntax for the command line options and -%%      arguments. --spec usage_cmd_line([option_spec()]) -> string(). -usage_cmd_line(OptSpecList) -> -    usage_cmd_line(OptSpecList, []). - -usage_cmd_line([{Name, Short, Long, ArgSpec, _Help} | Tail], Acc) -> -    CmdLine = -        case ArgSpec of -            undefined -> -                if -                    %% For options with short form and no argument. -                    Short =/= undefined -> -                        [$\s, $[, $-, Short, $]]; -                    %% For options with only long form and no argument. -                    Long =/= undefined -> -                        [$\s, $[, $-, $-, Long, $]]; -                    true -> -                        [] -                end; -            _ -> -                if -                    %% For options with short form and argument. -                    Short =/= undefined -> -                        [$\s, $[, $-, Short, $\s, $<, atom_to_list(Name), $>, $]]; -                    %% For options with only long form and argument. -                    Long =/= undefined -> -                        [$\s, $[, $-, $-, Long, $\s, $<, atom_to_list(Name), $>, $]]; -                    %% For options with neither short nor long form and argument. -                    true -> -                        [$\s, $<, atom_to_list(Name), $>] -                end -        end, -    usage_cmd_line(Tail, [CmdLine | Acc]); -usage_cmd_line([], Acc) -> -    lists:flatten(lists:reverse(Acc)). - - -%% @doc Return a string with the help message for each of the options and -%%      arguments. --spec usage_options([option_spec()]) -> string(). -usage_options(OptSpecList) -> -    lists:flatten(lists:reverse(usage_options_reverse(OptSpecList, []))). - -usage_options_reverse([{Name, Short, Long, _ArgSpec, Help} | Tail], Acc) -> -    Prefix = -        case Long of -            undefined -> -                case Short of -                    %% Neither short nor long form (non-option argument). -                    undefined -> -                        [$<, atom_to_list(Name), $>]; -                    %% Only short form. -                    _ -> -                        [$-, Short] -                end; -            _ -> -                case Short of -                    %% Only long form. -                    undefined -> -                        [$-, $- | Long]; -                    %% Both short and long form. -                    _ -> -                        [$-, Short, $,, $\s, $-, $- | Long] -                end -        end, -    usage_options_reverse(Tail, add_option_help(Prefix, Help, Acc)); -usage_options_reverse([], Acc) -> -    Acc. - - -%% @doc Add the help message corresponding to an option specification to a list -%%      with the correct indentation. --spec add_option_help(Prefix :: string(), Help :: string(), Acc :: string()) -> string(). -add_option_help(Prefix, Help, Acc) when is_list(Help), Help =/= [] -> -    FlatPrefix = lists:flatten(Prefix), -    case ((?INDENTATION * ?TAB_LENGTH) - 2 - length(FlatPrefix)) of -        TabSize when TabSize > 0 -> -            Tab = lists:duplicate(ceiling(TabSize / ?TAB_LENGTH), $\t), -            [[$\s, $\s, FlatPrefix, Tab, Help, $\n] | Acc]; -        _ -> -            % The indentation for the option description is 3 tabs (i.e. 24 characters) -            % IMPORTANT: Change the number of tabs below if you change the -            %            value of the INDENTATION macro. -            [[$\t, $\t, $\t, Help, $\n], [$\s, $\s, FlatPrefix, $\n] | Acc] -    end; -add_option_help(_Opt, _Prefix, Acc) -> -    Acc. - - - -%% @doc Return the smallest integral value not less than the argument. --spec ceiling(float()) -> integer(). -ceiling(X) -> -    T = erlang:trunc(X), -    case (X - T) of -        % Neg when Neg < 0 -> -        %    T; -        Pos when Pos > 0 -> -            T + 1; -        _ -> -            T -    end. diff --git a/src/rebar.erl b/src/rebar.erl index ded5ebe..36a7b36 100644 --- a/src/rebar.erl +++ b/src/rebar.erl @@ -87,7 +87,9 @@ run(["help"|RawCmds]) when RawCmds =/= [] ->  run(["help"]) ->      help();  run(["info"|_]) -> -    help(); +    %% Catch calls to 'rebar info' to avoid treating plugins' info/2 functions +    %% as commands. +    ?CONSOLE("Command 'info' not understood or not applicable~n", []);  run(["version"]) ->      ok = load_rebar_app(),      %% Display vsn and build time info @@ -178,10 +180,25 @@ run_aux(BaseConfig, Commands) ->  %%  help() ->      OptSpecList = option_spec_list(), -    getopt:usage(OptSpecList, "rebar", -                 "[var=value,...] <command,...>", -                 [{"var=value", "rebar global variables (e.g. force=1)"}, -                  {"command", "Command to run (e.g. compile)"}]), +    rebar_getopt:usage(OptSpecList, "rebar", +                       "[var=value,...] <command,...>", +                       [{"var=value", "rebar global variables (e.g. force=1)"}, +                        {"command", "Command to run (e.g. compile)"}]), + +    ?CONSOLE("To see a list of built-in commands, execute rebar -c.~n~n", []), +    ?CONSOLE( +       "Type 'rebar help <CMD1> <CMD2>' for help on specific commands." +       "~n~n", []), +    ?CONSOLE( +       "rebar allows you to abbreviate the command to run:~n" +       "$ rebar co           # same as rebar compile~n" +       "$ rebar eu           # same as rebar eunit~n" +       "$ rebar g-d          # same as rebar get-deps~n" +       "$ rebar x eu         # same as rebar xref eunit~n" +       "$ rebar l-d          # same as rebar list-deps~n" +       "$ rebar l-d l-t      # same as rebar list-deps list-templates~n" +       "$ rebar list-d l-te  # same as rebar list-deps list-templates~n" +       "~n", []),      ?CONSOLE(         "Core rebar.config options:~n"         "  ~p~n" @@ -212,7 +229,7 @@ help() ->  parse_args(RawArgs) ->      %% Parse getopt options      OptSpecList = option_spec_list(), -    case getopt:parse(OptSpecList, RawArgs) of +    case rebar_getopt:parse(OptSpecList, RawArgs) of          {ok, Args} ->              Args;          {error, {Reason, Data}} -> @@ -255,13 +272,27 @@ save_options(Config, {Options, NonOptArgs}) ->  %% set log level based on getopt option  %%  set_log_level(Config, Options) -> -    LogLevel = case proplists:get_all_values(verbose, Options) of -                   [] -> -                       rebar_log:default_level(); -                   Verbosities -> -                       lists:last(Verbosities) -               end, -    rebar_config:set_global(Config, verbose, LogLevel). +    {IsVerbose, Level} = +        case proplists:get_bool(quiet, Options) of +            true -> +                {false, rebar_log:error_level()}; +            false -> +                DefaultLevel = rebar_log:default_level(), +                case proplists:get_all_values(verbose, Options) of +                    [] -> +                        {false, DefaultLevel}; +                    Verbosities -> +                        {true, DefaultLevel + lists:last(Verbosities)} +                end +        end, + +    case IsVerbose of +        true -> +            Config1 = rebar_config:set_xconf(Config, is_verbose, true), +            rebar_config:set_global(Config1, verbose, Level); +        false -> +            rebar_config:set_global(Config, verbose, Level) +    end.  %%  %% show version information and halt @@ -314,50 +345,54 @@ show_info_maybe_halt(O, Opts, F) ->  %%  commands() ->      S = <<" -clean                                Clean -compile                              Compile sources +clean                                    Clean +compile                                  Compile sources -escriptize                           Generate escript archive +escriptize                               Generate escript archive -create      template= [var=foo,...]  Create skel based on template and vars -create-app  [appid=myapp]            Create simple app skel -create-node [nodeid=mynode]          Create simple node skel -list-templates                       List available templates +create      template= [var=foo,...]      Create skel based on template and vars +create-app  [appid=myapp]                Create simple app skel +create-lib  [libid=mylib]                Create simple lib skel +create-node [nodeid=mynode]              Create simple node skel +list-templates                           List available templates -doc                                  Generate Erlang program documentation +doc                                      Generate Erlang program documentation -check-deps                           Display to be fetched dependencies -get-deps                             Fetch dependencies -update-deps                          Update fetched dependencies -delete-deps                          Delete fetched dependencies -list-deps                            List dependencies +check-deps                               Display to be fetched dependencies +get-deps                                 Fetch dependencies +update-deps                              Update fetched dependencies +delete-deps                              Delete fetched dependencies +list-deps                                List dependencies -generate    [dump_spec=0/1]          Build release with reltool -overlay                              Run reltool overlays only +generate    [dump_spec=0/1]              Build release with reltool +overlay                                  Run reltool overlays only  generate-upgrade  previous_release=path  Build an upgrade package  generate-appups   previous_release=path  Generate appup files -eunit       [suites=foo]             Run eunit tests in foo.erl and -                                     test/foo_tests.erl -            [suites=foo] [tests=bar] Run specific eunit tests [first test name -                                     starting with 'bar' in foo.erl and -                                     test/foo_tests.erl] -            [tests=bar]              For every existing suite, run the first -                                     test whose name starts with bar and, if -                                     no such test exists, run the test whose -                                     name starts with bar in the suite's -                                     _tests module +eunit       [suite[s]=foo]               Run EUnit tests in foo.erl and +                                         test/foo_tests.erl +            [suite[s]=foo] [test[s]=bar] Run specific EUnit tests [first test +                                         name starting with 'bar' in foo.erl +                                         and test/foo_tests.erl] +            [test[s]=bar]                For every existing suite, run the first +                                         test whose name starts with bar and, if +                                         no such test exists, run the test whose +                                         name starts with bar in the suite's +                                         _tests module. +            [random_suite_order=true]    Run tests in a random order, either +            [random_suite_order=Seed]    with a random seed for the PRNG, or a +                                         specific one. -ct          [suites=] [case=]        Run common_test suites +ct          [suite[s]=] [case=]          Run common_test suites -qc                                   Test QuickCheck properties +qc                                       Test QuickCheck properties -xref                                 Run cross reference analysis +xref                                     Run cross reference analysis -help                                 Show the program options -version                              Show version information +help                                     Show the program options +version                                  Show version information  ">>,      io:put_chars(S). @@ -372,12 +407,12 @@ option_spec_list() ->      JobsHelp = io_lib:format(                   "Number of concurrent workers a command may use. Default: ~B",                   [Jobs]), -    VerboseHelp = "Verbosity level (-v, -vv, -vvv, --verbose 3). Default: 0",      [       %% {Name, ShortOpt, LongOpt, ArgSpec, HelpMsg}       {help,     $h, "help",     undefined, "Show the program options"},       {commands, $c, "commands", undefined, "Show available commands"}, -     {verbose,  $v, "verbose",  integer,   VerboseHelp}, +     {verbose,  $v, "verbose",  integer,   "Verbosity level (-v, -vv)"}, +     {quiet,    $q, "quiet",    boolean,   "Quiet, only print error messages"},       {version,  $V, "version",  undefined, "Show version information"},       {force,    $f, "force",    undefined, "Force"},       {defines,  $D, undefined,  string,    "Define compiler macro"}, @@ -414,11 +449,33 @@ filter_flags(Config, [Item | Rest], Commands) ->      end.  command_names() -> -    ["check-deps", "clean", "compile", "create", "create-app", "create-node", -     "ct", "delete-deps", "doc", "eunit", "escriptize", "generate", -     "generate-appups", "generate-upgrade", "get-deps", "help", "list-deps", -     "list-templates", "qc", "update-deps", "overlay", "shell", "version", -     "xref"]. +    [ +     "check-deps", +     "clean", +     "compile", +     "create", +     "create-app", +     "create-lib", +     "create-node", +     "ct", +     "delete-deps", +     "doc", +     "eunit", +     "escriptize", +     "generate", +     "generate-appups", +     "generate-upgrade", +     "get-deps", +     "help", +     "list-deps", +     "list-templates", +     "qc", +     "update-deps", +     "overlay", +     "shell", +     "version", +     "xref" +    ].  unabbreviate_command_names([]) ->      []; diff --git a/src/rebar_app_utils.erl b/src/rebar_app_utils.erl index 8158eb6..a2484e1 100644 --- a/src/rebar_app_utils.erl +++ b/src/rebar_app_utils.erl @@ -163,15 +163,6 @@ consult_app_file(Filename) ->          false ->              file:consult(Filename);          true -> -            %% TODO: EXPERIMENTAL For now let's warn the user if a -            %% script is going to be run. -            case filelib:is_regular([Filename, ".script"]) of -                true -> -                    ?CONSOLE("NOTICE: Using experimental *.app.src.script " -                             "functionality on ~s ~n", [Filename]); -                _ -> -                    ok -            end,              rebar_config:consult_file(Filename)      end. diff --git a/src/rebar_base_compiler.erl b/src/rebar_base_compiler.erl index a0dec30..1957070 100644 --- a/src/rebar_base_compiler.erl +++ b/src/rebar_base_compiler.erl @@ -49,7 +49,7 @@ run(Config, FirstFiles, RestFiles, CompileFn) ->              Jobs = rebar:get_jobs(Config),              ?DEBUG("Starting ~B compile worker(s)~n", [Jobs]),              Pids = [spawn_monitor(F) || _I <- lists:seq(1,Jobs)], -            compile_queue(Pids, RestFiles) +            compile_queue(Config, Pids, RestFiles)      end.  run(Config, FirstFiles, SourceDir, SourceExt, TargetDir, TargetExt, @@ -139,27 +139,31 @@ compile_each([Source | Rest], Config, CompileFn) ->          skipped ->              ?INFO("Skipped ~s\n", [Source]);          Error -> +            ?CONSOLE("Compiling ~s failed:\n", +                     [maybe_absname(Config, Source)]),              maybe_report(Error),              ?DEBUG("Compilation failed: ~p\n", [Error]),              ?FAIL      end,      compile_each(Rest, Config, CompileFn). -compile_queue([], []) -> +compile_queue(_Config, [], []) ->      ok; -compile_queue(Pids, Targets) -> +compile_queue(Config, Pids, Targets) ->      receive          {next, Worker} ->              case Targets of                  [] ->                      Worker ! empty, -                    compile_queue(Pids, Targets); +                    compile_queue(Config, Pids, Targets);                  [Source | Rest] ->                      Worker ! {compile, Source}, -                    compile_queue(Pids, Rest) +                    compile_queue(Config, Pids, Rest)              end; -        {fail, Error} -> +        {fail, {_, {source, Source}}=Error} -> +            ?CONSOLE("Compiling ~s failed:\n", +                     [maybe_absname(Config, Source)]),              maybe_report(Error),              ?DEBUG("Worker compilation failed: ~p\n", [Error]),              ?FAIL; @@ -167,20 +171,20 @@ compile_queue(Pids, Targets) ->          {compiled, Source, Warnings} ->              report(Warnings),              ?CONSOLE("Compiled ~s\n", [Source]), -            compile_queue(Pids, Targets); +            compile_queue(Config, Pids, Targets);          {compiled, Source} ->              ?CONSOLE("Compiled ~s\n", [Source]), -            compile_queue(Pids, Targets); +            compile_queue(Config, Pids, Targets);          {skipped, Source} ->              ?INFO("Skipped ~s\n", [Source]), -            compile_queue(Pids, Targets); +            compile_queue(Config, Pids, Targets);          {'DOWN', Mref, _, Pid, normal} ->              ?DEBUG("Worker exited cleanly\n", []),              Pids2 = lists:delete({Pid, Mref}, Pids), -            compile_queue(Pids2, Targets); +            compile_queue(Config, Pids2, Targets);          {'DOWN', _Mref, _, _Pid, Info} ->              ?DEBUG("Worker failed: ~p\n", [Info]), @@ -202,8 +206,7 @@ compile_worker(QueuePid, Config, CompileFn) ->                      QueuePid ! {skipped, Source},                      compile_worker(QueuePid, Config, CompileFn);                  Error -> -                    QueuePid ! {fail, [{error, Error}, -                                       {source, Source}]}, +                    QueuePid ! {fail, {{error, Error}, {source, Source}}},                      ok              end; @@ -224,7 +227,7 @@ format_warnings(Config, Source, Warnings, Opts) ->               end,      format_errors(Config, Source, Prefix, Warnings). -maybe_report([{error, {error, _Es, _Ws}=ErrorsAndWarnings}, {source, _}]) -> +maybe_report({{error, {error, _Es, _Ws}=ErrorsAndWarnings}, {source, _}}) ->      maybe_report(ErrorsAndWarnings);  maybe_report([{error, E}, {source, S}]) ->      report(["unexpected error compiling " ++ S, io_lib:fwrite("~n~p~n", [E])]); @@ -239,12 +242,7 @@ report(Messages) ->  format_errors(Config, _MainSource, Extra, Errors) ->      [begin -         AbsSource = case rebar_utils:processing_base_dir(Config) of -                         true -> -                             Source; -                         false -> -                             filename:absname(Source) -                     end, +         AbsSource = maybe_absname(Config, Source),           [format_error(AbsSource, Extra, Desc) || Desc <- Descs]       end       || {Source, Descs} <- Errors]. @@ -258,3 +256,11 @@ format_error(AbsSource, Extra, {Line, Mod, Desc}) ->  format_error(AbsSource, Extra, {Mod, Desc}) ->      ErrorDesc = Mod:format_error(Desc),      ?FMT("~s: ~s~s~n", [AbsSource, Extra, ErrorDesc]). + +maybe_absname(Config, Filename) -> +    case rebar_utils:processing_base_dir(Config) of +        true -> +            Filename; +        false -> +            filename:absname(Filename) +    end. diff --git a/src/rebar_config.erl b/src/rebar_config.erl index 461de5d..9b58d4f 100644 --- a/src/rebar_config.erl +++ b/src/rebar_config.erl @@ -31,7 +31,6 @@           get_all/2,           set/3,           set_global/3, get_global/3, -         is_verbose/1,           save_env/3, get_env/2, reset_envs/1,           set_skip_dir/2, is_skip_dir/2, reset_skip_dirs/1,           clean_config/2, @@ -110,10 +109,6 @@ get_global(Config, Key, Default) ->              Value      end. -is_verbose(Config) -> -    DefaulLevel = rebar_log:default_level(), -    get_global(Config, verbose, DefaulLevel) > DefaulLevel. -  consult_file(File) ->      case filename:extension(File) of          ".script" -> diff --git a/src/rebar_core.erl b/src/rebar_core.erl index 3172d64..4efc978 100644 --- a/src/rebar_core.erl +++ b/src/rebar_core.erl @@ -163,6 +163,13 @@ skip_or_process_dir({_, ModuleSetFile}=ModuleSet, Config, CurrentCodePath,  skip_or_process_dir1(AppFile, ModuleSet, Config, CurrentCodePath,                       Dir, Command, DirSet) ->      case rebar_app_utils:is_skipped_app(Config, AppFile) of +        {Config1, {true, _SkippedApp}} when Command == 'update-deps' -> +            %% update-deps does its own app skipping. Unfortunately there's no +            %% way to signal this to rebar_core, so we have to explicitly do it +            %% here... Otherwise if you use app=, it'll skip the toplevel +            %% directory and nothing will be updated. +            process_dir1(Dir, Command, DirSet, Config1, +                         CurrentCodePath, ModuleSet);          {Config1, {true, SkippedApp}} ->              ?DEBUG("Skipping app: ~p~n", [SkippedApp]),              Config2 = increment_operations(Config1), @@ -172,8 +179,9 @@ skip_or_process_dir1(AppFile, ModuleSet, Config, CurrentCodePath,                           CurrentCodePath, ModuleSet)      end. -process_dir1(Dir, Command, DirSet, Config0, CurrentCodePath, +process_dir1(Dir, Command, DirSet, Config, CurrentCodePath,               {DirModules, ModuleSetFile}) -> +    Config0 = rebar_config:set_xconf(Config, current_command, Command),      %% Get the list of modules for "any dir". This is a catch-all list      %% of modules that are processed in addition to modules associated      %% with this directory type. These any_dir modules are processed @@ -391,18 +399,19 @@ update_code_path(Config) ->          [] ->              no_change;          Paths -> -            OldPath = code:get_path(),              LibPaths = expand_lib_dirs(Paths, rebar_utils:get_cwd(), []),              ok = code:add_pathsa(LibPaths), -            {old, OldPath} +            %% track just the paths we added, so we can remove them without +            %% removing other paths added by this dep +            {added, LibPaths}      end.  restore_code_path(no_change) ->      ok; -restore_code_path({old, Path}) -> +restore_code_path({added, Paths}) ->      %% Verify that all of the paths still exist -- some dynamically      %% added paths can get blown away during clean. -    true = code:set_path([F || F <- Path, erl_prim_loader_is_file(F)]), +    _ = [code:del_path(F) || F <- Paths, erl_prim_loader_is_file(F)],      ok.  erl_prim_loader_is_file(File) -> @@ -412,7 +421,10 @@ expand_lib_dirs([], _Root, Acc) ->      Acc;  expand_lib_dirs([Dir | Rest], Root, Acc) ->      Apps = filelib:wildcard(filename:join([Dir, "*", "ebin"])), -    FqApps = [filename:join([Root, A]) || A <- Apps], +    FqApps = case filename:pathtype(Dir) of +                 absolute -> Apps; +                 _        -> [filename:join([Root, A]) || A <- Apps] +             end,      expand_lib_dirs(Rest, Root, Acc ++ FqApps). @@ -490,6 +502,8 @@ acc_modules([Module | Rest], Command, Config, File, Acc) ->  %%  plugin_modules(Config, PredirsAssoc) ->      Modules = lists:flatten(rebar_config:get_all(Config, plugins)), +    ?DEBUG("Plugins requested while processing ~s: ~p~n", +           [rebar_utils:get_cwd(), Modules]),      plugin_modules(Config, PredirsAssoc, ulist(Modules)).  ulist(L) -> @@ -530,21 +544,8 @@ plugin_modules(Config, PredirsAssoc, FoundModules, MissingModules) ->  load_plugin_modules(Config, PredirsAssoc, Modules) ->      Cwd = rebar_utils:get_cwd(), -    PluginDirs = case rebar_config:get_local(Config, plugin_dir, undefined) of -        undefined -> -            % Plugin can be in the project's "plugins" folder -            [filename:join(Cwd, "plugins")]; -        Dir -> -            [Dir] -    end ++ -    % We also want to include this case: -    % Plugin can be in "plugins" directory of the plugin base directory. For -    % example, Cwd depends on Plugin, and deps/Plugin/plugins/Plugin.erl is the -    % plugin. -    [ -        filename:join(Dir, "plugins") || -        Dir <- get_plugin_base_dirs(Cwd, PredirsAssoc) -    ], +    PluginDirs = get_all_plugin_dirs(Config, Cwd, PredirsAssoc), +    ?DEBUG("Plugin dirs for ~s:~n~p~n", [Cwd, PluginDirs]),      %% Find relevant sources in base_dir and plugin_dir      Erls = string:join([atom_to_list(M)++"\\.erl" || M <- Modules], "|"), @@ -559,12 +560,34 @@ load_plugin_modules(Config, PredirsAssoc, Modules) ->      NotLoaded = [V || V <- Modules, FilterMissing(V)],      {Loaded, NotLoaded}. -%% @doc PredirsAssoc is a dictionary of plugindir -> 'parent' pairs +get_all_plugin_dirs(Config, Cwd, PredirsAssoc) -> +    [rebar_utils:get_cwd()] +        ++ get_plugin_dir(Config, Cwd) +        ++ get_base_plugin_dirs(Cwd, PredirsAssoc). + +get_plugin_dir(Config, Cwd) -> +    case rebar_config:get_local(Config, plugin_dir, undefined) of +        undefined -> +            %% Plugin can be in the project's "plugins" folder +            [filename:join(Cwd, "plugins")]; +        Dir -> +            [Dir] +    end. + +%% We also want to include this case: +%% Plugin can be in "plugins" directory of the plugin base directory. +%% For example, Cwd depends on Plugin, and deps/Plugin/plugins/Plugin.erl +%% is the plugin. +get_base_plugin_dirs(Cwd, PredirsAssoc) -> +    [filename:join(Dir, "plugins") || +        Dir <- get_plugin_base_dirs(Cwd, PredirsAssoc)]. + +%% @doc PredirsAssoc is a dictionary of plugindir -> 'parent' pairs.  %% 'parent' in this case depends on plugin; therefore we have to give  %% all plugins that Cwd ('parent' in this case) depends on.  get_plugin_base_dirs(Cwd, PredirsAssoc) ->      [PluginDir || {PluginDir, Master} <- dict:to_list(PredirsAssoc), -        Master =:= Cwd]. +                  Master =:= Cwd].  is_missing_plugin(Loaded) ->      fun(Mod) -> not lists:member(Mod, Loaded) end. diff --git a/src/rebar_ct.erl b/src/rebar_ct.erl index 9951f8e..f3ed29f 100644 --- a/src/rebar_ct.erl +++ b/src/rebar_ct.erl @@ -101,14 +101,15 @@ run_test(TestDir, LogDir, Config, _File) ->      {Cmd, RawLog} = make_cmd(TestDir, LogDir, Config),      ?DEBUG("ct_run cmd:~n~p~n", [Cmd]),      clear_log(LogDir, RawLog), -    Output = case rebar_config:is_verbose(Config) of +    Output = case rebar_log:is_verbose(Config) of                   false ->                       " >> " ++ RawLog ++ " 2>&1";                   true ->                       " 2>&1 | tee -a " ++ RawLog               end, -    case rebar_utils:sh(Cmd ++ Output, [{env,[{"TESTDIR", TestDir}]}, return_on_error]) of +    ShOpts = [{env,[{"TESTDIR", TestDir}]}, return_on_error], +    case rebar_utils:sh(Cmd ++ Output, ShOpts) of          {ok,_} ->              %% in older versions of ct_run, this could have been a failure              %% that returned a non-0 code. Check for that! @@ -135,15 +136,20 @@ clear_log(LogDir, RawLog) ->  check_success_log(Config, RawLog) ->      check_log(Config, RawLog, fun(Msg) -> ?CONSOLE("DONE.\n~s\n", [Msg]) end). -check_fail_log(Config, RawLog, Command, {Rc, Output}) -> -    check_log(Config, RawLog, fun(_Msg) -> +-type err_handler() :: fun((string()) -> no_return()). +-spec failure_logger(string(), {integer(), string()}) -> err_handler(). +failure_logger(Command, {Rc, Output}) -> +    fun(_Msg) ->              ?ABORT("~s failed with error: ~w and output:~n~s~n",                     [Command, Rc, Output]) -    end). +    end. + +check_fail_log(Config, RawLog, Command, Result) -> +    check_log(Config, RawLog, failure_logger(Command, Result)).  check_log(Config,RawLog,Fun) ->      {ok, Msg} = -        rebar_utils:sh("grep -e 'TEST COMPLETE' -e '{error,make_failed}' " +        rebar_utils:sh("grep -e \"TEST COMPLETE\" -e \"{error,make_failed}\" "                         ++ RawLog, [{use_stdout, false}]),      MakeFailed = string:str(Msg, "{error,make_failed}") =/= 0,      RunFailed = string:str(Msg, ", 0 failed") =:= 0, @@ -166,7 +172,7 @@ check_log(Config,RawLog,Fun) ->  %% Show the log if it hasn't already been shown because verbose was on  show_log(Config, RawLog) ->      ?CONSOLE("Showing log\n", []), -    case rebar_config:is_verbose(Config) of +    case rebar_log:is_verbose(Config) of          false ->              {ok, Contents} = file:read_file(RawLog),              ?CONSOLE("~s", [Contents]); @@ -186,6 +192,15 @@ make_cmd(TestDir, RawLogDir, Config) ->                        ""                end, +    %% Check for the availability of ct_run; if we can't find it, generate a +    %% warning and use the old school, less reliable approach to running CT. +    BaseCmd = case os:find_executable("ct_run") of +                  false -> +                      "erl -noshell -s ct_run script_start -s erlang halt"; +                  _ -> +                      "ct_run -noshell" +              end, +      %% Add the code path of the rebar process to the code path. This      %% includes the dependencies in the code path. The directories      %% that are part of the root Erlang install are filtered out to @@ -197,14 +212,15 @@ make_cmd(TestDir, RawLogDir, Config) ->      CodePathString = string:join(CodeDirs, " "),      Cmd = case get_ct_specs(Cwd) of                undefined -> -                  ?FMT("erl " % should we expand ERL_PATH? -                       " -noshell -pa ~s ~s" +                  ?FMT("~s" +                       " -pa ~s" +                       " ~s"                         " ~s"                         " -logdir \"~s\""                         " -env TEST_DIR \"~s\"" -                       " ~s" -                       " -s ct_run script_start -s erlang halt", -                       [CodePathString, +                       " ~s", +                       [BaseCmd, +                        CodePathString,                          Include,                          build_name(Config),                          LogDir, @@ -216,14 +232,15 @@ make_cmd(TestDir, RawLogDir, Config) ->                        get_suites(Config, TestDir) ++                        get_case(Config);                SpecFlags -> -                  ?FMT("erl " % should we expand ERL_PATH? -                       " -noshell -pa ~s ~s" +                  ?FMT("~s" +                       " -pa ~s" +                       " ~s"                         " ~s"                         " -logdir \"~s\""                         " -env TEST_DIR \"~s\"" -                       " ~s" -                       " -s ct_run script_start -s erlang halt", -                       [CodePathString, +                       " ~s", +                       [BaseCmd, +                        CodePathString,                          Include,                          build_name(Config),                          LogDir, @@ -303,7 +320,7 @@ get_config_file(TestDir) ->      end.  get_suites(Config, TestDir) -> -    case rebar_config:get_global(Config, suites, undefined) of +    case get_suites(Config) of          undefined ->              " -dir " ++ TestDir;          Suites -> @@ -312,6 +329,14 @@ get_suites(Config, TestDir) ->              string:join([" -suite"] ++ Suites2, " ")      end. +get_suites(Config) -> +    case rebar_config:get_global(Config, suites, undefined) of +        undefined -> +            rebar_config:get_global(Config, suite, undefined);  +        Suites -> +            Suites +    end. +  find_suite_path(Suite, TestDir) ->      Path = filename:join(TestDir, Suite ++ "_SUITE.erl"),      case filelib:is_regular(Path) of diff --git a/src/rebar_deps.erl b/src/rebar_deps.erl index 313deaa..43bde04 100644 --- a/src/rebar_deps.erl +++ b/src/rebar_deps.erl @@ -68,37 +68,57 @@ preprocess(Config, _) ->      %% Add available deps to code path      Config3 = update_deps_code_path(Config2, AvailableDeps), -    %% If skip_deps=true, mark each dep dir as a skip_dir w/ the core so that -    %% the current command doesn't run on the dep dir. However, pre/postprocess -    %% WILL run (and we want it to) for transitivity purposes. -    %% -    %% Also, if skip_deps=comma,separated,app,list, then only the given -    %% dependencies are skipped. -    NewConfig = case rebar_config:get_global(Config3, skip_deps, false) of -        "true" -> -            lists:foldl( -                fun(#dep{dir = Dir}, C) -> -                        rebar_config:set_skip_dir(C, Dir) -                end, Config3, AvailableDeps); -        Apps when is_list(Apps) -> -            SkipApps = [list_to_atom(App) || App <- string:tokens(Apps, ",")], -            lists:foldl( -                fun(#dep{dir = Dir, app = App}, C) -> -                        case lists:member(App, SkipApps) of -                            true -> rebar_config:set_skip_dir(C, Dir); -                            false -> C -                        end -                end, Config3, AvailableDeps); -        _ -> -            Config3 -    end, -      %% Filtering out 'raw' dependencies so that no commands other than      %% deps-related can be executed on their directories.      NonRawAvailableDeps = [D || D <- AvailableDeps, not D#dep.is_raw], -    %% Return all the available dep directories for process -    {ok, NewConfig, dep_dirs(NonRawAvailableDeps)}. +    case rebar_config:get_xconf(Config, current_command, undefined) of +        'update-deps' -> +            %% Skip ALL of the dep folders, we do this because we don't want +            %% any other calls to preprocess() for update-deps beyond the +            %% toplevel directory. They aren't actually harmful, but they slow +            %% things down unnecessarily. +            NewConfig = lists:foldl( +                          fun(D, Acc) -> +                                  rebar_config:set_skip_dir(Acc, D#dep.dir) +                          end, +                          Config3, +                          collect_deps(rebar_utils:get_cwd(), Config3)), +            %% Return the empty list, as we don't want anything processed before +            %% us. +            {ok, NewConfig, []}; +        _ -> +            %% If skip_deps=true, mark each dep dir as a skip_dir w/ the core +            %% so that the current command doesn't run on the dep dir. +            %% However, pre/postprocess WILL run (and we want it to) for +            %% transitivity purposes. +            %% +            %% Also, if skip_deps=comma,separated,app,list, then only the given +            %% dependencies are skipped. +            NewConfig = +                case rebar_config:get_global(Config3, skip_deps, false) of +                    "true" -> +                        lists:foldl( +                          fun(#dep{dir = Dir}, C) -> +                                  rebar_config:set_skip_dir(C, Dir) +                          end, Config3, AvailableDeps); +                    Apps when is_list(Apps) -> +                        SkipApps = [list_to_atom(App) || +                                       App <- string:tokens(Apps, ",")], +                        lists:foldl( +                          fun(#dep{dir = Dir, app = App}, C) -> +                                  case lists:member(App, SkipApps) of +                                      true -> rebar_config:set_skip_dir(C, Dir); +                                      false -> C +                                  end +                          end, Config3, AvailableDeps); +                    _ -> +                        Config3 +                end, + +            %% Return all the available dep directories for process +            {ok, NewConfig, dep_dirs(NonRawAvailableDeps)} +    end.  postprocess(Config, _) ->      case rebar_config:get_xconf(Config, ?MODULE, undefined) of @@ -169,17 +189,24 @@ do_check_deps(Config) ->      {ok, save_dep_dirs(Config2, lists:reverse(PulledDeps))}.  'update-deps'(Config, _) -> -    %% Determine what deps are required -    RawDeps = rebar_config:get_local(Config, deps, []), -    {Config1, Deps} = find_deps(Config, read, RawDeps), - -    %% Update each dep -    UpdatedDeps = [update_source(Config1, D) -                   || D <- Deps, D#dep.source =/= undefined], +    Config1 = rebar_config:set_xconf(Config, depowner, dict:new()), +    {Config2, UpdatedDeps} = update_deps_int(Config1, []), +    DepOwners = rebar_config:get_xconf(Config2, depowner, dict:new()), + +    %% check for conflicting deps +    _ = [?ERROR("Conflicting dependencies for ~p: ~p~n", +                [K, [{"From: " ++ string:join(dict:fetch(D, DepOwners), ", "), +                      {D#dep.vsn_regex, D#dep.source}} || D <- V]]) +         || {K, V} <- dict:to_list( +                        lists:foldl( +                          fun(Dep, Acc) -> +                                  dict:append(Dep#dep.app, Dep, Acc) +                          end, dict:new(), UpdatedDeps)), +            length(V) > 1],      %% Add each updated dep to our list of dirs for post-processing. This yields      %% the necessary transitivity of the deps -    {ok, save_dep_dirs(Config1, UpdatedDeps)}. +    {ok, save_dep_dirs(Config, UpdatedDeps)}.  'delete-deps'(Config, _) ->      %% Delete all the available deps in our deps/ directory, if any @@ -230,21 +257,42 @@ info_help(Description) ->         [          Description,          {deps_dir, "deps"}, -        {deps, [application_name, -                {application_name, "1.0.*"}, -                {application_name, "1.0.*", -                 {git, "git://github.com/rebar/rebar.git", {branch, "master"}}}, -                {application_name, "", -                 {git, "git://github.com/rebar/rebar.git", {branch, "master"}}, -                 [raw]}]} +        {deps, +         [app_name, +          {rebar, "1.0.*"}, +          {rebar, ".*", +           {git, "git://github.com/rebar/rebar.git"}}, +          {rebar, ".*", +           {git, "git://github.com/rebar/rebar.git", "Rev"}}, +          {rebar, "1.0.*", +           {git, "git://github.com/rebar/rebar.git", {branch, "master"}}}, +          {rebar, "1.0.0", +           {git, "git://github.com/rebar/rebar.git", {tag, "1.0.0"}}}, +          {rebar, "", +           {git, "git://github.com/rebar/rebar.git", {branch, "master"}}, +           [raw]}, +          {app_name, ".*", {hg, "https://www.example.org/url"}}, +          {app_name, ".*", {rsync, "Url"}}, +          {app_name, ".*", {svn, "https://www.example.org/url"}}, +          {app_name, ".*", {svn, "svn://svn.example.org/url"}}, +          {app_name, ".*", {bzr, "https://www.example.org/url", "Rev"}}, +          {app_name, ".*", {fossil, "https://www.example.org/url"}}, +          {app_name, ".*", {fossil, "https://www.example.org/url", "Vsn"}}]}         ]).  %% Added because of trans deps,  %% need all deps in same dir and should be the one set by the root rebar.config +%% In case one is given globally, it has higher priority  %% Sets a default if root config has no deps_dir set  set_shared_deps_dir(Config, []) -> -    GlobalDepsDir = rebar_config:get_global(Config, deps_dir, "deps"), -    DepsDir = rebar_config:get_local(Config, deps_dir, GlobalDepsDir), +    LocalDepsDir = rebar_config:get_local(Config, deps_dir, "deps"), +    GlobalDepsDir = rebar_config:get_global(Config, deps_dir, LocalDepsDir), +    DepsDir = case os:getenv("REBAR_DEPS_DIR") of +                  false -> +                      GlobalDepsDir; +                  Dir -> +                      Dir +              end,      rebar_config:set_xconf(Config, deps_dir, DepsDir);  set_shared_deps_dir(Config, _DepsDir) ->      Config. @@ -256,7 +304,7 @@ get_deps_dir(Config) ->      get_deps_dir(Config, "").  get_deps_dir(Config, App) -> -    BaseDir = rebar_config:get_xconf(Config, base_dir, []), +    BaseDir = rebar_utils:base_dir(Config),      DepsDir = get_shared_deps_dir(Config, "deps"),      {true, filename:join([BaseDir, DepsDir, App])}. @@ -307,7 +355,8 @@ find_deps(Config, Mode, [{App, VsnRegex} | Rest], Acc) when is_atom(App) ->      find_deps(Config, Mode, [{App, VsnRegex, undefined} | Rest], Acc);  find_deps(Config, Mode, [{App, VsnRegex, Source} | Rest], Acc) ->      find_deps(Config, Mode, [{App, VsnRegex, Source, []} | Rest], Acc); -find_deps(Config, Mode, [{App, VsnRegex, Source, Opts} | Rest], Acc) when is_list(Opts) -> +find_deps(Config, Mode, [{App, VsnRegex, Source, Opts} | Rest], Acc) +  when is_list(Opts) ->      Dep = #dep { app = App,                   vsn_regex = VsnRegex,                   source = Source, @@ -411,7 +460,8 @@ is_app_available(Config, App, VsnRegex, Path, _IsRaw = false) ->              {Config, {false, {missing_app_file, Path}}}      end;  is_app_available(Config, App, _VsnRegex, Path, _IsRaw = true) -> -    ?DEBUG("is_app_available, looking for Raw Depencency ~p with Path ~p~n", [App, Path]), +    ?DEBUG("is_app_available, looking for Raw Depencency ~p with Path ~p~n", +           [App, Path]),      case filelib:is_dir(Path) of          true ->              %% TODO: look for version string in <Path>/VERSION file? Not clear @@ -434,8 +484,8 @@ use_source(Config, Dep, Count) ->      case filelib:is_dir(Dep#dep.dir) of          true ->              %% Already downloaded -- verify the versioning matches the regex -            case is_app_available(Config, Dep#dep.app, -                                  Dep#dep.vsn_regex, Dep#dep.dir, Dep#dep.is_raw) of +            case is_app_available(Config, Dep#dep.app, Dep#dep.vsn_regex, +                                  Dep#dep.dir, Dep#dep.is_raw) of                  {Config1, {true, _}} ->                      Dir = filename:join(Dep#dep.dir, "ebin"),                      ok = filelib:ensure_dir(filename:join(Dir, "dummy")), @@ -496,8 +546,6 @@ download_source(AppDir, {rsync, Url}) ->      rebar_utils:sh(?FMT("rsync -az --delete ~s/ ~s", [Url, AppDir]), []);  download_source(AppDir, {fossil, Url}) ->      download_source(AppDir, {fossil, Url, ""}); -download_source(AppDir, {fossil, Url, latest}) -> -    download_source(AppDir, {fossil, Url, ""});  download_source(AppDir, {fossil, Url, Version}) ->      Repository = filename:join(AppDir, filename:basename(AppDir) ++ ".fossil"),      ok = filelib:ensure_dir(Repository), @@ -532,10 +580,12 @@ update_source1(AppDir, {git, Url, ""}) ->  update_source1(AppDir, {git, _Url, {branch, Branch}}) ->      ShOpts = [{cd, AppDir}],      rebar_utils:sh("git fetch origin", ShOpts), -    rebar_utils:sh(?FMT("git checkout -q origin/~s", [Branch]), ShOpts); +    rebar_utils:sh(?FMT("git checkout -q ~s", [Branch]), ShOpts), +    rebar_utils:sh( +      ?FMT("git pull --ff-only --no-rebase -q origin ~s", [Branch]),ShOpts);  update_source1(AppDir, {git, _Url, {tag, Tag}}) ->      ShOpts = [{cd, AppDir}], -    rebar_utils:sh("git fetch --tags origin", ShOpts), +    rebar_utils:sh("git fetch origin", ShOpts),      rebar_utils:sh(?FMT("git checkout -q ~s", [Tag]), ShOpts);  update_source1(AppDir, {git, _Url, Refspec}) ->      ShOpts = [{cd, AppDir}], @@ -551,13 +601,90 @@ update_source1(AppDir, {rsync, Url}) ->      rebar_utils:sh(?FMT("rsync -az --delete ~s/ ~s",[Url,AppDir]),[]);  update_source1(AppDir, {fossil, Url}) ->      update_source1(AppDir, {fossil, Url, ""}); -update_source1(AppDir, {fossil, Url, latest}) -> -    update_source1(AppDir, {fossil, Url, ""});  update_source1(AppDir, {fossil, _Url, Version}) ->      ok = file:set_cwd(AppDir),      rebar_utils:sh("fossil pull", [{cd, AppDir}]),      rebar_utils:sh(?FMT("fossil update ~s", [Version]), []). +%% Recursively update deps, this is not done via rebar's usual dep traversal as +%% that is the wrong order (tips are updated before branches). Instead we do a +%% traverse the deps at each level completely before traversing *their* deps. +%% This allows updates to actually propogate down the tree, rather than fail to +%% flow up the tree, which was the previous behaviour. +update_deps_int(Config0, UDD) -> +    %% Determine what deps are required +    ConfDir = filename:basename(rebar_utils:get_cwd()), +    RawDeps = rebar_config:get_local(Config0, deps, []), +    {Config1, Deps} = find_deps(Config0, read, RawDeps), + +    %% Update each dep +    UpdatedDeps = [update_source(Config1, D) +                   || D <- Deps, D#dep.source =/= undefined, +                      not lists:member(D, UDD), +                      not should_skip_update_dep(Config1, D) +                  ], + +    lists:foldl(fun(Dep, {Config, Updated}) -> +                        {true, AppDir} = get_deps_dir(Config, Dep#dep.app), +                        Config2 = case has_vcs_dir(element(1, Dep#dep.source), +                                                   AppDir) of +                                      false -> +                                          %% If the dep did not exist (maybe it +                                          %% was added), clone it. +                                          %% We'll traverse ITS deps below and +                                          %% clone them if needed. +                                          {C1, _D1} = use_source(Config, Dep), +                                          C1; +                                      true -> +                                          Config +                                  end, +                        ok = file:set_cwd(AppDir), +                        Config3 = rebar_config:new(Config2), +                        %% track where a dep comes from... +                        DepOwner = dict:append( +                                     Dep, ConfDir, +                                     rebar_config:get_xconf(Config3, depowner, +                                                            dict:new())), +                        Config4 = rebar_config:set_xconf(Config3, depowner, +                                                         DepOwner), + +                        {Config5, Res} = update_deps_int(Config4, Updated), +                        {Config5, lists:umerge(lists:sort(Res), +                                               lists:sort(Updated))} +                end, {Config1, lists:umerge(lists:sort(UpdatedDeps), +                                            lists:sort(UDD))}, UpdatedDeps). + +should_skip_update_dep(Config, Dep) -> +    {true, AppDir} = get_deps_dir(Config, Dep#dep.app), +    case rebar_app_utils:is_app_dir(AppDir) of +        false -> +            false; +        {true, AppFile} -> +            case rebar_app_utils:is_skipped_app(Config, AppFile) of +                {_Config, {true, _SkippedApp}} -> +                    true; +                _ -> +                    false +            end +    end. + +%% Recursively walk the deps and build a list of them. +collect_deps(Dir, C) -> +    case file:set_cwd(Dir) of +        ok -> +            Config = rebar_config:new(C), +            RawDeps = rebar_config:get_local(Config, deps, []), +            {Config1, Deps} = find_deps(Config, read, RawDeps), + +            lists:flatten(Deps ++ [begin +                                       {true, AppDir} = get_deps_dir( +                                                          Config1, Dep#dep.app), +                                       collect_deps(AppDir, C) +                                   end || Dep <- Deps]); +        _ -> +            [] +    end. +  %% ===================================================================  %% Source helper functions diff --git a/src/rebar_dia_compiler.erl b/src/rebar_dia_compiler.erl index f81c734..ba9d159 100644 --- a/src/rebar_dia_compiler.erl +++ b/src/rebar_dia_compiler.erl @@ -75,8 +75,8 @@ compile_dia(Source, Target, Config) ->      case diameter_dict_util:parse({path, Source}, []) of          {ok, Spec} ->              FileName = dia_filename(Source, Spec), -            diameter_codegen:from_dict(FileName, Spec, Opts, erl), -            diameter_codegen:from_dict(FileName, Spec, Opts, hrl), +            _ = diameter_codegen:from_dict(FileName, Spec, Opts, erl), +            _ = diameter_codegen:from_dict(FileName, Spec, Opts, hrl),              HrlFile = filename:join("src", FileName ++ ".hrl"),              case filelib:is_regular(HrlFile) of                  true -> diff --git a/src/rebar_erlc_compiler.erl b/src/rebar_erlc_compiler.erl index caef0d2..a1740b0 100644 --- a/src/rebar_erlc_compiler.erl +++ b/src/rebar_erlc_compiler.erl @@ -34,6 +34,18 @@           info/2]).  -include("rebar.hrl"). +-include_lib("stdlib/include/erl_compile.hrl"). + +-define(ERLCINFO_VSN, 1). +-define(ERLCINFO_FILE, "erlcinfo"). +-type erlc_info_v() :: {digraph:vertex(), term()} | 'false'. +-type erlc_info_e() :: {digraph:vertex(), digraph:vertex()}. +-type erlc_info() :: {list(erlc_info_v()), list(erlc_info_e())}. +-record(erlcinfo, +        { +          vsn = ?ERLCINFO_VSN :: pos_integer(), +          info = {[], []} :: erlc_info() +        }).  %% ===================================================================  %% Public API @@ -89,7 +101,7 @@ compile(Config, _AppFile) ->      doterl_compile(Config, "ebin").  -spec clean(rebar_config:config(), file:filename()) -> 'ok'. -clean(_Config, _AppFile) -> +clean(Config, _AppFile) ->      MibFiles = rebar_utils:find_files("mibs", "^.*\\.mib\$"),      MIBs = [filename:rootname(filename:basename(MIB)) || MIB <- MibFiles],      rebar_file_utils:delete_each( @@ -102,6 +114,9 @@ clean(_Config, _AppFile) ->        [ binary_to_list(iolist_to_binary(re:replace(F, "\\.[x|y]rl$", ".erl")))          || F <- YrlFiles ]), +    %% Delete the build graph, if any +    rebar_file_utils:rm_rf(erlcinfo_file(Config)), +      %% Erlang compilation is recursive, so it's possible that we have a nested      %% directory structure in ebin with .beam files within. As such, we want      %% to scan whatever is left in the ebin/ directory for sub-dirs which @@ -156,7 +171,8 @@ test_compile(Config, Cmd, OutDir) ->      %% Compile erlang code to OutDir, using a tweaked config      %% with appropriate defines for eunit, and include all the test modules      %% as well. -    ok = doterl_compile(test_compile_config(Config, Cmd), OutDir, TestErls), +    ok = doterl_compile(test_compile_config(Config, ErlOpts, Cmd), +                        OutDir, TestErls),      {ok, SrcErls}. @@ -200,12 +216,11 @@ info_help(Description) ->          {yrl_first_files, []}         ]). -test_compile_config(Config, Cmd) -> +test_compile_config(Config, ErlOpts, Cmd) ->      {Config1, TriqOpts} = triq_opts(Config),      {Config2, PropErOpts} = proper_opts(Config1),      {Config3, EqcOpts} = eqc_opts(Config2), -    ErlOpts = rebar_config:get_list(Config3, erl_opts, []),      OptsAtom = list_to_atom(Cmd ++ "_compile_opts"),      EunitOpts = rebar_config:get_list(Config3, OptsAtom, []),      Opts0 = [{d, 'TEST'}] ++ @@ -259,7 +274,7 @@ doterl_compile(Config, OutDir) ->      doterl_compile(Config, OutDir, []).  doterl_compile(Config, OutDir, MoreSources) -> -    FirstErls = rebar_config:get_list(Config, erl_first_files, []), +    ErlFirstFiles = rebar_config:get_list(Config, erl_first_files, []),      ErlOpts = rebar_utils:erl_opts(Config),      ?DEBUG("erl_opts ~p~n", [ErlOpts]),      %% Support the src_dirs option allowing multiple directories to @@ -267,114 +282,270 @@ doterl_compile(Config, OutDir, MoreSources) ->      %% eunit tests be separated from the core application source.      SrcDirs = rebar_utils:src_dirs(proplists:append_values(src_dirs, ErlOpts)),      RestErls  = [Source || Source <- gather_src(SrcDirs, []) ++ MoreSources, -                           not lists:member(Source, FirstErls)], - -    %% Split RestErls so that parse_transforms and behaviours are instead added -    %% to erl_first_files, parse transforms first. -    %% This should probably be somewhat combined with inspect_epp -    [ParseTransforms, Behaviours, OtherErls] = -        lists:foldl(fun(F, [A, B, C]) -> -                            case compile_priority(F) of -                                parse_transform -> -                                    [[F | A], B, C]; -                                behaviour -> -                                    [A, [F | B], C]; -                                callback -> -                                    [A, [F | B], C]; -                                _ -> -                                    [A, B, [F | C]] -                            end -                    end, [[], [], []], RestErls), - -    NewFirstErls = FirstErls ++ ParseTransforms ++ Behaviours, - +                           not lists:member(Source, ErlFirstFiles)],      %% Make sure that ebin/ exists and is on the path      ok = filelib:ensure_dir(filename:join("ebin", "dummy.beam")),      CurrPath = code:get_path(),      true = code:add_path(filename:absname("ebin")),      OutDir1 = proplists:get_value(outdir, ErlOpts, OutDir), -    rebar_base_compiler:run(Config, NewFirstErls, OtherErls, -                            fun(S, C) -> -                                    internal_erl_compile(C, S, OutDir1, ErlOpts) -                            end), +    G = init_erlcinfo(Config, RestErls), +    %% Split RestErls so that files which are depended on are treated +    %% like erl_first_files. +    {OtherFirstErls, OtherErls} = +        lists:partition( +          fun(F) -> +                  Children = get_children(G, F), +                  log_files(?FMT("Files dependent on ~s", [F]), Children), + +                  case erls(Children) of +                      [] -> +                          %% There are no files dependent on this file. +                          false; +                      _ -> +                          %% There are some files dependent on the file. +                          %% Thus the file has higher priority +                          %% and should be compiled in the first place. +                          true +                  end +          end, RestErls), +    %% Dependencies of OtherFirstErls that must be compiled first. +    OtherFirstErlsDeps = lists:flatmap( +                           fun(Erl) -> erls(get_parents(G, Erl)) end, +                           OtherFirstErls), +    %% NOTE: In case the way we retrieve OtherFirstErlsDeps or merge +    %% it with OtherFirstErls does not result in the correct compile +    %% priorities, or the method in use proves to be too slow for +    %% certain projects, consider using a more elaborate method (maybe +    %% digraph_utils) or alternatively getting and compiling the .erl +    %% parents of an individual Source in internal_erl_compile. By not +    %% handling this in internal_erl_compile, we also avoid extra +    %% needs_compile/2 calls. +    FirstErls = ErlFirstFiles ++ uo_merge(OtherFirstErlsDeps, OtherFirstErls), +    ?DEBUG("Files to compile first: ~p~n", [FirstErls]), +    rebar_base_compiler:run( +      Config, FirstErls, OtherErls, +      fun(S, C) -> +              internal_erl_compile(C, S, OutDir1, ErlOpts, G) +      end),      true = code:set_path(CurrPath),      ok. +%% +%% Return all .erl files from a list of files +%% +erls(Files) -> +    [Erl || Erl <- Files, filename:extension(Erl) =:= ".erl"]. + +%% +%% Return a list without duplicates while preserving order +%% +ulist(L) -> +    ulist(L, []). + +ulist([H|T], Acc) -> +    case lists:member(H, T) of +        true -> +            ulist(T, Acc); +        false -> +            ulist(T, [H|Acc]) +    end; +ulist([], Acc) -> +    lists:reverse(Acc). + +%% +%% Merge two lists without duplicates while preserving order +%% +uo_merge(L1, L2) -> +    lists:foldl(fun(E, Acc) -> u_add_element(E, Acc) end, ulist(L1), L2). + +u_add_element(Elem, [Elem|_]=Set) -> Set; +u_add_element(Elem, [E1|Set])     -> [E1|u_add_element(Elem, Set)]; +u_add_element(Elem, [])           -> [Elem]. +  -spec include_path(file:filename(),                     rebar_config:config()) -> [file:filename(), ...].  include_path(Source, Config) ->      ErlOpts = rebar_config:get(Config, erl_opts, []), -    ["include", filename:dirname(Source)] -        ++ proplists:get_all_values(i, ErlOpts). - --spec inspect(file:filename(), -              [file:filename(), ...]) -> {string(), [string()]}. -inspect(Source, IncludePath) -> -    ModuleDefault = filename:basename(Source, ".erl"), -    case epp:open(Source, IncludePath) of -        {ok, Epp} -> -            inspect_epp(Epp, Source, ModuleDefault, []); -        {error, Reason} -> -            ?DEBUG("Failed to inspect ~s: ~p\n", [Source, Reason]), -            {ModuleDefault, []} -    end. - --spec inspect_epp(pid(), file:filename(), file:filename(), -                  [string()]) -> {string(), [string()]}. -inspect_epp(Epp, Source, Module, Includes) -> -    case epp:parse_erl_form(Epp) of -        {ok, {attribute, _, module, ModInfo}} -> -            ActualModuleStr = -                case ModInfo of -                    %% Typical module name, single atom -                    ActualModule when is_atom(ActualModule) -> -                        atom_to_list(ActualModule); -                    %% Packag-ized module name, list of atoms -                    ActualModule when is_list(ActualModule) -> -                        string:join([atom_to_list(P) || -                                        P <- ActualModule], "."); -                    %% Parameterized module name, single atom -                    {ActualModule, _} when is_atom(ActualModule) -> -                        atom_to_list(ActualModule); -                    %% Parameterized and packagized module name, list of atoms -                    {ActualModule, _} when is_list(ActualModule) -> -                        string:join([atom_to_list(P) || -                                        P <- ActualModule], ".") -                end, -            inspect_epp(Epp, Source, ActualModuleStr, Includes); -        {ok, {attribute, 1, file, {Module, 1}}} -> -            inspect_epp(Epp, Source, Module, Includes); -        {ok, {attribute, 1, file, {Source, 1}}} -> -            inspect_epp(Epp, Source, Module, Includes); -        {ok, {attribute, 1, file, {IncFile, 1}}} -> -            inspect_epp(Epp, Source, Module, [IncFile | Includes]); -        {eof, _} -> -            epp:close(Epp), -            {Module, Includes}; -        _ -> -            inspect_epp(Epp, Source, Module, Includes) -    end. +    lists:usort(["include", filename:dirname(Source)] +                ++ proplists:get_all_values(i, ErlOpts)).  -spec needs_compile(file:filename(), file:filename(),                      [string()]) -> boolean(). -needs_compile(Source, Target, Hrls) -> +needs_compile(Source, Target, Parents) ->      TargetLastMod = filelib:last_modified(Target),      lists:any(fun(I) -> TargetLastMod < filelib:last_modified(I) end, -              [Source] ++ Hrls). +              [Source] ++ Parents). + +check_erlcinfo(_Config, #erlcinfo{vsn=?ERLCINFO_VSN}) -> +    ok; +check_erlcinfo(Config, #erlcinfo{vsn=Vsn}) -> +    ?ABORT("~s file version is incompatible. expected: ~b got: ~b~n", +           [erlcinfo_file(Config), ?ERLCINFO_VSN, Vsn]); +check_erlcinfo(Config, _) -> +    ?ABORT("~s file is invalid. Please delete before next run.~n", +           [erlcinfo_file(Config)]). + +erlcinfo_file(Config) -> +    filename:join([rebar_utils:base_dir(Config), ".rebar", ?ERLCINFO_FILE]). + +init_erlcinfo(Config, Erls) -> +    G = restore_erlcinfo(Config), +    %% Get a unique list of dirs based on the source files' locations. +    %% This is used for finding files in sub dirs of the configured +    %% src_dirs. For example, src/sub_dir/foo.erl. +    Dirs = sets:to_list(lists:foldl( +                          fun(Erl, Acc) -> +                                  Dir = filename:dirname(Erl), +                                  sets:add_element(Dir, Acc) +                          end, sets:new(), Erls)), +    Updates = [update_erlcinfo(G, Erl, include_path(Erl, Config) ++ Dirs) +               || Erl <- Erls], +    Modified = lists:member(modified, Updates), +    ok = store_erlcinfo(G, Config, Modified), +    G. + +update_erlcinfo(G, Source, Dirs) -> +    case digraph:vertex(G, Source) of +        {_, LastUpdated} -> +            LastModified = filelib:last_modified(Source), +            if LastModified == 0 -> +                    %% The file doesn't exist anymore, +                    %% erase it from the graph. +                    %% All the edges will be erased automatically. +                    digraph:del_vertex(G, Source), +                    modified; +               LastUpdated < LastModified -> +                    modify_erlcinfo(G, Source, Dirs); +                    modified; +               true -> +                    unmodified +            end; +        false -> +            modify_erlcinfo(G, Source, Dirs), +            modified +    end. + +modify_erlcinfo(G, Source, Dirs) -> +    {ok, Fd} = file:open(Source, [read]), +    Incls = parse_attrs(Fd, []), +    AbsIncls = expand_file_names(Incls, Dirs), +    ok = file:close(Fd), +    LastUpdated = {date(), time()}, +    digraph:add_vertex(G, Source, LastUpdated), +    lists:foreach( +      fun(Incl) -> +              update_erlcinfo(G, Incl, Dirs), +              digraph:add_edge(G, Source, Incl) +      end, AbsIncls). + +restore_erlcinfo(Config) -> +    File = erlcinfo_file(Config), +    G = digraph:new(), +    case file:read_file(File) of +        {ok, Data} -> +            try binary_to_term(Data) of +                Erlcinfo -> +                    ok = check_erlcinfo(Config, Erlcinfo), +                    #erlcinfo{info=ErlcInfo} = Erlcinfo, +                    {Vs, Es} = ErlcInfo, +                    lists:foreach( +                      fun({V, LastUpdated}) -> +                              digraph:add_vertex(G, V, LastUpdated) +                      end, Vs), +                    lists:foreach( +                      fun({V1, V2}) -> +                              digraph:add_edge(G, V1, V2) +                      end, Es) +            catch +                error:badarg -> +                    ?ERROR( +                       "Failed (binary_to_term) to restore rebar info file." +                       " Discard file.~n", []), +                    ok +            end; +        _Err -> +            ok +    end, +    G. + +store_erlcinfo(_G, _Config, _Modified = false) -> +    ok; +store_erlcinfo(G, Config, _Modified) -> +    Vs = lists:map( +           fun(V) -> +                   digraph:vertex(G, V) +           end, digraph:vertices(G)), +    Es = lists:flatmap( +           fun({V, _}) -> +                   lists:map( +                     fun(E) -> +                             {_, V1, V2, _} = digraph:edge(G, E), +                             {V1, V2} +                     end, digraph:out_edges(G, V)) +           end, Vs), +    File = erlcinfo_file(Config), +    ok = filelib:ensure_dir(File), +    Data = term_to_binary(#erlcinfo{info={Vs, Es}}, [{compressed, 9}]), +    file:write_file(File, Data). + +%% NOTE: If, for example, one of the entries in Files, refers to +%% gen_server.erl, that entry will be dropped. It is dropped because +%% such an entry usually refers to the beam file, and we don't pass a +%% list of OTP src dirs for finding gen_server.erl's full path. Also, +%% if gen_server.erl was modified, it's not rebar's task to compile a +%% new version of the beam file. Therefore, it's reasonable to drop +%% such entries. Also see process_attr(behaviour, Form, Includes). +-spec expand_file_names([file:filename()], +                        [file:filename()]) -> [file:filename()]. +expand_file_names(Files, Dirs) -> +    %% We check if Files exist by itself or within the directories +    %% listed in Dirs. +    %% Return the list of files matched. +    lists:flatmap( +      fun(Incl) -> +              case filelib:is_regular(Incl) of +                  true -> +                      [Incl]; +                  false -> +                      lists:flatmap( +                        fun(Dir) -> +                                FullPath = filename:join(Dir, Incl), +                                case filelib:is_regular(FullPath) of +                                    true -> +                                        [FullPath]; +                                    false -> +                                        [] +                                end +                        end, Dirs) +              end +      end, Files). + +-spec get_parents(digraph(), file:filename()) -> [file:filename()]. +get_parents(G, Source) -> +    %% Return all files which the Source depends upon. +    digraph_utils:reachable_neighbours([Source], G). + +-spec get_children(digraph(), file:filename()) -> [file:filename()]. +get_children(G, Source) -> +    %% Return all files dependent on the Source. +    digraph_utils:reaching_neighbours([Source], G).  -spec internal_erl_compile(rebar_config:config(), file:filename(), -                           file:filename(), list()) -> 'ok' | 'skipped'. -internal_erl_compile(Config, Source, Outdir, ErlOpts) -> +                           file:filename(), list(), +                           digraph()) -> 'ok' | 'skipped'. +internal_erl_compile(Config, Source, OutDir, ErlOpts, G) ->      %% Determine the target name and includes list by inspecting the source file -    {Module, Hrls} = inspect(Source, include_path(Source, Config)), +    Module = filename:basename(Source, ".erl"), +    Parents = get_parents(G, Source), +    log_files(?FMT("~s depends on", [Source]), Parents),      %% Construct the target filename -    Target = filename:join([Outdir | string:tokens(Module, ".")]) ++ ".beam", +    Target = filename:join([OutDir | string:tokens(Module, ".")]) ++ ".beam",      ok = filelib:ensure_dir(Target),      %% If the file needs compilation, based on last mod date of includes or      %% the target -    case needs_compile(Source, Target, Hrls) of +    case needs_compile(Source, Target, Parents) of          true ->              Opts = [{outdir, filename:dirname(Target)}] ++                  ErlOpts ++ [{i, "include"}, return], @@ -401,7 +572,14 @@ compile_mib(Source, Target, Config) ->      case snmpc:compile(Source, Opts) of          {ok, _} ->              Mib = filename:rootname(Target), -            ok = snmpc:mib_to_hrl(Mib), +            MibToHrlOpts = +                case proplists:get_value(verbosity, Opts, undefined) of +                    undefined -> +                        #options{specific = []}; +                    Verbosity -> +                        #options{specific = [{verbosity, Verbosity}]} +                end, +            ok = snmpc:mib_to_hrl(Mib, Mib, MibToHrlOpts),              Hrl_filename = Mib ++ ".hrl",              rebar_file_utils:mv(Hrl_filename, "include"),              ok; @@ -455,40 +633,97 @@ delete_dir(Dir, Subdirs) ->      lists:foreach(fun(D) -> delete_dir(D, dirs(D)) end, Subdirs),      file:del_dir(Dir). --spec compile_priority(file:filename()) -> 'normal' | 'behaviour' | -                                           'callback' | -                                           'parse_transform'. -compile_priority(File) -> -    case epp_dodger:parse_file(File) of -        {error, _} -> -            normal; % couldn't parse the file, default priority -        {ok, Trees} -> -            F2 = fun({tree,arity_qualifier,_, -                      {arity_qualifier,{tree,atom,_,behaviour_info}, -                       {tree,integer,_,1}}}, _) -> -                         behaviour; -                    ({tree,arity_qualifier,_, -                      {arity_qualifier,{tree,atom,_,parse_transform}, -                       {tree,integer,_,2}}}, _) -> -                         parse_transform; -                    (_, Acc) -> -                         Acc -                 end, - -            F = fun({tree, attribute, _, -                     {attribute, {tree, atom, _, export}, -                      [{tree, list, _, {list, List, none}}]}}, Acc) -> -                        lists:foldl(F2, Acc, List); -                   ({tree, attribute, _, -                     {attribute, {tree, atom, _, callback},_}}, _Acc) -> -                        callback; -                   (_, Acc) -> -                        Acc -                end, +parse_attrs(Fd, Includes) -> +    case io:parse_erl_form(Fd, "") of +        {ok, Form, _Line} -> +            case erl_syntax:type(Form) of +                attribute -> +                    NewIncludes = process_attr(Form, Includes), +                    parse_attrs(Fd, NewIncludes); +                _ -> +                    parse_attrs(Fd, Includes) +            end; +        {eof, _} -> +            Includes; +        _Err -> +            parse_attrs(Fd, Includes) +    end. -            lists:foldl(F, normal, Trees) +process_attr(Form, Includes) -> +    try +        AttrName = erl_syntax:atom_value(erl_syntax:attribute_name(Form)), +        process_attr(AttrName, Form, Includes) +    catch _:_ -> +            %% TODO: We should probably try to be more specific here +            %% and not suppress all errors. +            Includes      end. +process_attr(import, Form, Includes) -> +    case erl_syntax_lib:analyze_import_attribute(Form) of +        {Mod, _Funs} -> +            [atom_to_list(Mod) ++ ".erl"|Includes]; +        Mod -> +            [atom_to_list(Mod) ++ ".erl"|Includes] +    end; +process_attr(file, Form, Includes) -> +    {File, _} = erl_syntax_lib:analyze_file_attribute(Form), +    [File|Includes]; +process_attr(include, Form, Includes) -> +    [FileNode] = erl_syntax:attribute_arguments(Form), +    File = erl_syntax:string_value(FileNode), +    [File|Includes]; +process_attr(include_lib, Form, Includes) -> +    [FileNode] = erl_syntax:attribute_arguments(Form), +    RawFile = erl_syntax:string_value(FileNode), +    File = maybe_expand_include_lib_path(RawFile), +    [File|Includes]; +process_attr(behaviour, Form, Includes) -> +    [FileNode] = erl_syntax:attribute_arguments(Form), +    File = erl_syntax:atom_name(FileNode) ++ ".erl", +    [File|Includes]; +process_attr(compile, Form, Includes) -> +    [Arg] = erl_syntax:attribute_arguments(Form), +    case erl_syntax:concrete(Arg) of +        {parse_transform, Mod} -> +            [atom_to_list(Mod) ++ ".erl"|Includes]; +        {core_transform, Mod} -> +            [atom_to_list(Mod) ++ ".erl"|Includes]; +        L when is_list(L) -> +            lists:foldl( +              fun({parse_transform, M}, Acc) -> +                      [atom_to_list(M) ++ ".erl"|Acc]; +                 ({core_transform, M}, Acc) -> +                      [atom_to_list(M) ++ ".erl"|Acc]; +                 (_, Acc) -> +                      Acc +              end, Includes, L) +    end. + +%% Given the filename from an include_lib attribute, if the path +%% exists, return unmodified, or else get the absolute ERL_LIBS +%% path. +maybe_expand_include_lib_path(File) -> +    case filelib:is_regular(File) of +        true -> +            File; +        false -> +            expand_include_lib_path(File) +    end. + +%% Given a path like "stdlib/include/erl_compile.hrl", return +%% "OTP_INSTALL_DIR/lib/erlang/lib/stdlib-x.y.z/include/erl_compile.hrl". +%% Usually a simple [Lib, SubDir, File1] = filename:split(File) should +%% work, but to not crash when an unusual include_lib path is used, +%% utilize more elaborate logic. +expand_include_lib_path(File) -> +    File1 = filename:basename(File), +    Split = filename:split(filename:dirname(File)), +    Lib = hd(Split), +    SubDir = filename:join(tl(Split)), +    Dir = code:lib_dir(list_to_atom(Lib), list_to_atom(SubDir)), +    filename:join(Dir, File1). +  %%  %% Ensure all files in a list are present and abort if one is missing  %% @@ -501,3 +736,13 @@ check_file(File) ->          false -> ?ABORT("File ~p is missing, aborting\n", [File]);          true -> File      end. + +%% Print prefix followed by list of files. If the list is empty, print +%% on the same line, otherwise use a separate line. +log_files(Prefix, Files) -> +    case Files of +        [] -> +            ?DEBUG("~s: ~p~n", [Prefix, Files]); +        _ -> +            ?DEBUG("~s:~n~p~n", [Prefix, Files]) +    end. diff --git a/src/rebar_erlydtl_compiler.erl b/src/rebar_erlydtl_compiler.erl index 4449be6..556e841 100644 --- a/src/rebar_erlydtl_compiler.erl +++ b/src/rebar_erlydtl_compiler.erl @@ -116,8 +116,8 @@ compile(Config, _AppFile) ->                                       option(source_ext, DtlOpts),                                       option(out_dir, DtlOpts),                                       option(module_ext, DtlOpts) ++ ".beam", -                                     fun(S, T, _C) -> -                                        compile_dtl(S, T, DtlOpts) +                                     fun(S, T, C) -> +                                        compile_dtl(C, S, T, DtlOpts)                                       end,                                       [{check_last_mod, false},                                        {recursive, option(recursive, DtlOpts)}]) @@ -169,58 +169,71 @@ default(out_dir)  -> "ebin";  default(source_ext) -> ".dtl";  default(module_ext) -> "_dtl";  default(custom_tags_dir) -> ""; -default(compiler_options) -> [report, return]; +default(compiler_options) -> [return];  default(recursive) -> true. -compile_dtl(Source, Target, DtlOpts) -> +compile_dtl(Config, Source, Target, DtlOpts) ->      case code:which(erlydtl) of          non_existing ->              ?ERROR("~n===============================================~n"                     " You need to install erlydtl to compile DTL templates~n"                     " Download the latest tarball release from github~n" -                   "    http://code.google.com/p/erlydtl/~n" +                   "    https://github.com/erlydtl/erlydtl/releases~n"                     " and install it into your erlang library dir~n"                     "===============================================~n~n", []),              ?FAIL;          _ ->              case needs_compile(Source, Target, DtlOpts) of                  true -> -                    do_compile(Source, Target, DtlOpts); +                    do_compile(Config, Source, Target, DtlOpts);                  false ->                      skipped              end      end. -do_compile(Source, Target, DtlOpts) -> +do_compile(Config, Source, Target, DtlOpts) ->      %% TODO: Check last mod on target and referenced DTLs here.. +    %% erlydtl >= 0.8.1 does not use the extra indirection using the +    %% compiler_options. Kept for backward compatibility with older +    %% versions of erlydtl. +    CompilerOptions = option(compiler_options, DtlOpts), + +    Sorted = proplists:unfold( +               lists:sort( +                 [{out_dir, option(out_dir, DtlOpts)}, +                  {doc_root, option(doc_root, DtlOpts)}, +                  {custom_tags_dir, option(custom_tags_dir, DtlOpts)}, +                  {compiler_options, CompilerOptions} +                  |CompilerOptions])), +      %% ensure that doc_root and out_dir are defined,      %% using defaults if necessary -    Opts = lists:ukeymerge(1, -            DtlOpts, -            lists:sort( -                [{out_dir, option(out_dir, DtlOpts)}, -                 {doc_root, option(doc_root, DtlOpts)}, -                 {custom_tags_dir, option(custom_tags_dir, DtlOpts)}, -                 {compiler_options, option(compiler_options, DtlOpts)}])), +    Opts = lists:ukeymerge(1, DtlOpts, Sorted),      ?INFO("Compiling \"~s\" -> \"~s\" with options:~n    ~s~n",          [Source, Target, io_lib:format("~p", [Opts])]),      case erlydtl:compile(Source,                           module_name(Target),                           Opts) of -        ok -> ok; -        {error, {File, [{Pos, _Mod, Err}]}} -> -            ?ERROR("Compiling template ~p failed:~n    (~s): ~p~n", -                [File, err_location(Pos), Err]); -        Reason -> -            ?ERROR("Compiling template ~s failed:~n  ~p~n", -                   [Source, Reason]), -            ?FAIL +        ok -> +            ok; +        {ok, _Mod} -> +            ok; +        {ok, _Mod, Ws} -> +            rebar_base_compiler:ok_tuple(Config, Source, Ws); +        {ok, _Mod, _Bin, Ws} -> +            rebar_base_compiler:ok_tuple(Config, Source, Ws); +        error -> +            rebar_base_compiler:error_tuple(Config, Source, [], [], Opts); +        {error, {_File, _Msgs} = Error} -> +            rebar_base_compiler:error_tuple(Config, Source, [Error], [], Opts); +        {error, Msg} -> +            Es = [{Source, [{erlydtl_parser, Msg}]}], +            rebar_base_compiler:error_tuple(Config, Source, Es, [], Opts); +        {error, Es, Ws} -> +            rebar_base_compiler:error_tuple(Config, Source, Es, Ws, Opts)      end. -err_location({L,C}) -> io_lib:format("line:~w, col:~w", [L, C]); -err_location(L)     -> io_lib:format("line:~w", [L]). -  module_name(Target) ->      F = filename:basename(Target),      string:substr(F, 1, length(F)-length(".beam")). diff --git a/src/rebar_eunit.erl b/src/rebar_eunit.erl index cdc0787..d969f96 100644 --- a/src/rebar_eunit.erl +++ b/src/rebar_eunit.erl @@ -84,8 +84,7 @@ eunit(Config, _AppFile) ->      ok = ensure_dirs(),      %% Save code path      CodePath = setup_code_path(), -    CompileOnly = rebar_utils:get_experimental_global(Config, compile_only, -                                                      false), +    CompileOnly = rebar_config:get_global(Config, compile_only, false),      {ok, SrcErls} = rebar_erlc_compiler:test_compile(Config, "eunit",                                                       ?EUNIT_DIR),      case CompileOnly of @@ -121,12 +120,16 @@ info_help(Description) ->         "  ~p~n"         "  ~p~n"         "Valid command line options:~n" -       "  suites=\"foo,bar\" (Run tests in foo.erl, test/foo_tests.erl and~n" +       "  suite[s]=\"foo,bar\" (Run tests in foo.erl, test/foo_tests.erl and~n"         "                    tests in bar.erl, test/bar_tests.erl)~n" -       "  tests=\"baz\" (For every existing suite, run the first test whose~n" +       "  test[s]=\"baz\" (For every existing suite, run the first test whose~n"         "               name starts with bar and, if no such test exists,~n"         "               run the test whose name starts with bar in the~n" -       "               suite's _tests module)~n", +       "               suite's _tests module)~n" +       "  random_suite_order=true (Run tests in random order)~n" +       "  random_suite_order=Seed (Run tests in random order,~n" +       "                           with the PRNG seeded with Seed)~n" +       "  compile_only=true (Compile but do not run tests)",         [          Description,          {eunit_opts, []}, @@ -150,7 +153,7 @@ run_eunit(Config, CodePath, SrcErls) ->                          AllBeamFiles),      OtherBeamFiles = TestBeamFiles --          [filename:rootname(N) ++ "_tests.beam" || N <- AllBeamFiles], -    ModuleBeamFiles = BeamFiles ++ OtherBeamFiles, +    ModuleBeamFiles = randomize_suites(Config, BeamFiles ++ OtherBeamFiles),      %% Get modules to be run in eunit      AllModules = [rebar_utils:beam_to_mod(?EUNIT_DIR, N) || N <- AllBeamFiles], @@ -215,7 +218,7 @@ setup_code_path() ->  %%  filter_suites(Config, Modules) -> -    RawSuites = rebar_config:get_global(Config, suites, ""), +    RawSuites = get_suites(Config),      SuitesProvided = RawSuites =/= "",      Suites = [list_to_atom(Suite) || Suite <- string:tokens(RawSuites, ",")],      {SuitesProvided, filter_suites1(Modules, Suites)}. @@ -223,7 +226,42 @@ filter_suites(Config, Modules) ->  filter_suites1(Modules, []) ->      Modules;  filter_suites1(Modules, Suites) -> -    [M || M <- Modules, lists:member(M, Suites)]. +    [M || M <- Suites, lists:member(M, Modules)]. + +get_suites(Config) -> +    case rebar_config:get_global(Config, suites, "") of +        "" -> +            rebar_config:get_global(Config, suite, ""); +        Suites -> +            Suites +    end. + +%% +%% == randomize suites == +%% + +randomize_suites(Config, Modules) -> +    case rebar_config:get_global(Config, random_suite_order, undefined) of +        undefined -> +            Modules; +        "true" -> +            Seed = crypto:rand_uniform(1, 65535), +            randomize_suites1(Modules, Seed); +        String -> +            try list_to_integer(String) of +                Seed -> +                    randomize_suites1(Modules, Seed) +            catch +                error:badarg -> +                    ?ERROR("Bad random seed provided: ~p~n", [String]), +                    ?FAIL +            end +    end. + +randomize_suites1(Modules, Seed) -> +    _ = random:seed(35, Seed, 1337), +    ?CONSOLE("Randomizing suite order with seed ~b~n", [Seed]), +    [X||{_,X} <- lists:sort([{random:uniform(), M} || M <- Modules])].  %%  %% == get matching tests == @@ -259,8 +297,16 @@ get_tests(Config, SuitesProvided, ModuleBeamFiles, FilteredModules) ->                end,      get_matching_tests(Config, Modules). +get_tests(Config) -> +    case rebar_config:get_global(Config, tests, "") of +        "" -> +            rebar_config:get_global(Config, test, ""); +        Suites -> +            Suites +    end. +  get_matching_tests(Config, Modules) -> -    RawFunctions = rebar_utils:get_experimental_global(Config, tests, ""), +    RawFunctions = get_tests(Config),      Tests = [list_to_atom(F1) || F1 <- string:tokens(RawFunctions, ",")],      case Tests of          [] -> @@ -408,7 +454,7 @@ perform_eunit(Config, Tests) ->  get_eunit_opts(Config) ->      %% Enable verbose in eunit if so requested.. -    BaseOpts = case rebar_config:is_verbose(Config) of +    BaseOpts = case rebar_log:is_verbose(Config) of                     true ->                         [verbose];                     false -> @@ -561,7 +607,10 @@ align_notcovered_count(Module, Covered, NotCovered, true) ->  cover_write_index(Coverage, SrcModules) ->      {ok, F} = file:open(filename:join([?EUNIT_DIR, "index.html"]), [write]), -    ok = file:write(F, "<html><head><title>Coverage Summary</title></head>\n"), +    ok = file:write(F, "<!DOCTYPE HTML><html>\n" +                        "<head><meta charset=\"utf-8\">" +                        "<title>Coverage Summary</title></head>\n" +                        "<body>\n"),      IsSrcCoverage = fun({Mod,_C,_N}) -> lists:member(Mod, SrcModules) end,      {SrcCoverage, TestCoverage} = lists:partition(IsSrcCoverage, Coverage),      cover_write_index_section(F, "Source", SrcCoverage), @@ -579,7 +628,7 @@ cover_write_index_section(F, SectionName, Coverage) ->      TotalCoverage = percentage(Covered, NotCovered),      %% Write the report -    ok = file:write(F, ?FMT("<body><h1>~s Summary</h1>\n", [SectionName])), +    ok = file:write(F, ?FMT("<h1>~s Summary</h1>\n", [SectionName])),      ok = file:write(F, ?FMT("<h3>Total: ~s</h3>\n", [TotalCoverage])),      ok = file:write(F, "<table><tr><th>Module</th><th>Coverage %</th></tr>\n"), @@ -666,7 +715,8 @@ reset_after_eunit({OldProcesses, WasAlive, OldAppEnvs, _OldACs}) ->                   end,               ok = application:unset_env(App, K)           end || App <- Apps, App /= rebar, -                {K, _V} <- application:get_all_env(App)], +                {K, _V} <- application:get_all_env(App), +                K =/= included_applications],      reconstruct_app_env_vars(Apps), @@ -798,11 +848,11 @@ pause_until_net_kernel_stopped() ->  pause_until_net_kernel_stopped(0) ->      exit(net_kernel_stop_failed);  pause_until_net_kernel_stopped(N) -> -    try -        timer:sleep(100), -        pause_until_net_kernel_stopped(N - 1) -    catch -        error:badarg -> +    case node() of +        'nonode@nohost' ->              ?DEBUG("Stopped net kernel.\n", []), -            ok +            ok; +        _ -> +            timer:sleep(100), +            pause_until_net_kernel_stopped(N - 1)      end. diff --git a/src/rebar_getopt.erl b/src/rebar_getopt.erl new file mode 100644 index 0000000..79b871d --- /dev/null +++ b/src/rebar_getopt.erl @@ -0,0 +1,914 @@ +%%%------------------------------------------------------------------- +%%% @author Juan Jose Comellas <juanjo@comellas.org> +%%% @copyright (C) 2009 Juan Jose Comellas +%%% @doc Parses command line options with a format similar to that of GNU getopt. +%%% @end +%%% +%%% This source file is subject to the New BSD License. You should have received +%%% a copy of the New BSD license with this software. If not, it can be +%%% retrieved from: http://www.opensource.org/licenses/bsd-license.php +%%%------------------------------------------------------------------- +-module(rebar_getopt). +-author('juanjo@comellas.org'). + +-export([parse/2, check/2, parse_and_check/2, format_error/2, +         usage/2, usage/3, usage/4, tokenize/1]). +-export([usage_cmd_line/2]). + +-define(LINE_LENGTH, 75). +-define(MIN_USAGE_COMMAND_LINE_OPTION_LENGTH, 25). + +%% Position of each field in the option specification tuple. +-define(OPT_NAME, 1). +-define(OPT_SHORT, 2). +-define(OPT_LONG, 3). +-define(OPT_ARG, 4). +-define(OPT_HELP, 5). + +-define(IS_OPT_SPEC(Opt), (tuple_size(Opt) =:= ?OPT_HELP)). +-define(IS_WHITESPACE(Char), ((Char) =:= $\s orelse (Char) =:= $\t orelse +                              (Char) =:= $\n orelse (Char) =:= $\r)). + +%% Atom indicating the data type that an argument can be converted to. +-type arg_type()                                :: 'atom' | 'binary' | 'boolean' | 'float' | 'integer' | 'string'. +%% Data type that an argument can be converted to. +-type arg_value()                               :: atom() | binary() | boolean() | float() | integer() | string(). +%% Argument specification. +-type arg_spec()                                :: arg_type() | {arg_type(), arg_value()} | undefined. +%% Option type and optional default argument. +-type simple_option()                           :: atom(). +-type compound_option()                         :: {atom(), arg_value()}. +-type option()                                  :: simple_option() | compound_option(). +%% Command line option specification. +-type option_spec() :: { +                   Name                         :: atom(), +                   Short                        :: char() | undefined, +                   Long                         :: string() | undefined, +                   ArgSpec                      :: arg_spec(), +                   Help                         :: string() | undefined +                  }. +%% Output streams +-type output_stream()                           :: 'standard_io' | 'standard_error'. + +%% For internal use +-type usage_line()                              :: {OptionText :: string(), HelpText :: string()}. +-type usage_line_with_length()                  :: {OptionLength :: non_neg_integer(), OptionText :: string(), HelpText :: string()}. + + +-export_type([arg_type/0, arg_value/0, arg_spec/0, simple_option/0, compound_option/0, option/0, option_spec/0]). + + +%% @doc Parse the command line options and arguments returning a list of tuples +%%      and/or atoms using the Erlang convention for sending options to a +%%      function.  Additionally perform check if all required options (the ones +%%      without default values) are present.  The function is a combination of +%%      two calls: parse/2 and check/2. +-spec parse_and_check([option_spec()], string() | [string()]) -> +                {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: term()}}. +parse_and_check(OptSpecList, CmdLine) when is_list(OptSpecList), is_list(CmdLine) -> +    case parse(OptSpecList, CmdLine) of +        {ok, {Opts, _}} = Result -> +            case check(OptSpecList, Opts) of +                ok    -> Result; +                Error -> Error +            end; +        Error -> +            Error +    end. + +%% @doc Check the parsed command line arguments returning ok if all required +%%      options (i.e. that don't have defaults) are present, and returning +%%      error otherwise. +-spec check([option_spec()], [option()]) -> +                ok | {error, {Reason :: atom(), Option :: atom()}}. +check(OptSpecList, ParsedOpts) when is_list(OptSpecList), is_list(ParsedOpts) -> +    try +        RequiredOpts = [Name || {Name, _, _, Arg, _} <- OptSpecList, +                                not is_tuple(Arg) andalso Arg =/= undefined], +        lists:foreach(fun (Option) -> +            case proplists:is_defined(Option, ParsedOpts) of +                true -> +                    ok; +                false -> +                    throw({error, {missing_required_option, Option}}) +            end +        end, RequiredOpts) +    catch +        _:Error -> +            Error +    end. + + +%% @doc Parse the command line options and arguments returning a list of tuples +%%      and/or atoms using the Erlang convention for sending options to a +%%      function. +-spec parse([option_spec()], string() | [string()]) -> +                   {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: term()}}. +parse(OptSpecList, CmdLine) when is_list(CmdLine) -> +    try +        Args = if +                   is_integer(hd(CmdLine)) -> tokenize(CmdLine); +                   true                    -> CmdLine +               end, +        parse(OptSpecList, [], [], 0, Args) +    catch +        throw: {error, {_Reason, _Data}} = Error -> +            Error +    end. + + +-spec parse([option_spec()], [option()], [string()], integer(), [string()]) -> +                   {ok, {[option()], [string()]}}. +%% Process the option terminator. +parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, ["--" | Tail]) -> +    %% Any argument present after the terminator is not considered an option. +    {ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc, Tail)}}; +%% Process long options. +parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["--" ++ OptArg = OptStr | Tail]) -> +    parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg); +%% Process short options. +parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["-" ++ ([_Char | _] = OptArg) = OptStr | Tail]) -> +    parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg); +%% Process non-option arguments. +parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail]) -> +    case find_non_option_arg(OptSpecList, ArgPos) of +        {value, OptSpec} when ?IS_OPT_SPEC(OptSpec) -> +            parse(OptSpecList, add_option_with_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos + 1, Tail); +        false -> +            parse(OptSpecList, OptAcc, [Arg | ArgAcc], ArgPos, Tail) +    end; +parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, []) -> +    %% Once we have completed gathering the options we add the ones that were +    %% not present but had default arguments in the specification. +    {ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc)}}. + + +%% @doc Format the error code returned by prior call to parse/2 or check/2. +-spec format_error([option_spec()], {error, {Reason :: atom(), Data :: term()}} | +                   {Reason :: term(), Data :: term()}) -> string(). +format_error(OptSpecList, {error, Reason}) -> +    format_error(OptSpecList, Reason); +format_error(OptSpecList, {missing_required_option, Name}) -> +    {_Name, Short, Long, _Type, _Help} = lists:keyfind(Name, 1, OptSpecList), +    lists:flatten(["missing required option: -", [Short], " (", to_string(Long), ")"]); +format_error(_OptSpecList, {invalid_option, OptStr}) -> +    lists:flatten(["invalid option: ", to_string(OptStr)]); +format_error(_OptSpecList, {invalid_option_arg, {Name, Arg}}) -> +    lists:flatten(["option \'", to_string(Name) ++ "\' has invalid argument: ", to_string(Arg)]); +format_error(_OptSpecList, {invalid_option_arg, OptStr}) -> +    lists:flatten(["invalid option argument: ", to_string(OptStr)]); +format_error(_OptSpecList, {Reason, Data}) -> +    lists:flatten([to_string(Reason), " ", to_string(Data)]). + + +%% @doc Parse a long option, add it to the option accumulator and continue +%%      parsing the rest of the arguments recursively. +%%      A long option can have the following syntax: +%%        --foo      Single option 'foo', no argument +%%        --foo=bar  Single option 'foo', argument "bar" +%%        --foo bar  Single option 'foo', argument "bar" +-spec parse_long_option([option_spec()], [option()], [string()], integer(), [string()], string(), string()) -> +                               {ok, {[option()], [string()]}}. +parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) -> +    case split_assigned_arg(OptArg) of +        {Long, Arg} -> +            %% Get option that has its argument within the same string +            %% separated by an equal ('=') character (e.g. "--port=1000"). +            parse_long_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg); + +        Long -> +            case lists:keyfind(Long, ?OPT_LONG, OptSpecList) of +                {Name, _Short, Long, undefined, _Help} -> +                    parse(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args); + +                {_Name, _Short, Long, _ArgSpec, _Help} = OptSpec -> +                    %% The option argument string is empty, but the option requires +                    %% an argument, so we look into the next string in the list. +                    %% e.g ["--port", "1000"] +                    parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec); +                false -> +                    throw({error, {invalid_option, OptStr}}) +            end +    end. + + +%% @doc Parse an option where the argument is 'assigned' in the same string using +%%      the '=' character, add it to the option accumulator and continue parsing the +%%      rest of the arguments recursively. This syntax is only valid for long options. +-spec parse_long_option_assigned_arg([option_spec()], [option()], [string()], integer(), +                                     [string()], string(), string(), string()) -> +                                            {ok, {[option()], [string()]}}. +parse_long_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg) -> +    case lists:keyfind(Long, ?OPT_LONG, OptSpecList) of +        {_Name, _Short, Long, ArgSpec, _Help} = OptSpec -> +            case ArgSpec of +                undefined -> +                    throw({error, {invalid_option_arg, OptStr}}); +                _ -> +                    parse(OptSpecList, add_option_with_assigned_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args) +            end; +        false -> +            throw({error, {invalid_option, OptStr}}) +    end. + + +%% @doc Split an option string that may contain an option with its argument +%%      separated by an equal ('=') character (e.g. "port=1000"). +-spec split_assigned_arg(string()) -> {Name :: string(), Arg :: string()} | string(). +split_assigned_arg(OptStr) -> +    split_assigned_arg(OptStr, OptStr, []). + +split_assigned_arg(_OptStr, "=" ++ Tail, Acc) -> +    {lists:reverse(Acc), Tail}; +split_assigned_arg(OptStr, [Char | Tail], Acc) -> +    split_assigned_arg(OptStr, Tail, [Char | Acc]); +split_assigned_arg(OptStr, [], _Acc) -> +    OptStr. + + +%% @doc Retrieve the argument for an option from the next string in the list of +%%      command-line parameters or set the value of the argument from the argument +%%      specification (for boolean and integer arguments), if possible. +parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec) -> +    ArgSpecType = arg_spec_type(ArgSpec), +    case Args =:= [] orelse is_implicit_arg(ArgSpecType, hd(Args)) of +        true -> +            parse(OptSpecList, add_option_with_implicit_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args); +        false -> +            [Arg | Tail] = Args, +            try +                parse(OptSpecList, [{Name, to_type(ArgSpecType, Arg)} | OptAcc], ArgAcc, ArgPos, Tail) +            catch +                error:_ -> +                    throw({error, {invalid_option_arg, {Name, Arg}}}) +            end +    end. + + +%% @doc Parse a short option, add it to the option accumulator and continue +%%      parsing the rest of the arguments recursively. +%%      A short option can have the following syntax: +%%        -a       Single option 'a', no argument or implicit boolean argument +%%        -a foo   Single option 'a', argument "foo" +%%        -afoo    Single option 'a', argument "foo" +%%        -abc     Multiple options: 'a'; 'b'; 'c' +%%        -bcafoo  Multiple options: 'b'; 'c'; 'a' with argument "foo" +%%        -aaa     Multiple repetitions of option 'a' (only valid for options with integer arguments) +-spec parse_short_option([option_spec()], [option()], [string()], integer(), [string()], string(), string()) -> +                                {ok, {[option()], [string()]}}. +parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) -> +    parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, first, OptArg). + +parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptPos, [Short | Arg]) -> +    case lists:keyfind(Short, ?OPT_SHORT, OptSpecList) of +        {Name, Short, _Long, undefined, _Help} -> +            parse_short_option(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args, OptStr, first, Arg); + +        {_Name, Short, _Long, ArgSpec, _Help} = OptSpec -> +            %% The option has a specification, so it requires an argument. +            case Arg of +                [] -> +                    %% The option argument string is empty, but the option requires +                    %% an argument, so we look into the next string in the list. +                    parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec, OptPos); + +                _ -> +                    case is_valid_arg(ArgSpec, Arg) of +                        true -> +                            parse(OptSpecList, add_option_with_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args); +                        _ -> +                            NewOptAcc = case OptPos of +                                            first -> add_option_with_implicit_arg(OptSpec, OptAcc); +                                            _     -> add_option_with_implicit_incrementable_arg(OptSpec, OptAcc) +                                        end, +                            parse_short_option(OptSpecList, NewOptAcc, ArgAcc, ArgPos, Args, OptStr, next, Arg) +                    end +            end; + +        false -> +            throw({error, {invalid_option, OptStr}}) +    end; +parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, _OptStr, _OptPos, []) -> +    parse(OptSpecList, OptAcc, ArgAcc, ArgPos, Args). + + +%% @doc Retrieve the argument for an option from the next string in the list of +%%      command-line parameters or set the value of the argument from the argument +%%      specification (for boolean and integer arguments), if possible. +parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec, OptPos) -> +    case Args =:= [] orelse is_implicit_arg(ArgSpec, hd(Args)) of +        true when OptPos =:= first -> +            parse(OptSpecList, add_option_with_implicit_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args); +        true -> +            parse(OptSpecList, add_option_with_implicit_incrementable_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args); +        false -> +            [Arg | Tail] = Args, +            try +                parse(OptSpecList, [{Name, to_type(ArgSpec, Arg)} | OptAcc], ArgAcc, ArgPos, Tail) +            catch +                error:_ -> +                    throw({error, {invalid_option_arg, {Name, Arg}}}) +            end +    end. + + +%% @doc Find the option for the discrete argument in position specified in the +%%      Pos argument. +-spec find_non_option_arg([option_spec()], integer()) -> {value, option_spec()} | false. +find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} = OptSpec | _Tail], 0) -> +    {value, OptSpec}; +find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} | Tail], Pos) -> +    find_non_option_arg(Tail, Pos - 1); +find_non_option_arg([_Head | Tail], Pos) -> +    find_non_option_arg(Tail, Pos); +find_non_option_arg([], _Pos) -> +    false. + + +%% @doc Append options that were not present in the command line arguments with +%%      their default arguments. +-spec append_default_options([option_spec()], [option()]) -> [option()]. +append_default_options([{Name, _Short, _Long, {_Type, DefaultArg}, _Help} | Tail], OptAcc) -> +    append_default_options(Tail, +                           case lists:keymember(Name, 1, OptAcc) of +                               false -> +                                   [{Name, DefaultArg} | OptAcc]; +                               _ -> +                                   OptAcc +                           end); +%% For options with no default argument. +append_default_options([_Head | Tail], OptAcc) -> +    append_default_options(Tail, OptAcc); +append_default_options([], OptAcc) -> +    OptAcc. + + +%% @doc Add an option with argument converting it to the data type indicated by the +%%      argument specification. +-spec add_option_with_arg(option_spec(), string(), [option()]) -> [option()]. +add_option_with_arg({Name, _Short, _Long, ArgSpec, _Help} = OptSpec, Arg, OptAcc) -> +    case is_valid_arg(ArgSpec, Arg) of +        true -> +            try +                [{Name, to_type(ArgSpec, Arg)} | OptAcc] +            catch +                error:_ -> +                    throw({error, {invalid_option_arg, {Name, Arg}}}) +            end; +        false -> +            add_option_with_implicit_arg(OptSpec, OptAcc) +    end. + + +%% @doc Add an option with argument that was part of an assignment expression +%%      (e.g. "--verbose=3") converting it to the data type indicated by the +%%      argument specification. +-spec add_option_with_assigned_arg(option_spec(), string(), [option()]) -> [option()]. +add_option_with_assigned_arg({Name, _Short, _Long, ArgSpec, _Help}, Arg, OptAcc) -> +    try +        [{Name, to_type(ArgSpec, Arg)} | OptAcc] +    catch +        error:_ -> +            throw({error, {invalid_option_arg, {Name, Arg}}}) +    end. + + +%% @doc Add an option that required an argument but did not have one. Some data +%%      types (boolean, integer) allow implicit or assumed arguments. +-spec add_option_with_implicit_arg(option_spec(), [option()]) -> [option()]. +add_option_with_implicit_arg({Name, _Short, _Long, ArgSpec, _Help}, OptAcc) -> +    case arg_spec_type(ArgSpec) of +        boolean -> +            %% Special case for boolean arguments: if there is no argument we +            %% set the value to 'true'. +            [{Name, true} | OptAcc]; +        integer -> +            %% Special case for integer arguments: if the option had not been set +            %% before we set the value to 1. This is needed to support options like +            %% "-v" to return something like {verbose, 1}. +            [{Name, 1} | OptAcc]; +        _ -> +            throw({error, {missing_option_arg, Name}}) +    end. + + +%% @doc Add an option with an implicit or assumed argument. +-spec add_option_with_implicit_incrementable_arg(option_spec() | arg_spec(), [option()]) -> [option()]. +add_option_with_implicit_incrementable_arg({Name, _Short, _Long, ArgSpec, _Help}, OptAcc) -> +    case arg_spec_type(ArgSpec) of +        boolean -> +            %% Special case for boolean arguments: if there is no argument we +            %% set the value to 'true'. +            [{Name, true} | OptAcc]; +        integer -> +            %% Special case for integer arguments: if the option had not been set +            %% before we set the value to 1; if not we increment the previous value +            %% the option had. This is needed to support options like "-vvv" to +            %% return something like {verbose, 3}. +            case OptAcc of +                [{Name, Count} | Tail] -> +                    [{Name, Count + 1} | Tail]; +                _ -> +                    [{Name, 1} | OptAcc] +            end; +        _ -> +            throw({error, {missing_option_arg, Name}}) +    end. + + +%% @doc Retrieve the data type form an argument specification. +-spec arg_spec_type(arg_spec()) -> arg_type() | undefined. +arg_spec_type({Type, _DefaultArg}) -> +    Type; +arg_spec_type(Type) when is_atom(Type) -> +    Type. + + +%% @doc Convert an argument string to its corresponding data type. +-spec to_type(arg_spec() | arg_type(), string()) -> arg_value(). +to_type({Type, _DefaultArg}, Arg) -> +    to_type(Type, Arg); +to_type(binary, Arg) -> +    list_to_binary(Arg); +to_type(atom, Arg) -> +    list_to_atom(Arg); +to_type(integer, Arg) -> +    list_to_integer(Arg); +to_type(float, Arg) -> +    list_to_float(Arg); +to_type(boolean, Arg) -> +    LowerArg = string:to_lower(Arg), +    case is_arg_true(LowerArg) of +        true -> +            true; +        _ -> +            case is_arg_false(LowerArg) of +                true -> +                    false; +                false -> +                    erlang:error(badarg) +            end +    end; +to_type(_Type, Arg) -> +    Arg. + + +-spec is_arg_true(string()) -> boolean(). +is_arg_true(Arg) -> +    (Arg =:= "true") orelse (Arg =:= "t") orelse +    (Arg =:= "yes") orelse (Arg =:= "y") orelse +    (Arg =:= "on") orelse (Arg =:= "enabled") orelse +    (Arg =:= "1"). + + +-spec is_arg_false(string()) -> boolean(). +is_arg_false(Arg) -> +    (Arg =:= "false") orelse (Arg =:= "f") orelse +    (Arg =:= "no") orelse (Arg =:= "n") orelse +    (Arg =:= "off") orelse (Arg =:= "disabled") orelse +    (Arg =:= "0"). + + +-spec is_valid_arg(arg_spec(), nonempty_string()) -> boolean(). +is_valid_arg({Type, _DefaultArg}, Arg) -> +    is_valid_arg(Type, Arg); +is_valid_arg(boolean, Arg) -> +    is_boolean_arg(Arg); +is_valid_arg(integer, Arg) -> +    is_non_neg_integer_arg(Arg); +is_valid_arg(float, Arg) -> +    is_non_neg_float_arg(Arg); +is_valid_arg(_Type, _Arg) -> +    true. + + +-spec is_implicit_arg(arg_spec(), nonempty_string()) -> boolean(). +is_implicit_arg({Type, _DefaultArg}, Arg) -> +    is_implicit_arg(Type, Arg); +is_implicit_arg(boolean, Arg) -> +    not is_boolean_arg(Arg); +is_implicit_arg(integer, Arg) -> +    not is_integer_arg(Arg); +is_implicit_arg(_Type, _Arg) -> +    false. + + +-spec is_boolean_arg(string()) -> boolean(). +is_boolean_arg(Arg) -> +    LowerArg = string:to_lower(Arg), +    is_arg_true(LowerArg) orelse is_arg_false(LowerArg). + + +-spec is_integer_arg(string()) -> boolean(). +is_integer_arg("-" ++ Tail) -> +    is_non_neg_integer_arg(Tail); +is_integer_arg(Arg) -> +    is_non_neg_integer_arg(Arg). + + +-spec is_non_neg_integer_arg(string()) -> boolean(). +is_non_neg_integer_arg([Head | Tail]) when Head >= $0, Head =< $9 -> +    is_non_neg_integer_arg(Tail); +is_non_neg_integer_arg([_Head | _Tail]) -> +    false; +is_non_neg_integer_arg([]) -> +    true. + + +-spec is_non_neg_float_arg(string()) -> boolean(). +is_non_neg_float_arg([Head | Tail]) when (Head >= $0 andalso Head =< $9) orelse Head =:= $. -> +    is_non_neg_float_arg(Tail); +is_non_neg_float_arg([_Head | _Tail]) -> +    false; +is_non_neg_float_arg([]) -> +    true. + + +%% @doc  Show a message on standard_error indicating the command line options and +%%       arguments that are supported by the program. +-spec usage([option_spec()], string()) -> ok. +usage(OptSpecList, ProgramName) -> +    usage(OptSpecList, ProgramName, standard_error). + + +%% @doc  Show a message on standard_error or standard_io indicating the command line options and +%%       arguments that are supported by the program. +-spec usage([option_spec()], string(), output_stream() | string()) -> ok. +usage(OptSpecList, ProgramName, OutputStream) when is_atom(OutputStream) -> +    io:format(OutputStream, "~s~n~n~s~n", +              [usage_cmd_line(ProgramName, OptSpecList), usage_options(OptSpecList)]); +%% @doc  Show a message on standard_error indicating the command line options and +%%       arguments that are supported by the program. The CmdLineTail argument +%%       is a string that is added to the end of the usage command line. +usage(OptSpecList, ProgramName, CmdLineTail) -> +    usage(OptSpecList, ProgramName, CmdLineTail, standard_error). + + +%% @doc  Show a message on standard_error or standard_io indicating the command line options and +%%       arguments that are supported by the program. The CmdLineTail argument +%%       is a string that is added to the end of the usage command line. +-spec usage([option_spec()], ProgramName :: string(), CmdLineTail :: string(), output_stream() | [{string(), string()}]) -> ok. +usage(OptSpecList, ProgramName, CmdLineTail, OutputStream) when is_atom(OutputStream) -> +    io:format(OutputStream, "~s~n~n~s~n", +              [usage_cmd_line(ProgramName, OptSpecList, CmdLineTail), usage_options(OptSpecList)]); +%% @doc  Show a message on standard_error indicating the command line options and +%%       arguments that are supported by the program. The CmdLineTail and OptionsTail +%%       arguments are a string that is added to the end of the usage command line +%%       and a list of tuples that are added to the end of the options' help lines. +usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail) -> +    usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail, standard_error). + + +%% @doc  Show a message on standard_error or standard_io indicating the command line options and +%%       arguments that are supported by the program. The CmdLineTail and OptionsTail +%%       arguments are a string that is added to the end of the usage command line +%%       and a list of tuples that are added to the end of the options' help lines. +-spec usage([option_spec()], ProgramName :: string(), CmdLineTail :: string(), +            [{OptionName :: string(), Help :: string()}], output_stream()) -> ok. +usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail, OutputStream) -> +    io:format(OutputStream, "~s~n~n~s~n", +              [usage_cmd_line(ProgramName, OptSpecList, CmdLineTail), usage_options(OptSpecList, OptionsTail)]). + + +-spec usage_cmd_line(ProgramName :: string(), [option_spec()]) -> iolist(). +usage_cmd_line(ProgramName, OptSpecList) -> +    usage_cmd_line(ProgramName, OptSpecList, ""). + +-spec usage_cmd_line(ProgramName :: string(), [option_spec()], CmdLineTail :: string()) -> iolist(). +usage_cmd_line(ProgramName, OptSpecList, CmdLineTail) -> +    Prefix = "Usage: " ++ ProgramName, +    PrefixLength = length(Prefix), +    LineLength = line_length(), +    %% Only align the command line options after the program name when there is +    %% enough room to do so (i.e. at least 25 characters). If not, show the +    %% command line options below the program name with a 2-character indentation. +    if +        (LineLength - PrefixLength) > ?MIN_USAGE_COMMAND_LINE_OPTION_LENGTH -> +            Indentation = lists:duplicate(PrefixLength, $\s), +            [FirstOptLine | OptLines] = usage_cmd_line_options(LineLength - PrefixLength, OptSpecList, CmdLineTail), +            IndentedOptLines = [[Indentation | OptLine] || OptLine <- OptLines], +            [Prefix, FirstOptLine | IndentedOptLines]; +        true -> +            IndentedOptLines = [[" " | OptLine] || OptLine <- usage_cmd_line_options(LineLength, OptSpecList, CmdLineTail)], +            [Prefix, $\n, IndentedOptLines] +    end. + + +%% @doc Return a list of the lines corresponding to the usage command line +%%      already wrapped according to the maximum MaxLineLength. +-spec usage_cmd_line_options(MaxLineLength :: non_neg_integer(), [option_spec()], CmdLineTail :: string()) -> iolist(). +usage_cmd_line_options(MaxLineLength, OptSpecList, CmdLineTail) -> +    usage_cmd_line_options(MaxLineLength, OptSpecList ++ string:tokens(CmdLineTail, " "), [], 0, []). + +usage_cmd_line_options(MaxLineLength, [OptSpec | Tail], LineAcc, LineAccLength, Acc) -> +    Option = [$\s | lists:flatten(usage_cmd_line_option(OptSpec))], +    OptionLength = length(Option), +    %% We accumulate the options in LineAcc until its length is over the +    %% maximum allowed line length. When that happens, we append the line in +    %% LineAcc to the list with all the lines in the command line (Acc). +    NewLineAccLength = LineAccLength + OptionLength, +    if +        NewLineAccLength < MaxLineLength -> +            usage_cmd_line_options(MaxLineLength, Tail, [Option | LineAcc], NewLineAccLength, Acc); +        true -> +            usage_cmd_line_options(MaxLineLength, Tail, [Option], OptionLength + 1, +                                   [lists:reverse([$\n | LineAcc]) | Acc]) +    end; +usage_cmd_line_options(MaxLineLength, [], [_ | _] = LineAcc, _LineAccLength, Acc) -> +    %% If there was a non-empty line in LineAcc when there are no more options +    %% to process, we add it to the list of lines to return. +    usage_cmd_line_options(MaxLineLength, [], [], 0, [lists:reverse(LineAcc) | Acc]); +usage_cmd_line_options(_MaxLineLength, [], [], _LineAccLength, Acc) -> +    lists:reverse(Acc). + + +-spec usage_cmd_line_option(option_spec()) -> string(). +usage_cmd_line_option({_Name, Short, _Long, undefined, _Help}) when Short =/= undefined -> +    %% For options with short form and no argument. +    [$[, $-, Short, $]]; +usage_cmd_line_option({_Name, _Short, Long, undefined, _Help}) when Long =/= undefined -> +    %% For options with only long form and no argument. +    [$[, $-, $-, Long, $]]; +usage_cmd_line_option({_Name, _Short, _Long, undefined, _Help}) -> +    []; +usage_cmd_line_option({Name, Short, Long, ArgSpec, _Help}) when is_atom(ArgSpec) -> +    %% For options with no default argument. +    if +        %% For options with short form and argument. +        Short =/= undefined -> [$[, $-, Short, $\s, $<, atom_to_list(Name), $>, $]]; +        %% For options with only long form and argument. +        Long =/= undefined  -> [$[, $-, $-, Long, $\s, $<, atom_to_list(Name), $>, $]]; +        %% For options with neither short nor long form and argument. +        true                -> [$[, $<, atom_to_list(Name), $>, $]] +    end; +usage_cmd_line_option({Name, Short, Long, ArgSpec, _Help}) when is_tuple(ArgSpec) -> +    %% For options with default argument. +    if +        %% For options with short form and default argument. +        Short =/= undefined -> [$[, $-, Short, $\s, $[, $<, atom_to_list(Name), $>, $], $]]; +        %% For options with only long form and default argument. +        Long =/= undefined  -> [$[, $-, $-, Long, $\s, $[, $<, atom_to_list(Name), $>, $], $]]; +        %% For options with neither short nor long form and default argument. +        true                -> [$[, $<, atom_to_list(Name), $>, $]] +    end; +usage_cmd_line_option(Option) when is_list(Option) -> +    %% For custom options that are added to the command line. +    Option. + + +%% @doc Return a list of help messages to print for each of the options and arguments. +-spec usage_options([option_spec()]) -> [string()]. +usage_options(OptSpecList) -> +    usage_options(OptSpecList, []). + + +%% @doc Return a list of usage lines to print for each of the options and arguments. +-spec usage_options([option_spec()], [{OptionName :: string(), Help :: string()}]) -> [string()]. +usage_options(OptSpecList, CustomHelp) -> +    %% Add the usage lines corresponding to the option specifications. +    {MaxOptionLength0, UsageLines0} = add_option_spec_help_lines(OptSpecList, 0, []), +    %% Add the custom usage lines. +    {MaxOptionLength, UsageLines} = add_custom_help_lines(CustomHelp, MaxOptionLength0, UsageLines0), +    MaxLineLength = line_length(), +    lists:reverse([format_usage_line(MaxOptionLength + 1, MaxLineLength, UsageLine) || UsageLine <- UsageLines]). + + +-spec add_option_spec_help_lines([option_spec()], PrevMaxOptionLength :: non_neg_integer(), [usage_line_with_length()]) -> +                                        {MaxOptionLength :: non_neg_integer(), [usage_line_with_length()]}. +add_option_spec_help_lines([OptSpec | Tail], PrevMaxOptionLength, Acc) -> +    OptionText = usage_option_text(OptSpec), +    HelpText = usage_help_text(OptSpec), +    {MaxOptionLength, ColsWithLength} = get_max_option_length({OptionText, HelpText}, PrevMaxOptionLength), +    add_option_spec_help_lines(Tail, MaxOptionLength, [ColsWithLength | Acc]); +add_option_spec_help_lines([], MaxOptionLength, Acc) -> +    {MaxOptionLength, Acc}. + + +-spec add_custom_help_lines([usage_line()], PrevMaxOptionLength :: non_neg_integer(), [usage_line_with_length()]) -> +                                   {MaxOptionLength :: non_neg_integer(), [usage_line_with_length()]}. +add_custom_help_lines([CustomCols | Tail], PrevMaxOptionLength, Acc) -> +    {MaxOptionLength, ColsWithLength} = get_max_option_length(CustomCols, PrevMaxOptionLength), +    add_custom_help_lines(Tail, MaxOptionLength, [ColsWithLength | Acc]); +add_custom_help_lines([], MaxOptionLength, Acc) -> +    {MaxOptionLength, Acc}. + + +-spec usage_option_text(option_spec()) -> string(). +usage_option_text({Name, undefined, undefined, _ArgSpec, _Help}) -> +    %% Neither short nor long form (non-option argument). +    "<" ++ atom_to_list(Name) ++ ">"; +usage_option_text({_Name, Short, undefined, _ArgSpec, _Help}) -> +    %% Only short form. +    [$-, Short]; +usage_option_text({_Name, undefined, Long, _ArgSpec, _Help}) -> +    %% Only long form. +    [$-, $- | Long]; +usage_option_text({_Name, Short, Long, _ArgSpec, _Help}) -> +    %% Both short and long form. +    [$-, Short, $,, $\s, $-, $- | Long]. + + +-spec usage_help_text(option_spec()) -> string(). +usage_help_text({_Name, _Short, _Long, {_ArgType, ArgValue}, [_ | _] = Help}) -> +    Help ++ " [default: " ++ default_arg_value_to_string(ArgValue) ++ "]"; +usage_help_text({_Name, _Short, _Long, _ArgSpec, Help}) -> +    Help. + + +%% @doc Calculate the maximum width of the column that shows the option's short +%%      and long form. +-spec get_max_option_length(usage_line(), PrevMaxOptionLength :: non_neg_integer()) -> +                                   {MaxOptionLength :: non_neg_integer(), usage_line_with_length()}. +get_max_option_length({OptionText, HelpText}, PrevMaxOptionLength) -> +    OptionLength = length(OptionText), +    {erlang:max(OptionLength, PrevMaxOptionLength), {OptionLength, OptionText, HelpText}}. + + +%% @doc Format the usage line that is shown for the options' usage. Each usage +%%      line has 2 columns. The first column shows the options in their short +%%      and long form. The second column shows the wrapped (if necessary) help +%%      text lines associated with each option. e.g.: +%% +%%        -h, --host  Database server host name or IP address; this is the +%%                    hostname of the server where the database is running +%%                    [default: localhost] +%%        -p, --port  Database server port [default: 1000] +%% +-spec format_usage_line(MaxOptionLength :: non_neg_integer(), MaxLineLength :: non_neg_integer(), +                        usage_line_with_length()) -> iolist(). +format_usage_line(MaxOptionLength, MaxLineLength, {OptionLength, OptionText, [_ | _] = HelpText}) +  when MaxOptionLength < (MaxLineLength div 2) -> +    %% If the width of the column where the options are shown is smaller than +    %% half the width of a console line then we show the help text line aligned +    %% next to its corresponding option, with a separation of at least 2 +    %% characters. +    [Head | Tail] = wrap_text_line(MaxLineLength - MaxOptionLength - 3, HelpText), +    FirstLineIndentation = lists:duplicate(MaxOptionLength - OptionLength + 1, $\s), +    Indentation = [$\n | lists:duplicate(MaxOptionLength + 3, $\s)], +    ["  ", OptionText, FirstLineIndentation, Head, +     [[Indentation, Line] || Line <- Tail], $\n]; +format_usage_line(_MaxOptionLength, MaxLineLength, {_OptionLength, OptionText, [_ | _] = HelpText}) -> +    %% If the width of the first column is bigger than the width of a console +    %% line, we show the help text on the next line with an indentation of 6 +    %% characters. +    HelpLines = wrap_text_line(MaxLineLength - 6, HelpText), +    ["  ", OptionText, [["\n      ", Line] || Line <- HelpLines], $\n]; +format_usage_line(_MaxOptionLength, _MaxLineLength, {_OptionLength, OptionText, _HelpText}) -> +    ["  ", OptionText, $\n]. + + +%% @doc Wrap a text line converting it into several text lines so that the +%%      length of each one of them is never over Length characters. +-spec wrap_text_line(Length :: non_neg_integer(), Text :: string()) -> [string()]. +wrap_text_line(Length, Text) -> +    wrap_text_line(Length, Text, [], 0, []). + +wrap_text_line(Length, [Char | Tail], Acc, Count, CurrentLineAcc) when Count < Length -> +    wrap_text_line(Length, Tail, Acc, Count + 1, [Char | CurrentLineAcc]); +wrap_text_line(Length, [_ | _] = Help, Acc, Count, CurrentLineAcc) -> +    %% Look for the first whitespace character in the current (reversed) line +    %% buffer to get a wrapped line. If there is no whitespace just cut the +    %% line at the position corresponding to the maximum length. +    {NextLineAcc, WrappedLine} = case string:cspan(CurrentLineAcc, " \t") of +                                     WhitespacePos when WhitespacePos < Count -> +                                         lists:split(WhitespacePos, CurrentLineAcc); +                                     _ -> +                                         {[], CurrentLineAcc} +                                 end, +    wrap_text_line(Length, Help, [lists:reverse(WrappedLine) | Acc], length(NextLineAcc), NextLineAcc); +wrap_text_line(_Length, [], Acc, _Count, [_ | _] = CurrentLineAcc) -> +    %% If there was a non-empty line when we reached the buffer, add it to the accumulator +    lists:reverse([lists:reverse(CurrentLineAcc) | Acc]); +wrap_text_line(_Length, [], Acc, _Count, _CurrentLineAcc) -> +    lists:reverse(Acc). + + +default_arg_value_to_string(Value) when is_atom(Value) -> +    atom_to_list(Value); +default_arg_value_to_string(Value) when is_binary(Value) -> +    binary_to_list(Value); +default_arg_value_to_string(Value) when is_integer(Value) -> +    integer_to_list(Value); +default_arg_value_to_string(Value) when is_float(Value) -> +    lists:flatten(io_lib:format("~w", [Value])); +default_arg_value_to_string(Value) -> +    Value. + + +%% @doc Tokenize a command line string with support for single and double +%%      quoted arguments (needed for arguments that have embedded whitespace). +%%      The function also supports the expansion of environment variables in +%%      both the Unix (${VAR}; $VAR) and Windows (%VAR%) formats. It does NOT +%%      support wildcard expansion of paths. +-spec tokenize(CmdLine :: string()) -> [nonempty_string()]. +tokenize(CmdLine) -> +    tokenize(CmdLine, [], []). + +-spec tokenize(CmdLine :: string(), Acc :: [string()], ArgAcc :: string()) -> [string()]. +tokenize([Sep | Tail], Acc, ArgAcc) when ?IS_WHITESPACE(Sep) -> +    NewAcc = case ArgAcc of +                 [_ | _] -> +                     %% Found separator: add to the list of arguments. +                     [lists:reverse(ArgAcc) | Acc]; +                 [] -> +                     %% Found separator with no accumulated argument; discard it. +                     Acc +             end, +    tokenize(Tail, NewAcc, []); +tokenize([QuotationMark | Tail], Acc, ArgAcc) when QuotationMark =:= $"; QuotationMark =:= $' -> +    %% Quoted argument (might contain spaces, tabs, etc.) +    tokenize_quoted_arg(QuotationMark, Tail, Acc, ArgAcc); +tokenize([Char | _Tail] = CmdLine, Acc, ArgAcc) when Char =:= $$; Char =:= $% -> +    %% Unix and Windows environment variable expansion: ${VAR}; $VAR; %VAR% +    {NewCmdLine, Var} = expand_env_var(CmdLine), +    tokenize(NewCmdLine, Acc, lists:reverse(Var, ArgAcc)); +tokenize([$\\, Char | Tail], Acc, ArgAcc) -> +    %% Escaped char. +    tokenize(Tail, Acc, [Char | ArgAcc]); +tokenize([Char | Tail], Acc, ArgAcc) -> +    tokenize(Tail, Acc, [Char | ArgAcc]); +tokenize([], Acc, []) -> +    lists:reverse(Acc); +tokenize([], Acc, ArgAcc) -> +    lists:reverse([lists:reverse(ArgAcc) | Acc]). + +-spec tokenize_quoted_arg(QuotationMark :: char(), CmdLine :: string(), Acc :: [string()], ArgAcc :: string()) -> [string()]. +tokenize_quoted_arg(QuotationMark, [QuotationMark | Tail], Acc, ArgAcc) -> +    %% End of quoted argument +    tokenize(Tail, Acc, ArgAcc); +tokenize_quoted_arg(QuotationMark, [$\\, Char | Tail], Acc, ArgAcc) -> +    %% Escaped char. +    tokenize_quoted_arg(QuotationMark, Tail, Acc, [Char | ArgAcc]); +tokenize_quoted_arg($" = QuotationMark, [Char | _Tail] = CmdLine, Acc, ArgAcc) when Char =:= $$; Char =:= $% -> +    %% Unix and Windows environment variable expansion (only for double-quoted arguments): ${VAR}; $VAR; %VAR% +    {NewCmdLine, Var} = expand_env_var(CmdLine), +    tokenize_quoted_arg(QuotationMark, NewCmdLine, Acc, lists:reverse(Var, ArgAcc)); +tokenize_quoted_arg(QuotationMark, [Char | Tail], Acc, ArgAcc) -> +    tokenize_quoted_arg(QuotationMark, Tail, Acc, [Char | ArgAcc]); +tokenize_quoted_arg(_QuotationMark, CmdLine, Acc, ArgAcc) -> +    tokenize(CmdLine, Acc, ArgAcc). + + +-spec expand_env_var(CmdLine :: nonempty_string()) -> {string(), string()}. +expand_env_var(CmdLine) -> +    case CmdLine of +        "${" ++ Tail -> +            expand_env_var("${", $}, Tail, []); +        "$" ++ Tail -> +            expand_env_var("$", Tail, []); +        "%" ++ Tail -> +            expand_env_var("%", $%, Tail, []) +    end. + +-spec expand_env_var(Prefix :: string(), EndMark :: char(), CmdLine :: string(), Acc :: string()) -> {string(), string()}. +expand_env_var(Prefix, EndMark, [Char | Tail], Acc) +  when (Char >= $A andalso Char =< $Z) orelse (Char >= $a andalso Char =< $z) orelse +       (Char >= $0 andalso Char =< $9) orelse (Char =:= $_) -> +    expand_env_var(Prefix, EndMark, Tail, [Char | Acc]); +expand_env_var(Prefix, EndMark, [EndMark | Tail], Acc) -> +    {Tail, get_env_var(Prefix, [EndMark], Acc)}; +expand_env_var(Prefix, _EndMark, CmdLine, Acc) -> +    {CmdLine, Prefix ++ lists:reverse(Acc)}. + + +-spec expand_env_var(Prefix :: string(), CmdLine :: string(), Acc :: string()) -> {string(), string()}. +expand_env_var(Prefix, [Char | Tail], Acc) +  when (Char >= $A andalso Char =< $Z) orelse (Char >= $a andalso Char =< $z) orelse +       (Char >= $0 andalso Char =< $9) orelse (Char =:= $_) -> +    expand_env_var(Prefix, Tail, [Char | Acc]); +expand_env_var(Prefix, CmdLine, Acc) -> +    {CmdLine, get_env_var(Prefix, "", Acc)}. + + +-spec get_env_var(Prefix :: string(), Suffix :: string(), Acc :: string()) -> string(). +get_env_var(Prefix, Suffix, [_ | _] = Acc) -> +    Name = lists:reverse(Acc), +    %% Only expand valid/existing variables. +    case os:getenv(Name) of +        false -> Prefix ++ Name ++ Suffix; +        Value -> Value +    end; +get_env_var(Prefix, Suffix, []) -> +    Prefix ++ Suffix. + + +-spec line_length() -> 0..?LINE_LENGTH. +line_length() -> +    case io:columns() of +        {ok, Columns} when Columns < ?LINE_LENGTH -> +            Columns - 1; +        _ -> +            ?LINE_LENGTH +    end. + + +-spec to_string(term()) -> string(). +to_string(List) when is_list(List) -> +    case io_lib:printable_list(List) of +        true  -> List; +        false -> io_lib:format("~p", [List]) +    end; +to_string(Atom) when is_atom(Atom) -> +    atom_to_list(Atom); +to_string(Value) -> +    io_lib:format("~p", [Value]). diff --git a/src/rebar_lfe_compiler.erl b/src/rebar_lfe_compiler.erl index 2a047d8..8488b0f 100644 --- a/src/rebar_lfe_compiler.erl +++ b/src/rebar_lfe_compiler.erl @@ -70,8 +70,8 @@ compile_lfe(Source, _Target, Config) ->                     "~n", []),              ?FAIL;          _ -> -            Opts = [{i, "include"}, {outdir, "ebin"}, return] -                ++ rebar_config:get_list(Config, erl_opts, []), +            ErlOpts = rebar_utils:erl_opts(Config), +            Opts = [{i, "include"}, {outdir, "ebin"}, return] ++ ErlOpts,              case lfe_comp:file(Source, Opts) of                  {ok, _Mod, Ws} ->                      rebar_base_compiler:ok_tuple(Config, Source, Ws); diff --git a/src/rebar_log.erl b/src/rebar_log.erl index 4108c9c..ba25332 100644 --- a/src/rebar_log.erl +++ b/src/rebar_log.erl @@ -27,8 +27,17 @@  -module(rebar_log).  -export([init/1, -         set_level/1, default_level/0, -         log/3]). +         set_level/1, +         error_level/0, +         default_level/0, +         log/3, +         log/4, +         is_verbose/1]). + +-define(ERROR_LEVEL, 0). +-define(WARN_LEVEL,  1). +-define(INFO_LEVEL,  2). +-define(DEBUG_LEVEL, 3).  %% ===================================================================  %% Public API @@ -37,35 +46,39 @@  init(Config) ->      Verbosity = rebar_config:get_global(Config, verbose, default_level()),      case valid_level(Verbosity) of -        0 -> set_level(error); -        1 -> set_level(warn); -        2 -> set_level(info); -        3 -> set_level(debug) +        ?ERROR_LEVEL -> set_level(error); +        ?WARN_LEVEL  -> set_level(warn); +        ?INFO_LEVEL  -> set_level(info); +        ?DEBUG_LEVEL -> set_level(debug)      end.  set_level(Level) ->      ok = application:set_env(rebar, log_level, Level).  log(Level, Str, Args) -> +    log(standard_io, Level, Str, Args). + +log(Device, Level, Str, Args) ->      {ok, LogLevel} = application:get_env(rebar, log_level),      case should_log(LogLevel, Level) of          true -> -            io:format(log_prefix(Level) ++ Str, Args); +            io:format(Device, log_prefix(Level) ++ Str, Args);          false ->              ok      end. -default_level() -> error_level(). +error_level() -> ?ERROR_LEVEL. +default_level() -> ?WARN_LEVEL. + +is_verbose(Config) -> +    rebar_config:get_xconf(Config, is_verbose, false).  %% ===================================================================  %% Internal functions  %% ===================================================================  valid_level(Level) -> -    erlang:max(error_level(), erlang:min(Level, debug_level())). - -error_level() -> 0. -debug_level() -> 3. +    erlang:max(?ERROR_LEVEL, erlang:min(Level, ?DEBUG_LEVEL)).  should_log(debug, _)     -> true;  should_log(info, debug)  -> false; diff --git a/src/mustache.erl b/src/rebar_mustache.erl index f6963cd..9016c0f 100644 --- a/src/mustache.erl +++ b/src/rebar_mustache.erl @@ -23,7 +23,7 @@  %% See the README at http://github.com/mojombo/mustache.erl for additional  %% documentation and usage examples. --module(mustache).  %% v0.1.0 +-module(rebar_mustache).  %% v0.1.0  -author("Tom Preston-Werner").  -export([compile/1, compile/2, render/1, render/2, render/3, get/2, get/3, escape/1, start/1]). @@ -31,6 +31,8 @@                   section_re = undefined,                   tag_re = undefined}). +-define(MUSTACHE_STR, "rebar_mustache"). +  compile(Body) when is_list(Body) ->    State = #mstate{},    CompiledTemplate = pre_compile(Body, State), @@ -108,7 +110,7 @@ compile_section(Name, Content, State) ->    Mod = State#mstate.mod,    Result = compiler(Content, State),    "fun() -> " ++ -    "case mustache:get(" ++ Name ++ ", Ctx, " ++ atom_to_list(Mod) ++ ") of " ++ +    "case " ++ ?MUSTACHE_STR ++ ":get(" ++ Name ++ ", Ctx, " ++ atom_to_list(Mod) ++ ") of " ++        "\"true\" -> " ++          Result ++ "; " ++        "\"false\" -> " ++ @@ -143,10 +145,10 @@ tag_kind(T, {K0, K1}) ->  compile_tag(none, Content, State) ->    Mod = State#mstate.mod, -  "mustache:escape(mustache:get(" ++ Content ++ ", Ctx, " ++ atom_to_list(Mod) ++ "))"; +  ?MUSTACHE_STR ++ ":escape(" ++ ?MUSTACHE_STR ++ ":get(" ++ Content ++ ", Ctx, " ++ atom_to_list(Mod) ++ "))";  compile_tag("{", Content, State) ->    Mod = State#mstate.mod, -  "mustache:get(" ++ Content ++ ", Ctx, " ++ atom_to_list(Mod) ++ ")"; +  ?MUSTACHE_STR ++ ":get(" ++ Content ++ ", Ctx, " ++ atom_to_list(Mod) ++ ")";  compile_tag("!", _Content, _State) ->    "[]". diff --git a/src/rebar_neotoma_compiler.erl b/src/rebar_neotoma_compiler.erl index 5607aa4..5549dc4 100644 --- a/src/rebar_neotoma_compiler.erl +++ b/src/rebar_neotoma_compiler.erl @@ -56,8 +56,8 @@ compile(Config, _AppFile) ->      rebar_base_compiler:run(Config, [],                              option(doc_root, NeoOpts), ".peg",                              option(out_dir, NeoOpts), -                            option(module_ext, NeoOpts) ++ ".beam", -                            fun compile_neo/3, [{check_last_mod,false}]). +                            option(module_ext, NeoOpts) ++ ".erl", +                            fun compile_neo/3, [{check_last_mod, true}]).  %% ============================================================================  %% Internal functions @@ -70,10 +70,10 @@ info(help, compile) ->         "Valid rebar.config options:~n"         "  ~p~n",         [ -        {neotom_opts, [{doc_root, "src"}, -                       {out_dir, "src"}, -                       {source_ext, ".peg"}, -                       {module_ext, ""}]} +        {neotoma_opts, [{doc_root, "src"}, +                        {out_dir, "src"}, +                        {source_ext, ".peg"}, +                        {module_ext, ""}]}         ]).  neotoma_opts(Config) -> diff --git a/src/rebar_otp_app.erl b/src/rebar_otp_app.erl index 6c52336..b3566c8 100644 --- a/src/rebar_otp_app.erl +++ b/src/rebar_otp_app.erl @@ -119,8 +119,12 @@ preprocess(Config, AppSrcFile) ->              {Config2, Vsn} = rebar_app_utils:app_vsn(Config1, AppSrcFile),              A2 = lists:keystore(vsn, 1, A1, {vsn, Vsn}), +            %% systools:make_relup/4 fails with {missing_param, registered} +            %% without a 'registered' value. +            A3 = ensure_registered(A2), +              %% Build the final spec as a string -            Spec = io_lib:format("~p.\n", [{application, AppName, A2}]), +            Spec = io_lib:format("~p.\n", [{application, AppName, A3}]),              %% Setup file .app filename and write new contents              AppFile = rebar_app_utils:app_src_to_app(AppSrcFile), @@ -205,3 +209,12 @@ validate_modules(AppName, Mods) ->  ebin_modules() ->      lists:sort([rebar_utils:beam_to_mod("ebin", N) ||                     N <- rebar_utils:beams("ebin")]). + +ensure_registered(AppData) -> +    case lists:keyfind(registered, 1, AppData) of +        false -> +            [{registered, []} | AppData]; +        {registered, _} -> +            %% We could further check whether the value is a list of atoms. +            AppData +    end. diff --git a/src/rebar_qc.erl b/src/rebar_qc.erl index 53a6f52..99d37a2 100644 --- a/src/rebar_qc.erl +++ b/src/rebar_qc.erl @@ -67,7 +67,9 @@ info(help, qc) ->         "Valid rebar.config options:~n"         "  {qc_opts, [{qc_mod, module()}, Options]}~n"         "  ~p~n" -       "  ~p~n", +       "  ~p~n" +       "Valid command line options:~n" +       "  compile_only=true (Compile but do not test properties)",         [          {qc_compile_opts, []},          {qc_first_files, []} @@ -142,8 +144,7 @@ run(Config, QC, QCOpts) ->      ok = ensure_dirs(),      CodePath = setup_codepath(), -    CompileOnly = rebar_utils:get_experimental_global(Config, compile_only, -                                                      false), +    CompileOnly = rebar_config:get_global(Config, compile_only, false),      %% Compile erlang code to ?QC_DIR, using a tweaked config      %% with appropriate defines, and include all the test modules      %% as well. diff --git a/src/rebar_reltool.erl b/src/rebar_reltool.erl index 18b0702..9f9488e 100644 --- a/src/rebar_reltool.erl +++ b/src/rebar_reltool.erl @@ -166,22 +166,32 @@ process_overlay(Config, ReltoolConfig) ->  %% providing an additional file on the command-line.  %%  overlay_vars(Config, Vars0, ReltoolConfig) -> -    BaseVars = load_vars_file(proplists:get_value(overlay_vars, ReltoolConfig)), -    OverrideVars = load_vars_file(rebar_config:get_global(Config, -                                                          overlay_vars, -                                                          undefined)), -    M = fun(_Key, _Base, Override) -> Override end, +    BaseVars = load_vars_file([proplists:get_value(overlay_vars, ReltoolConfig)]), +    OverlayVars = rebar_config:get_global(Config, overlay_vars, []), +    OverrideVars = load_vars_file(string:tokens(OverlayVars, ",")), +    M = fun merge_overlay_var/3,       dict:merge(M, dict:merge(M, Vars0, BaseVars), OverrideVars). +merge_overlay_var(_Key, _Base, Override) -> Override. +  %%  %% If a filename is provided, construct a dict of terms  %% -load_vars_file(undefined) -> +load_vars_file([undefined]) -> +    dict:new(); +load_vars_file([]) ->      dict:new(); -load_vars_file(File) -> +load_vars_file(Files) -> +    load_vars_file(Files, dict:new()). + +load_vars_file([], Dict) -> +    Dict; +load_vars_file([File | Files], BaseVars) ->      case rebar_config:consult_file(File) of          {ok, Terms} -> -            dict:from_list(Terms); +            OverrideVars = dict:from_list(Terms), +            M = fun merge_overlay_var/3, +            load_vars_file(Files, dict:merge(M, BaseVars, OverrideVars));          {error, Reason} ->              ?ABORT("Unable to load overlay_vars from ~p: ~p\n", [File, Reason])      end. diff --git a/src/rebar_templater.erl b/src/rebar_templater.erl index e997975..43bb8da 100644 --- a/src/rebar_templater.erl +++ b/src/rebar_templater.erl @@ -27,6 +27,7 @@  -module(rebar_templater).  -export(['create-app'/2, +         'create-lib'/2,           'create-node'/2,           'list-templates'/2,           create/2]). @@ -50,6 +51,10 @@      %% Alias for create w/ template=simpleapp      create1(Config, "simpleapp"). +'create-lib'(Config, _File) -> +    %% Alias for create w/ template=simplelib +    create1(Config, "simplelib"). +  'create-node'(Config, _File) ->      %% Alias for create w/ template=simplenode      create1(Config, "simplenode"). @@ -98,7 +103,7 @@ render(Bin, Context) ->      ReOpts = [global, {return, list}],      Str0 = re:replace(Bin, "\\\\", "\\\\\\", ReOpts),      Str1 = re:replace(Str0, "\"", "\\\\\"", ReOpts), -    mustache:render(Str1, Context). +    rebar_mustache:render(Str1, Context).  %% ===================================================================  %% Internal functions @@ -116,6 +121,12 @@ info(help, 'create-app') ->         "~n"         "Valid command line options:~n"         "  [appid=myapp]~n", []); +info(help, 'create-lib') -> +    ?CONSOLE( +       "Create simple lib skel.~n" +       "~n" +       "Valid command line options:~n" +       "  [libid=mylib]~n", []);  info(help, 'create-node') ->      ?CONSOLE(         "Create simple node skel.~n" @@ -347,6 +358,10 @@ write_file(Output, Data, Force) ->              {error, exists}      end. +prepend_instructions(Instructions, Rest) when is_list(Instructions) -> +    Instructions ++ Rest; +prepend_instructions(Instruction, Rest) -> +    [Instruction|Rest].  %%  %% Execute each instruction in a template definition file. @@ -364,6 +379,23 @@ execute_template(_Files, [], _TemplateType, _TemplateName,              ?ERROR("One or more files already exist on disk and "                     "were not generated:~n~s~s", [Msg , Help])      end; +execute_template(Files, [{'if', Cond, True} | Rest], TemplateType, +                 TemplateName, Context, Force, ExistingFiles) -> +    execute_template(Files, [{'if', Cond, True, []}|Rest], TemplateType, +                     TemplateName, Context, Force, ExistingFiles); +execute_template(Files, [{'if', Cond, True, False} | Rest], TemplateType, +                 TemplateName, Context, Force, ExistingFiles) -> +    Instructions = case dict:find(Cond, Context) of +                       {ok, true} -> +                           True; +                       {ok, "true"} -> +                           True; +                       _ -> +                           False +                   end, +    execute_template(Files, prepend_instructions(Instructions, Rest), +                     TemplateType, TemplateName, Context, Force, +                     ExistingFiles);  execute_template(Files, [{template, Input, Output} | Rest], TemplateType,                   TemplateName, Context, Force, ExistingFiles) ->      InputName = filename:join(filename:dirname(TemplateName), Input), diff --git a/src/rebar_upgrade.erl b/src/rebar_upgrade.erl index d18603c..3a38a08 100644 --- a/src/rebar_upgrade.erl +++ b/src/rebar_upgrade.erl @@ -87,7 +87,8 @@ info(help, 'generate-upgrade') ->      ?CONSOLE("Build an upgrade package.~n"               "~n"               "Valid command line options:~n" -             "  previous_release=path~n", +             "  previous_release=path~n" +             "  target_dir=target_dir (optional)~n",               []).  run_checks(Config, OldVerPath, ReltoolConfig) -> @@ -97,10 +98,7 @@ run_checks(Config, OldVerPath, ReltoolConfig) ->      {Name, Ver} = rebar_rel_utils:get_reltool_release_info(ReltoolConfig), -    NewVerPath = -        filename:join( -          [rebar_rel_utils:get_target_parent_dir(Config, ReltoolConfig), -           Name]), +    NewVerPath = rebar_rel_utils:get_target_dir(Config, ReltoolConfig),      true = rebar_utils:prop_check(filelib:is_dir(NewVerPath),                                    "Release directory doesn't exist (~p)~n",                                    [NewVerPath]), @@ -184,13 +182,24 @@ boot_files(TargetDir, Ver, Name) ->            filename:join([TargetDir, "releases", Ver, "start_clean.boot"]),            filename:join([".", ?TMP, "releases", Ver, "start_clean.boot"])), -    {ok, _} = file:copy( -                filename:join([TargetDir, "releases", Ver, "sys.config"]), -                filename:join([".", ?TMP, "releases", Ver, "sys.config"])), - -    {ok, _} = file:copy( -                filename:join([TargetDir, "releases", Ver, "vm.args"]), -                filename:join([".", ?TMP, "releases", Ver, "vm.args"])). +    SysConfig = filename:join([TargetDir, "releases", Ver, "sys.config"]), +    _ = case filelib:is_regular(SysConfig) of +            true -> +                {ok, _} = file:copy( +                            SysConfig, +                            filename:join([".", ?TMP, "releases", Ver, +                                           "sys.config"])); +            false -> ok +        end, + +    VmArgs = filename:join([TargetDir, "releases", Ver, "vm.args"]), +    case filelib:is_regular(VmArgs) of +        true -> +            {ok, _} = file:copy( +                        VmArgs, +                        filename:join([".", ?TMP, "releases", Ver, "vm.args"])); +        false -> {ok, 0} +    end.  make_tar(NameVer, NewVer, NewName) ->      Filename = NameVer ++ ".tar.gz", diff --git a/src/rebar_utils.erl b/src/rebar_utils.erl index fd93f98..2d227b6 100644 --- a/src/rebar_utils.erl +++ b/src/rebar_utils.erl @@ -52,6 +52,7 @@           erl_opts/1,           src_dirs/1,           ebin_dir/0, +         base_dir/1,           processing_base_dir/1, processing_base_dir/2]).  -include("rebar.hrl"). @@ -108,6 +109,7 @@ sh(Command0, Options0) ->      Command = patch_on_windows(Command0, proplists:get_value(env, Options, [])),      PortSettings = proplists:get_all_values(port_settings, Options) ++          [exit_status, {line, 16384}, use_stdio, stderr_to_stdout, hide], +    ?DEBUG("Port Cmd: ~p\nPort Opts: ~p\n", [Command, PortSettings]),      Port = open_port({spawn, Command}, PortSettings),      case sh_loop(Port, OutputHandler, []) of @@ -191,20 +193,20 @@ expand_env_variable(InStr, VarName, RawVarValue) ->              %% No variables to expand              InStr;          _ -> -            VarValue = re:replace(RawVarValue, "\\\\", "\\\\\\\\", [global]), +            ReOpts = [global, unicode, {return, list}], +            VarValue = re:replace(RawVarValue, "\\\\", "\\\\\\\\", ReOpts),              %% Use a regex to match/replace:              %% Given variable "FOO": match $FOO\s | $FOOeol | ${FOO}              RegEx = io_lib:format("\\\$(~s(\\s|$)|{~s})", [VarName, VarName]), -            ReOpts = [global, {return, list}, unicode],              re:replace(InStr, RegEx, [VarValue, "\\2"], ReOpts)      end. -vcs_vsn(Config, Vcs, Dir) -> -    Key = {Vcs, Dir}, +vcs_vsn(Config, Vsn, Dir) -> +    Key = {Vsn, Dir},      Cache = rebar_config:get_xconf(Config, vsn_cache),      case dict:find(Key, Cache) of          error -> -            VsnString = vcs_vsn_1(Vcs, Dir), +            VsnString = vcs_vsn_1(Vsn, Dir),              Cache1 = dict:store(Key, VsnString, Cache),              Config1 = rebar_config:set_xconf(Config, vsn_cache, Cache1),              {Config1, VsnString}; @@ -306,12 +308,15 @@ src_dirs(SrcDirs) ->  ebin_dir() ->      filename:join(get_cwd(), "ebin"). +base_dir(Config) -> +    rebar_config:get_xconf(Config, base_dir). +  processing_base_dir(Config) ->      Cwd = rebar_utils:get_cwd(),      processing_base_dir(Config, Cwd).  processing_base_dir(Config, Dir) -> -    Dir =:= rebar_config:get_xconf(Config, base_dir). +    Dir =:= base_dir(Config).  %% ====================================================================  %% Internal functions @@ -440,11 +445,12 @@ emulate_escript_foldl(Fun, Acc, File) ->  vcs_vsn_1(Vcs, Dir) ->      case vcs_vsn_cmd(Vcs) of -        {unknown, VsnString} -> -            ?DEBUG("vcs_vsn: Unknown VCS atom in vsn field: ~p\n", [Vcs]), +        {plain, VsnString} ->              VsnString;          {cmd, CmdString} ->              vcs_vsn_invoke(CmdString, Dir); +        unknown -> +            ?ABORT("vcs_vsn: Unknown vsn format: ~p\n", [Vcs]);          Cmd ->              %% If there is a valid VCS directory in the application directory,              %% use that version info @@ -477,7 +483,8 @@ vcs_vsn_cmd(bzr)    -> "bzr revno";  vcs_vsn_cmd(svn)    -> "svnversion";  vcs_vsn_cmd(fossil) -> "fossil info";  vcs_vsn_cmd({cmd, _Cmd}=Custom) -> Custom; -vcs_vsn_cmd(Version) -> {unknown, Version}. +vcs_vsn_cmd(Version) when is_list(Version) -> {plain, Version}; +vcs_vsn_cmd(_) -> unknown.  vcs_vsn_invoke(Cmd, Dir) ->      {ok, VsnString} = rebar_utils:sh(Cmd, [{cd, Dir}, {use_stdout, false}]), diff --git a/src/rebar_xref.erl b/src/rebar_xref.erl index 8c0a872..16e8cc4 100644 --- a/src/rebar_xref.erl +++ b/src/rebar_xref.erl @@ -51,7 +51,7 @@ xref(Config, _) ->      xref:set_default(xref, [{warnings,                               rebar_config:get(Config, xref_warnings, false)}, -                            {verbose, rebar_config:is_verbose(Config)}]), +                            {verbose, rebar_log:is_verbose(Config)}]),      {ok, _} = xref:add_directory(xref, "ebin"), @@ -60,27 +60,20 @@ xref(Config, _) ->      true = code:add_path(rebar_utils:ebin_dir()),      %% Get list of xref checks we want to run -    XrefChecks = rebar_config:get(Config, xref_checks, -                                  [exports_not_used, -                                   undefined_function_calls]), - -    %% Look for exports that are unused by anything -    ExportsNoWarn = -        case lists:member(exports_not_used, XrefChecks) of -            true -> -                check_exports_not_used(); -            false -> -                true -        end, +    ConfXrefChecks = rebar_config:get(Config, xref_checks, +                                      [exports_not_used, +                                       undefined_function_calls]), -    %% Look for calls to undefined functions -    UndefNoWarn = -        case lists:member(undefined_function_calls, XrefChecks) of -            true -> -                check_undefined_function_calls(); -            false -> -                true -        end, +    SupportedXrefs = [undefined_function_calls, undefined_functions, +                      locals_not_used, exports_not_used, +                      deprecated_function_calls, deprecated_functions], + +    XrefChecks = sets:to_list(sets:intersection( +                                sets:from_list(SupportedXrefs), +                                sets:from_list(ConfXrefChecks))), + +    %% Run xref checks +    XrefNoWarn = xref_checks(XrefChecks),      %% Run custom queries      QueryChecks = rebar_config:get(Config, xref_queries, []), @@ -92,7 +85,7 @@ xref(Config, _) ->      %% Stop xref      stopped = xref:stop(xref), -    case lists:member(false, [ExportsNoWarn, UndefNoWarn, QueryNoWarn]) of +    case lists:member(false, [XrefNoWarn, QueryNoWarn]) of          true ->              ?FAIL;          false -> @@ -110,35 +103,31 @@ info(help, xref) ->         "Valid rebar.config options:~n"         "  ~p~n"         "  ~p~n" +       "  ~p~n"         "  ~p~n",         [          {xref_warnings, false}, -        {xref_checks, [exports_not_used, undefined_function_calls]}, +        {xref_extra_paths,[]}, +        {xref_checks, [undefined_function_calls, undefined_functions, +                       locals_not_used, exports_not_used, +                       deprecated_function_calls, deprecated_functions]},          {xref_queries,           [{"(xc - uc) || (xu - x - b"             " - (\"mod\":\".*foo\"/\"4\"))",[]}]}         ]). -check_exports_not_used() -> -    {ok, UnusedExports0} = xref:analyze(xref, exports_not_used), -    UnusedExports = filter_away_ignored(UnusedExports0), - -    %% Report all the unused functions -    display_mfas(UnusedExports, "is unused export (Xref)"), -    UnusedExports =:= []. +xref_checks(XrefChecks) -> +    XrefWarnCount = lists:foldl(fun run_xref_check/2, 0, XrefChecks), +    XrefWarnCount =:= 0. -check_undefined_function_calls() -> -    {ok, UndefinedCalls0} = xref:analyze(xref, undefined_function_calls), -    UndefinedCalls = -        [{find_mfa_source(Caller), format_fa(Caller), format_mfa(Target)} -         || {Caller, Target} <- UndefinedCalls0], - -    lists:foreach( -      fun({{Source, Line}, FunStr, Target}) -> -              ?CONSOLE("~s:~w: Warning ~s calls undefined function ~s\n", -                       [Source, Line, FunStr, Target]) -      end, UndefinedCalls), -    UndefinedCalls =:= []. +run_xref_check(XrefCheck, Acc) -> +    {ok, Results} = xref:analyze(xref, XrefCheck), +    FilteredResults =filter_xref_results(XrefCheck, Results), +    lists:foreach(fun(Res) -> +                          display_xref_result(XrefCheck, Res) +                  end, +                  FilteredResults), +    Acc + length(FilteredResults).  check_query({Query, Value}) ->      {ok, Answer} = xref:q(xref, Query), @@ -157,8 +146,9 @@ code_path(Config) ->      %% functions, even though those functions are present as part      %% of compilation. H/t to @dluna. Long term we should tie more      %% properly into the overall compile code path if possible. -    BaseDir = rebar_config:get_xconf(Config, base_dir), +    BaseDir = rebar_utils:base_dir(Config),      [P || P <- code:get_path() ++ +              rebar_config:get(Config, xref_extra_paths, []) ++                [filename:join(BaseDir, filename:join(SubDir, "ebin"))                 || SubDir <- rebar_config:get(Config, sub_dirs, [])],            filelib:is_dir(P)]. @@ -166,41 +156,98 @@ code_path(Config) ->  %%  %% Ignore behaviour functions, and explicitly marked functions  %% -filter_away_ignored(UnusedExports) -> -    %% Functions can be ignored by using -    %% -ignore_xref([{F, A}, ...]). - -    %% Setup a filter function that builds a list of behaviour callbacks and/or -    %% any functions marked to ignore. We then use this list to mask any -    %% functions marked as unused exports by xref -    F = fun(Mod) -> -                Attrs  = Mod:module_info(attributes), -                Ignore = keyall(ignore_xref, Attrs), -                Callbacks = [B:behaviour_info(callbacks) -                             || B <- keyall(behaviour, Attrs)], -                [{Mod, F, A} || {F, A} <- Ignore ++ lists:flatten(Callbacks)] +%% Functions can be ignored by using +%% -ignore_xref([{F, A}, {M, F, A}...]). + +get_xref_ignorelist(Mod, XrefCheck) -> +    %% Get ignore_xref attribute and combine them in one list +    Attributes = +        try +            Mod:module_info(attributes) +        catch +            _Class:_Error -> []          end, -    AttrIgnore = -        lists:flatten( -          lists:map(F, lists:usort([M || {M, _, _} <- UnusedExports]))), -    [X || X <- UnusedExports, not lists:member(X, AttrIgnore)]. + +    IgnoreXref = keyall(ignore_xref, Attributes), + +    BehaviourCallbacks = get_behaviour_callbacks(XrefCheck, Attributes), + +    %% And create a flat {M,F,A} list +    lists:foldl( +      fun({F, A}, Acc) -> [{Mod,F,A} | Acc]; +         ({M, F, A}, Acc) -> [{M,F,A} | Acc] +      end, [], lists:flatten([IgnoreXref, BehaviourCallbacks])).  keyall(Key, List) ->      lists:flatmap(fun({K, L}) when Key =:= K -> L; (_) -> [] end, List). -display_mfas([], _Message) -> -    ok; -display_mfas([{_Mod, Fun, Args} = MFA | Rest], Message) -> -    {Source, Line} = find_mfa_source(MFA), -    ?CONSOLE("~s:~w: Warning: function ~s/~w ~s\n", -             [Source, Line, Fun, Args, Message]), -    display_mfas(Rest, Message). +get_behaviour_callbacks(exports_not_used, Attributes) -> +    [B:behaviour_info(callbacks) || B <- keyall(behaviour, Attributes)]; +get_behaviour_callbacks(_XrefCheck, _Attributes) -> +    []. + +parse_xref_result({_, MFAt}) -> MFAt; +parse_xref_result(MFAt) -> MFAt. + +filter_xref_results(XrefCheck, XrefResults) -> +    SearchModules = lists:usort( +                      lists:map( +                        fun({Mt,_Ft,_At}) -> Mt; +                           ({{Ms,_Fs,_As},{_Mt,_Ft,_At}}) -> Ms; +                           (_) -> undefined +                        end, XrefResults)), + +    Ignores = lists:flatmap(fun(Module) -> +                                    get_xref_ignorelist(Module, XrefCheck) +                            end, SearchModules), + +    [Result || Result <- XrefResults, +               not lists:member(parse_xref_result(Result), Ignores)]. + +display_xref_result(Type, XrefResult) -> +    { Source, SMFA, TMFA } = case XrefResult of +                                 {MFASource, MFATarget} -> +                                     {format_mfa_source(MFASource), +                                      format_mfa(MFASource), +                                      format_mfa(MFATarget)}; +                                 MFATarget -> +                                     {format_mfa_source(MFATarget), +                                      format_mfa(MFATarget), +                                      undefined} +                             end, +    case Type of +        undefined_function_calls -> +            ?CONSOLE("~sWarning: ~s calls undefined function ~s (Xref)\n", +                     [Source, SMFA, TMFA]); +        undefined_functions -> +            ?CONSOLE("~sWarning: ~s is undefined function (Xref)\n", +                     [Source, SMFA]); +        locals_not_used -> +            ?CONSOLE("~sWarning: ~s is unused local function (Xref)\n", +                     [Source, SMFA]); +        exports_not_used -> +            ?CONSOLE("~sWarning: ~s is unused export (Xref)\n", +                     [Source, SMFA]); +        deprecated_function_calls -> +            ?CONSOLE("~sWarning: ~s calls deprecated function ~s (Xref)\n", +                     [Source, SMFA, TMFA]); +        deprecated_functions -> +            ?CONSOLE("~sWarning: ~s is deprecated function (Xref)\n", +                     [Source, SMFA]); +        Other -> +            ?CONSOLE("~sWarning: ~s - ~s xref check: ~s (Xref)\n", +                     [Source, SMFA, TMFA, Other]) +    end.  format_mfa({M, F, A}) ->      ?FMT("~s:~s/~w", [M, F, A]). -format_fa({_M, F, A}) -> -    ?FMT("~s/~w", [F, A]). +format_mfa_source(MFA) -> +    case find_mfa_source(MFA) of +        {module_not_found, function_not_found} -> ""; +        {Source, function_not_found} -> ?FMT("~s: ", [Source]); +        {Source, Line} -> ?FMT("~s:~w: ", [Source, Line]) +    end.  %%  %% Extract an element from a tuple, or undefined if N > tuple size @@ -213,14 +260,18 @@ safe_element(N, Tuple) ->              Value      end. -  %%  %% Given a MFA, find the file and LOC where it's defined. Note that  %% xref doesn't work if there is no abstract_code, so we can avoid  %% being too paranoid here.  %%  find_mfa_source({M, F, A}) -> -    {M, Bin, _} = code:get_object_code(M), +    case code:get_object_code(M) of +        error -> {module_not_found, function_not_found}; +        {M, Bin, _} -> find_function_source(M,F,A,Bin) +    end. + +find_function_source(M, F, A, Bin) ->      AbstractCode = beam_lib:chunks(Bin, [abstract_code]),      {ok, {M, [{abstract_code, {raw_abstract_v1, Code}}]}} = AbstractCode,      %% Extract the original source filename from the abstract code | 
