aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile38
-rw-r--r--README.md4
-rw-r--r--rebar.config8
-rw-r--r--src/rcl_cmd_args.erl125
-rw-r--r--src/rcl_goal_utils.erl4
-rw-r--r--src/rcl_log.erl87
-rw-r--r--src/rcl_state.erl56
-rw-r--r--src/rcl_util.erl9
-rw-r--r--src/relcool.erl102
-rw-r--r--test/rclt_command_SUITE.erl100
11 files changed, 476 insertions, 58 deletions
diff --git a/.gitignore b/.gitignore
index 33a87e2..61eb48f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ ebin/*
relcool
# This is a generated file that should be ignored
src/rcl_goal.erl
+logs \ No newline at end of file
diff --git a/Makefile b/Makefile
index db7d373..ef6fbe5 100644
--- a/Makefile
+++ b/Makefile
@@ -14,9 +14,20 @@
# specific language governing permissions and limitations
# under the License.
#
+
+
+ERLFLAGS= -pa $(CURDIR)/.eunit -pa $(CURDIR)/ebin -pa $(CURDIR)/deps/*/ebin
+
+RELCOOL_PLT=$(CURDIR)/.relcool_plt
+
+# =============================================================================
+# Verify that the programs we need to run are installed on this system
+# =============================================================================
ERL = $(shell which erl)
-ERLFLAGS= -pa $(CURDIR)/.eunit -pa $(CURDIR)/ebin -pa $(CURDIR)/deps/**/ebin
+ifeq ($(ERL),)
+$(error "Erlang not available on this system")
+endif
REBAR=$(shell which rebar)
@@ -24,11 +35,12 @@ ifeq ($(REBAR),)
$(error "Rebar not available on this system")
endif
-RELCOOL_PLT=$(CURDIR)/.relcool_plt
-
-.PHONY: all compile doc clean eunit dialyzer typer shell distclean pdf get-deps escript
+# =============================================================================
+# Rules to build the system
+# =============================================================================
+.PHONY: all compile doc clean test dialyzer typer shell distclean pdf get-deps escript
-all: compile eunit dialyzer
+all: compile escript test dialyzer
get-deps:
$(REBAR) get-deps
@@ -46,6 +58,11 @@ doc:
eunit: compile
$(REBAR) skip_deps=true eunit
+ct: compile
+ $(REBAR) skip_deps=true ct
+
+test: compile eunit ct
+
$(RELCOOL_PLT):
@echo Building local plt at $(RELCOOL_PLT)
@echo
@@ -53,7 +70,7 @@ $(RELCOOL_PLT):
--apps erts kernel stdlib -r deps
dialyzer: $(RELCOOL_PLT)
- dialyzer --plt $(RELCOOL_PLT) -Wrace_conditions --src src
+ dialyzer --plt $(RELCOOL_PLT) -Wrace_conditions -I include -pa $(CURDIR)/ebin --src src
typer:
typer --plt $(RELCOOL_PLT) -r ./src
@@ -71,8 +88,11 @@ pdf:
pandoc README.md -o README.pdf
clean:
- $(REBAR) clean
+ - rm -rf $(CURDIR)/test/*.beam
+ - rm -rf $(CURDIR)/test/*_SUITE_data
+ - rm -rf $(CURDIR)/logs
+ $(REBAR) skip_deps=true clean
distclean: clean
- rm -rf $(RELCOOL_PLT)
- rm -rvf $(CURDIR)/deps/*
+ - rm -rf $(RELCOOL_PLT)
+ - rm -rvf $(CURDIR)/deps/*
diff --git a/README.md b/README.md
index f2d3899..b38b860 100644
--- a/README.md
+++ b/README.md
@@ -27,8 +27,8 @@ additional specification information for releases.
-v *STRING*, \--relvsn=*STRING*
: Specify the version for the release
--t *STRING*, \--target-spec *STRING*
-: Specify a target constraint on the system. These are usually the OTP
+-g *STRING*, \--goals *STRING*
+: Specify a goal to the system. These are usually the OTP
Apps that are part of the release
-o *STRING*, \--output-dir *STRING*
diff --git a/rebar.config b/rebar.config
index e31e544..289db92 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,15 +1,15 @@
%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
{deps, [{neotoma, "",
- {git, "https://github.com/seancribbs/neotoma.git", {tag, "1.5"}}},
+ {git, "https://github.com/ericbmerritt/neotoma.git", {tag, "1.5.1"}}},
{erlware_commons, "",
- {git, "https://github.com/ericbmerritt/erlware_commons.git", {tag, "v0.7.0"}}},
+ {git, "https://github.com/ericbmerritt/erlware_commons.git", {tag, "v0.8.0"}}},
{getopt, "",
{git, "https://github.com/jcomellas/getopt.git", {tag, "v0.5.1"}}},
{depsolver, "",
- {git, "https://github.com/ericbmerritt/depsolver.git", {tag, "v0.0.2"}}}]}.
+ {git, "https://github.com/ericbmerritt/depsolver.git", {tag, "v0.1.0"}}}]}.
{escript_incl_apps,
- [getopt, depsolver]}.
+ [getopt, depsolver, erlware_commons]}.
{erl_opts, [debug_info, warnings_as_errors, inline]}.
diff --git a/src/rcl_cmd_args.erl b/src/rcl_cmd_args.erl
new file mode 100644
index 0000000..454e762
--- /dev/null
+++ b/src/rcl_cmd_args.erl
@@ -0,0 +1,125 @@
+%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
+%%%
+%%% This file is provided to you under the Apache License,
+%%% Version 2.0 (the "License"); you may not use this file
+%%% except in compliance with the License. You may obtain
+%%% a copy of the License at
+%%%
+%%% http://www.apache.org/licenses/LICENSE-2.0
+%%%
+%%% Unless required by applicable law or agreed to in writing,
+%%% software distributed under the License is distributed on an
+%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%%% KIND, either express or implied. See the License for the
+%%% specific language governing permissions and limitations
+%%% under the License.
+%%%---------------------------------------------------------------------------
+%%% @author Eric Merritt <[email protected]>
+%%% @copyright (C) 2012 Erlware, LLC.
+%%%
+%%% @doc Trivial utility file to help handle common tasks
+-module(rcl_cmd_args).
+
+-export([args2state/1]).
+
+%%============================================================================
+%% types
+%%============================================================================
+
+
+%%============================================================================
+%% API
+%%============================================================================
+-spec args2state({error, Reason::term()} | {[getopt:option()], [string()]}) ->
+ {error, Reason::term()} |
+ {ok, {rcl_state:t(), [string()]}}.
+args2state(Error={error, _}) ->
+ Error;
+args2state({ok, {Opts, Targets}}) ->
+ RelName = proplists:get_value(relname, Opts, undefined),
+ RelVsn = proplists:get_value(relvsn, Opts, undefined),
+ case create_log(Opts,
+ [{relname, RelName},
+ {relvsn, RelVsn}]) of
+ Error = {error, _} ->
+ Error;
+ {ok, CommandLineConfig} ->
+ {ok, {rcl_state:new(CommandLineConfig), Targets}}
+ end.
+
+%%%===================================================================
+%%% Internal Functions
+%%%===================================================================
+
+-spec create_log([getopt:option()], rcl_state:cmd_args()) ->
+ {ok, rcl_state:cmd_args()} | {error, Reason::term()}.
+create_log(Opts, Acc) ->
+ LogLevel = proplists:get_value(log_level, Opts, 0),
+ if
+ LogLevel >= 0, LogLevel =< 2 ->
+ create_goals(Opts, [{log, rcl_log:new(LogLevel)} | Acc]);
+ true ->
+ {error, {invalid_log_level, LogLevel}}
+ end.
+
+-spec create_goals([getopt:option()], rcl_state:cmd_args()) ->
+ {ok, rcl_state:cmd_args()} | {error, Reason::term()}.
+create_goals(Opts, Acc) ->
+ case convert_goals(proplists:get_all_values(goals, Opts), []) of
+ Error={error, {failed_to_parse, _Spec}} ->
+ Error;
+ {ok, Specs} ->
+ create_output_dir(Opts, [{goals, Specs} | Acc])
+ end.
+
+-spec convert_goals([string()], [depsolver:constraint()]) ->
+ {error,{failed_to_parse, string()}} |
+ {ok,[depsolver:constraint()]}.
+convert_goals([], Specs) ->
+ %% Reverse the specs because order matters to depsolver
+ {ok, lists:reverse(Specs)};
+convert_goals([RawSpec | Rest], Acc) ->
+ case rcl_goal:parse(RawSpec) of
+ {ok, Spec} ->
+ convert_goals(Rest, [Spec | Acc]);
+ {fail, _} ->
+ {error, {failed_to_parse, RawSpec}}
+ end.
+-spec create_output_dir([getopt:option()], rcl_state:cmd_args()) ->
+ {ok, rcl_state:cmd_args()} | {error, Reason::term()}.
+create_output_dir(Opts, Acc) ->
+ OutputDir = proplists:get_value(output_dir, Opts, "./relcool_output"),
+ case filelib:is_dir(OutputDir) of
+ false ->
+ case rcl_util:mkdir_p(OutputDir) of
+ ok ->
+ create_lib_dirs(Opts, [{output_dir, OutputDir} | Acc]);
+ {error, _} ->
+ {error, {unable_to_create_output_dir, OutputDir}}
+ end;
+ true ->
+ create_lib_dirs(Opts, [{output_dir, OutputDir} | Acc])
+ end.
+
+-spec create_lib_dirs([getopt:option()], rcl_state:cmd_args()) ->
+ {ok, rcl_state:cmd_args()} | {error, Reason::term()}.
+create_lib_dirs(Opts, Acc) ->
+ Dirs = proplists:get_all_values(lib_dir, Opts),
+ case check_lib_dirs(Dirs) of
+ Error = {error, _} ->
+ Error;
+ ok ->
+ {ok, [{lib_dirs, Dirs} | Acc]}
+ end.
+
+-spec check_lib_dirs([string()]) -> ok | {error, {Reason::atom(), Dir::string()}}.
+check_lib_dirs([]) ->
+ ok;
+check_lib_dirs([Dir | Rest]) ->
+ case filelib:is_dir(Dir) of
+ false ->
+ {error, {not_directory, Dir}};
+ true ->
+ check_lib_dirs(Rest)
+ end.
diff --git a/src/rcl_goal_utils.erl b/src/rcl_goal_utils.erl
index 75fcda4..213a87d 100644
--- a/src/rcl_goal_utils.erl
+++ b/src/rcl_goal_utils.erl
@@ -3,7 +3,7 @@
%%%
%%% This file is provided to you under the Apache License,
%%% Version 2.0 (the "License"); you may not use this file
-%%% except in compliance with the License. You may obtain
+%%% except in compliance with the License. You may obtain
%%% a copy of the License at
%%%
%%% http://www.apache.org/licenses/LICENSE-2.0
@@ -29,7 +29,6 @@
%% types
%%============================================================================
-
%%============================================================================
%% API
%%============================================================================
@@ -68,7 +67,6 @@ to_op(<<":btwn:">>) ->
to_op(<<":between:">>) ->
'between'.
-
to_vsn(Version) when erlang:is_list(Version) ->
to_vsn(erlang:iolist_to_binary(Version));
to_vsn(Vsn) ->
diff --git a/src/rcl_log.erl b/src/rcl_log.erl
index 63eee14..a5fb43a 100644
--- a/src/rcl_log.erl
+++ b/src/rcl_log.erl
@@ -32,11 +32,13 @@
error/2,
error/3,
log_level/1,
- atom_log_level/1]).
+ atom_log_level/1,
+ format/1]).
-export_type([int_log_level/0,
log_level/0,
- state/0]).
+ log_fun/0,
+ t/0]).
-include_lib("relcool/include/relcool.hrl").
@@ -48,13 +50,15 @@
%% Why no warn? because for our purposes there is no difference between error
%% and warn
-type log_level() :: error | info | debug.
--opaque state() :: {?MODULE, log_level()}.
+-opaque t() :: {?MODULE, int_log_level()}.
+
+-type log_fun() :: fun(() -> iolist()).
%%============================================================================
%% API
%%============================================================================
%% @doc Create a new 'log level' for the system
--spec new(int_log_level() | log_level()) -> state().
+-spec new(int_log_level() | log_level()) -> t().
new(LogLevel) when LogLevel >= 0, LogLevel =< 2 ->
{?MODULE, LogLevel};
new(AtomLogLevel)
@@ -68,44 +72,66 @@ new(AtomLogLevel)
end,
new(LogLevel).
-%% @doc log at the debug level given the current log state with a string
--spec debug(state(), string()) -> none().
+
+%% @doc log at the debug level given the current log state with a string or
+%% function that returns a string
+-spec debug(t(), string() | log_fun()) -> ok.
+debug(LogState, Fun)
+ when erlang:is_function(Fun) ->
+ log(LogState, ?RCL_DEBUG, Fun);
debug(LogState, String) ->
debug(LogState, "~s~n", [String]).
%% @doc log at the debug level given the current log state with a format string
%% and argements @see io:format/2
--spec debug(state(), string(), [any()]) -> none().
+-spec debug(t(), string(), [any()]) -> ok.
debug(LogState, FormatString, Args) ->
log(LogState, ?RCL_DEBUG, FormatString, Args).
-%% @doc log at the info level given the current log state with a string
--spec info(state(), string()) -> none().
+%% @doc log at the info level given the current log state with a string or
+%% function that returns a string
+-spec info(t(), string() | log_fun()) -> ok.
+info(LogState, Fun)
+ when erlang:is_function(Fun) ->
+ log(LogState, ?RCL_INFO, Fun);
info(LogState, String) ->
info(LogState, "~s~n", [String]).
%% @doc log at the info level given the current log state with a format string
%% and argements @see io:format/2
--spec info(state(), string(), [any()]) -> none().
+-spec info(t(), string(), [any()]) -> ok.
info(LogState, FormatString, Args) ->
log(LogState, ?RCL_INFO, FormatString, Args).
-%% @doc log at the error level given the current log state with a string
--spec error(state(), string()) -> none().
+%% @doc log at the error level given the current log state with a string or
+%% format string that returns a function
+-spec error(t(), string() | log_fun()) -> ok.
+error(LogState, Fun)
+ when erlang:is_function(Fun) ->
+ log(LogState, ?RCL_ERROR, Fun);
error(LogState, String) ->
error(LogState, "~s~n", [String]).
%% @doc log at the error level given the current log state with a format string
%% and argements @see io:format/2
--spec error(state(), string(), [any()]) -> none().
+-spec error(t(), string(), [any()]) -> ok.
error(LogState, FormatString, Args) ->
log(LogState, ?RCL_ERROR, FormatString, Args).
+%% @doc Execute the fun passed in if log level is as expected.
+-spec log(t(), int_log_level(), log_fun()) -> ok.
+log({?MODULE, DetailLogLevel}, LogLevel, Fun)
+ when DetailLogLevel >= LogLevel ->
+ io:format("~s~n", [Fun()]);
+log(_, _, _) ->
+ ok.
+
+
%% @doc when the module log level is less then or equal to the log level for the
%% call then write the log info out. When its not then ignore the call.
--spec log(state(), int_log_level(), string(), [any()]) -> ok.
+-spec log(t(), int_log_level(), string(), [any()]) -> ok.
log({?MODULE, DetailLogLevel}, LogLevel, FormatString, Args)
- when DetailLogLevel =< LogLevel,
+ when DetailLogLevel >= LogLevel,
erlang:is_list(Args) ->
io:format(FormatString, Args);
log(_, _, _, _) ->
@@ -113,7 +139,7 @@ log(_, _, _, _) ->
%% @doc return a boolean indicating if the system should log for the specified
%% levelg
--spec should(state(), int_log_level()) -> ok.
+-spec should(t(), int_log_level() | any()) -> boolean().
should({?MODULE, DetailLogLevel}, LogLevel)
when DetailLogLevel >= LogLevel ->
true;
@@ -121,12 +147,12 @@ should(_, _) ->
false.
%% @doc get the current log level as an integer
--spec log_level(state()) -> int_log_level().
+-spec log_level(t()) -> int_log_level().
log_level({?MODULE, DetailLogLevel}) ->
DetailLogLevel.
%% @doc get the current log level as an atom
--spec atom_log_level(state()) -> log_level().
+-spec atom_log_level(t()) -> log_level().
atom_log_level({?MODULE, ?RCL_ERROR}) ->
error;
atom_log_level({?MODULE, ?RCL_INFO}) ->
@@ -134,6 +160,13 @@ atom_log_level({?MODULE, ?RCL_INFO}) ->
atom_log_level({?MODULE, ?RCL_DEBUG}) ->
debug.
+-spec format(t()) -> iolist().
+format(Log) ->
+ [<<"(">>,
+ erlang:integer_to_list(log_level(Log)), <<":">>,
+ erlang:atom_to_list(atom_log_level(Log)),
+ <<")">>].
+
%%%===================================================================
%%% Test Functions
%%%===================================================================
@@ -143,23 +176,23 @@ atom_log_level({?MODULE, ?RCL_DEBUG}) ->
should_test() ->
ErrorLogState = new(error),
- ?assert(should(ErrorLogState, ?RCL_ERROR)),
- ?assert(not should(ErrorLogState, ?RCL_INFO)),
- ?assert(not should(ErrorLogState, ?RCL_DEBUG)),
+ ?assertMatch(true, should(ErrorLogState, ?RCL_ERROR)),
+ ?assertMatch(true, not should(ErrorLogState, ?RCL_INFO)),
+ ?assertMatch(true, not should(ErrorLogState, ?RCL_DEBUG)),
?assertEqual(?RCL_ERROR, log_level(ErrorLogState)),
?assertEqual(error, atom_log_level(ErrorLogState)),
InfoLogState = new(info),
- ?assert(should(InfoLogState, ?RCL_ERROR)),
- ?assert(should(InfoLogState, ?RCL_INFO)),
- ?assert(not should(InfoLogState, ?RCL_DEBUG)),
+ ?assertMatch(true, should(InfoLogState, ?RCL_ERROR)),
+ ?assertMatch(true, should(InfoLogState, ?RCL_INFO)),
+ ?assertMatch(true, not should(InfoLogState, ?RCL_DEBUG)),
?assertEqual(?RCL_INFO, log_level(InfoLogState)),
?assertEqual(info, atom_log_level(InfoLogState)),
DebugLogState = new(debug),
- ?assert(should(DebugLogState, ?RCL_ERROR)),
- ?assert(should(DebugLogState, ?RCL_INFO)),
- ?assert(should(DebugLogState, ?RCL_DEBUG)),
+ ?assertMatch(true, should(DebugLogState, ?RCL_ERROR)),
+ ?assertMatch(true, should(DebugLogState, ?RCL_INFO)),
+ ?assertMatch(true, should(DebugLogState, ?RCL_DEBUG)),
?assertEqual(?RCL_DEBUG, log_level(DebugLogState)),
?assertEqual(debug, atom_log_level(DebugLogState)).
diff --git a/src/rcl_state.erl b/src/rcl_state.erl
index 7175d1f..3a61e58 100644
--- a/src/rcl_state.erl
+++ b/src/rcl_state.erl
@@ -24,30 +24,72 @@
-module(rcl_state).
-export([new/1,
- log/1]).
+ log/1,
+ output_dir/1,
+ lib_dirs/1,
+ goals/1,
+ format/1,
+ format/2]).
--export_type([state/0]).
+-export_type([t/0,
+ cmd_args/0]).
--record(?MODULE, {log :: rcl_log:state()}).
+-record(?MODULE, {log :: rcl_log:t(),
+ output_dir :: file:name(),
+ lib_dirs=[] :: [file:name()],
+ goals=[] :: [depsolver:constraint()]}).
%%============================================================================
%% types
%%============================================================================
--opaque state() :: record(?MODULE).
+
+-type cmd_args() :: proplists:proplist().
+-opaque t() :: record(?MODULE).
%%============================================================================
%% API
%%============================================================================
%% @doc Create a new 'log level' for the system
--spec new(proplists:proplist()) -> state().
+-spec new(proplists:proplist()) -> t().
new(PropList) when erlang:is_list(PropList) ->
- #?MODULE{log = proplists:get_value(log, PropList, rcl_log:new(error))}.
+ #?MODULE{log = proplists:get_value(log, PropList, rcl_log:new(error)),
+ output_dir=proplists:get_value(output_dir, PropList, ""),
+ lib_dirs=proplists:get_value(lib_dirs, PropList, []),
+ goals=proplists:get_value(goals, PropList, [])}.
%% @doc get the current log state for the system
--spec log(state()) -> rc_log:state().
+-spec log(t()) -> rcl_log:t().
log(#?MODULE{log=LogState}) ->
LogState.
+-spec output_dir(t()) -> file:name().
+output_dir(#?MODULE{output_dir=OutDir}) ->
+ OutDir.
+
+-spec lib_dirs(t()) -> [file:name()].
+lib_dirs(#?MODULE{lib_dirs=LibDir}) ->
+ LibDir.
+
+-spec goals(t()) -> [depsolver:constraints()].
+goals(#?MODULE{goals=TS}) ->
+ TS.
+
+-spec format(t()) -> iolist().
+format(Mod) ->
+ format(Mod, 0).
+
+-spec format(t(), non_neg_integer()) -> iolist().
+format(#?MODULE{log=LogState, output_dir=OutDir, lib_dirs=LibDirs, goals=Goals},
+ Indent) ->
+ [rcl_util:indent(Indent),
+ <<"state:\n">>,
+ rcl_util:indent(Indent + 1), <<"log: ">>, rcl_log:format(LogState), <<",\n">>,
+ rcl_util:indent(Indent + 1), "goals: \n",
+ [[rcl_util:indent(Indent + 2), depsolver:format_constraint(Goal), ",\n"] || Goal <- Goals],
+ rcl_util:indent(Indent + 1), "output_dir: ", OutDir, "\n",
+ rcl_util:indent(Indent + 1), "lib_dirs: \n",
+ [[rcl_util:indent(Indent + 2), LibDir, ",\n"] || LibDir <- LibDirs]].
+
%%%===================================================================
%%% Test Functions
%%%===================================================================
diff --git a/src/rcl_util.erl b/src/rcl_util.erl
index c0b5c5d..be20302 100644
--- a/src/rcl_util.erl
+++ b/src/rcl_util.erl
@@ -21,8 +21,10 @@
%%% @doc Trivial utility file to help handle common tasks
-module(rcl_util).
--export([mkdir_p/1]).
+-export([mkdir_p/1,
+ indent/1]).
+-define(ONE_LEVEL_INDENT, " ").
%%============================================================================
%% types
%%============================================================================
@@ -41,6 +43,11 @@ mkdir_p(Path) ->
DirName = filename:join([filename:absname(Path), "tmp"]),
filelib:ensure_dir(DirName).
+%% @doc ident to the level specified
+-spec indent(non_neg_integer()) -> iolist().
+indent(Amount) when erlang:is_integer(Amount) ->
+ [?ONE_LEVEL_INDENT || _ <- lists:seq(1, Amount)].
+
%%%===================================================================
%%% Test Functions
%%%===================================================================
diff --git a/src/relcool.erl b/src/relcool.erl
index 7702207..f8a9bbd 100644
--- a/src/relcool.erl
+++ b/src/relcool.erl
@@ -1,12 +1,104 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
+%%%
+%%% This file is provided to you under the Apache License,
+%%% Version 2.0 (the "License"); you may not use this file
+%%% except in compliance with the License. You may obtain
+%%% a copy of the License at
+%%%
+%%% http://www.apache.org/licenses/LICENSE-2.0
+%%%
+%%% Unless required by applicable law or agreed to in writing,
+%%% software distributed under the License is distributed on an
+%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%%% KIND, either express or implied. See the License for the
+%%% specific language governing permissions and limitations
+%%% under the License.
%%%---------------------------------------------------------------------------
%%% @author Eric Merritt <[email protected]>
+%%% @copyright (C) 2012 Erlware, LLC.
%%% @doc
-%%% @copyright (C) 2007-2012 Erlware, Inc.
-%%%---------------------------------------------------------------------------
-module(relcool).
--export([main/1]).
+-export([main/1, opt_spec_list/0]).
+
+%%============================================================================
+%% types
+%%============================================================================
+
+%%============================================================================
+%% API
+%%============================================================================
+-spec main([string()]) -> ok.
+main(Args) ->
+ io:format("~p~n", [Args]),
+ OptSpecList = opt_spec_list(),
+ case rcl_cmd_args:args2state(getopt:parse(OptSpecList, Args)) of
+ {ok, {State, _Target}} ->
+ run_relcool_process(State);
+ Error={error, _} ->
+ report_error(Error)
+ end.
+
+-spec opt_spec_list() -> [getopt:option_spec()].
+opt_spec_list() ->
+ [
+ {relname, $n, "relname", string, "Specify the name for the release that will be generated"},
+ {relvsn, $v, "relvsn", string, "Specify the version for the release"},
+ {goals, $g, "goal", string, "Specify a target constraint on the system. These are "
+ "usually the OTP"},
+ {output_dir, $o, "output-dir", string, "The output directory for the release. This is `./` by default."},
+ {lib_dir, $l, "lib-dir", string, "Additional dirs that should be searched for OTP Apps"},
+ {log_level, $V, "verbose", {integer, 2}, "Verbosity level, maybe between 0 and 2"}
+ ].
-main(_Args) ->
+%%============================================================================
+%% internal api
+%%============================================================================
+run_relcool_process(State) ->
+ rcl_log:info(rcl_state:log(State), "Starting relcool build process ..."),
+ rcl_log:debug(rcl_state:log(State),
+ fun() ->
+ rcl_state:format(State)
+ end),
ok.
+
+-spec usage() -> ok.
+usage() ->
+ getopt:usage(opt_spec_list(), "relcool", "[*release-specification-file*]").
+
+
+-spec report_error(term()) -> none().
+report_error(Error) ->
+ io:format("~s~n", [to_error(Error)]),
+ usage(),
+ erlang:halt(127).
+
+-spec to_error({error, Reason::term()}) -> string().
+to_error({error,{invalid_option_arg, Arg}}) ->
+ case Arg of
+ {goals, Goal} ->
+ io_lib:format("Invalid Goal argument -g ~p~n", [Goal]);
+ {relname, RelName} ->
+ io_lib:format("Invalid Release Name argument -n ~p~n", [RelName]);
+ {relvsn, RelVsn} ->
+ io_lib:format("Invalid Release Version argument -n ~p~n", [RelVsn]);
+ {output_dir, Outdir} ->
+ io_lib:format("Invalid Output Directory argument -n ~p~n", [Outdir]);
+ {lib_dir, LibDir} ->
+ io_lib:format("Invalid Library Directory argument -n ~p~n", [LibDir]);
+ {log_level, LogLevel} ->
+ io_lib:format("Invalid Library Directory argument -n ~p~n", [LogLevel])
+ end;
+to_error({error, {failed_to_parse, Spec}}) ->
+ io_lib:format("Unable to parse spec ~s", [Spec]);
+to_error({error, {unable_to_create_output_dir, OutputDir}}) ->
+ io_lib:format("Unable to create output directory (possible permissions issue): ~s",
+ [OutputDir]);
+to_error({error, {not_directory, Dir}}) ->
+ io_lib:format("Library directory does not exist: ~s", [Dir]);
+to_error({error, {invalid_log_level, LogLevel}}) ->
+ io_lib:format("Invalid log level specified -V ~p, log level must be in the"
+ " range 0..2", [LogLevel]);
+to_error({error, Error}) ->
+ io_lib:format("~p~n", [Error]).
diff --git a/test/rclt_command_SUITE.erl b/test/rclt_command_SUITE.erl
new file mode 100644
index 0000000..5967cc5
--- /dev/null
+++ b/test/rclt_command_SUITE.erl
@@ -0,0 +1,100 @@
+%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
+%%%
+%%% This file is provided to you under the Apache License,
+%%% Version 2.0 (the "License"); you may not use this file
+%%% except in compliance with the License. You may obtain
+%%% a copy of the License at
+%%%
+%%% http://www.apache.org/licenses/LICENSE-2.0
+%%%
+%%% Unless required by applicable law or agreed to in writing,
+%%% software distributed under the License is distributed on an
+%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%%% KIND, either express or implied. See the License for the
+%%% specific language governing permissions and limitations
+%%% under the License.
+%%%-------------------------------------------------------------------
+%%% @author Eric Merrit <[email protected]>
+%%% @copyright (C) 2012, Eric Merrit
+-module(rclt_command_SUITE).
+
+-export([suite/0,
+ init_per_suite/1,
+ end_per_suite/1,
+ all/0,
+ normal_passing_case/1,
+ lib_fail_case/1,
+ output_fail_case/1,
+ spec_parse_fail_case/1]).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+suite() ->
+ [{timetrap,{seconds,30}}].
+
+init_per_suite(Config) ->
+ Config.
+
+end_per_suite(_Config) ->
+ ok.
+
+all() ->
+ [normal_passing_case, lib_fail_case, output_fail_case].
+
+normal_passing_case(Config) ->
+ DataDir = proplists:get_value(data_dir, Config),
+ Lib1 = filename:join([DataDir, "lib1"]),
+ Lib2 = filename:join([DataDir, "lib2"]),
+ Outdir = filename:join([DataDir, "outdir"]),
+ ok = rcl_util:mkdir_p(Lib1),
+ ok = rcl_util:mkdir_p(Lib2),
+ Goal1 = "app1<=33.33+build4",
+ Goal2 = "app2:btwn:33.22,45.22+build.21",
+
+ LogLevel = "2",
+ RelName = "foo-release",
+ RelVsn = "33.222",
+ CmdLine = ["-V", LogLevel, "-g",Goal1,"-g",Goal2, "-l", Lib1, "-l", Lib2,
+ "-n", RelName, "-v", RelVsn, "-o", Outdir],
+ {ok, {State, Target}} = rcl_cmd_args:args2state(getopt:parse(relcool:opt_spec_list(), CmdLine)),
+ ?assertMatch([], Target),
+ ?assertMatch([Lib1, Lib2],
+ rcl_state:lib_dirs(State)),
+ ?assertMatch(Outdir, rcl_state:output_dir(State)),
+
+ ?assertMatch([{<<"app1">>,{{33,33},{[],[<<"build4">>]}},lte},
+ {<<"app2">>,
+ {{33,22},{[],[]}},
+ {{45,22},{[],[<<"build">>,21]}},
+ between}], rcl_state:goals(State)).
+
+
+lib_fail_case(Config) ->
+ DataDir = proplists:get_value(data_dir, Config),
+ Lib1 = filename:join([DataDir, "lib1"]),
+ Lib2 = filename:join([DataDir, "lib3333"]),
+ ok = rcl_util:mkdir_p(Lib1),
+
+ CmdLine = ["-l", Lib1, "-l", Lib2],
+ ?assertMatch({error, {not_directory, Lib2}},
+ rcl_cmd_args:args2state(getopt:parse(relcool:opt_spec_list(), CmdLine))).
+
+
+output_fail_case(Config) ->
+ DataDir = proplists:get_value(data_dir, Config),
+ UnwritableDir = filename:join([DataDir, "unwritable"]),
+ ok = rcl_util:mkdir_p(UnwritableDir),
+ ok = file:change_mode(UnwritableDir, 8#555),
+ CanNotCreate = filename:join([UnwritableDir, "out-dir-should-not-create"]),
+
+ CmdLine = ["-o", CanNotCreate],
+ ?assertMatch({error, {unable_to_create_output_dir, CanNotCreate}},
+ rcl_cmd_args:args2state(getopt:parse(relcool:opt_spec_list(), CmdLine))).
+
+spec_parse_fail_case(_Config) ->
+ Spec = "aaeu:3333:33.22a44",
+ CmdLine = ["-g", Spec],
+ ?assertMatch({error, {failed_to_parse, _Spec}},
+ rcl_cmd_args:args2state(getopt:parse(relcool:opt_spec_list(), CmdLine))).