aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTristan Sloughter <[email protected]>2013-02-07 12:19:10 -0800
committerTristan Sloughter <[email protected]>2013-02-07 12:19:10 -0800
commit6d27a1af18f8337bbcfd3994787bf8725b94868a (patch)
tree93feb4e317d35d9d69443551b83175d21a0a0808
parentaac84f08f2b557ae2a7bf45001c47aec503203ab (diff)
parent6734e9ec4e0dd4c0678d10e8b8e48552ffb4e995 (diff)
downloadrelx-6d27a1af18f8337bbcfd3994787bf8725b94868a.tar.gz
relx-6d27a1af18f8337bbcfd3994787bf8725b94868a.tar.bz2
relx-6d27a1af18f8337bbcfd3994787bf8725b94868a.zip
Merge pull request #28 from erlware/next
Next
-rw-r--r--Makefile15
-rw-r--r--README.md10
-rw-r--r--rebar.config7
-rw-r--r--src/rcl_app_discovery.erl223
-rw-r--r--src/rcl_app_info.erl6
-rw-r--r--src/rcl_cmd_args.erl124
-rw-r--r--src/rcl_depsolver.erl7
-rw-r--r--src/rcl_goal_utils.erl2
-rw-r--r--src/rcl_log.erl13
-rw-r--r--src/rcl_provider.erl16
-rw-r--r--src/rcl_prv_assembler.erl130
-rw-r--r--src/rcl_prv_config.erl72
-rw-r--r--src/rcl_prv_discover.erl242
-rw-r--r--src/rcl_prv_overlay.erl418
-rw-r--r--src/rcl_prv_release.erl45
-rw-r--r--src/rcl_rel_discovery.erl166
-rw-r--r--src/rcl_release.erl76
-rw-r--r--src/rcl_state.erl83
-rw-r--r--src/rcl_topo.erl17
-rw-r--r--src/rcl_util.erl28
-rw-r--r--src/relcool.erl92
-rw-r--r--test/rclt_command_SUITE.erl22
-rw-r--r--test/rclt_discover_SUITE.erl15
-rw-r--r--test/rclt_goal.erl2
-rw-r--r--test/rclt_release_SUITE.erl480
25 files changed, 1859 insertions, 452 deletions
diff --git a/Makefile b/Makefile
index 2e43f9d..96e14ae 100644
--- a/Makefile
+++ b/Makefile
@@ -38,16 +38,20 @@ endif
.PHONY: all compile doc clean test dialyzer typer shell distclean pdf \
get-deps escript clean-common-test-data rebuild
-all: compile escript dialyzer test
+all: deps compile escript dialyzer test
# =============================================================================
# Rules to build the system
# =============================================================================
-get-deps:
+deps:
$(REBAR) get-deps
$(REBAR) compile
+update-deps:
+ $(REBAR) update-deps
+ $(REBAR) compile
+
compile:
$(REBAR) skip_deps=true compile
@@ -61,7 +65,12 @@ eunit: compile clean-common-test-data
$(REBAR) skip_deps=true eunit
ct: compile clean-common-test-data
- $(REBAR) skip_deps=true ct
+ mkdir -p $(CURDIR) logs
+ ct_run -pa $(CURDIR)/ebin \
+ -pa $(CURDIR)/deps/*/ebin \
+ -logdir $(CURDIR)/logs \
+ -dir $(CURDIR)/test/ \
+ -suite rclt_command_SUITE rclt_discover_SUITE -suite rclt_release_SUITE
test: compile eunit ct
diff --git a/README.md b/README.md
index b38b860..5407002 100644
--- a/README.md
+++ b/README.md
@@ -14,13 +14,16 @@ applications it will generate a release output. That output depends
heavily on what plugins available and what options are defined, but
usually it is simple a well configured release directory.
- relcool -d ~/my-dirs --relname foo --relvsn 0.0.1 --target-spec myapp --target-spec getopt>=0.5.1 -o output-dir --targz
+ relcool -c relcool.config -l ~/my-dirs --relname foo --relvsn 0.0.1 --target-spec myapp --target-spec getopt>=0.5.1 -o output-dir
The *release-specification-file* is optional but otherwise contains
additional specification information for releases.
# OPTIONS
+-r *STRING*, \--root *STRING*
+: Specify the root directory for the project (if different from cwd)
+
-n *STRING*, \--relname *STRING*
: Specify the name for the release that will be generated
@@ -40,6 +43,9 @@ additional specification information for releases.
-V *INTEGER*, \--verbose *INTEGER*
: The verbosity level of the system. Valid values are 1 - 3
+-c *INTEGER*, \--config *INTEGER*
+: The custom config file for the relcool system
+
# CONFIGURATION FILES
Configuration files
@@ -47,3 +53,5 @@ Configuration files
# SEE ALSO
`reltool` (1).
+
+[relcool wiki](https://github.com/erlware/relcool/wiki)
diff --git a/rebar.config b/rebar.config
index 2b2f0b3..8ffc212 100644
--- a/rebar.config
+++ b/rebar.config
@@ -8,6 +8,9 @@
{erlware_commons, ".*",
{git, "https://github.com/erlware/erlware_commons.git",
{branch, "next"}}},
+ {erlydtl, ".*",
+ {git, "https://github.com/evanmiller/erlydtl.git",
+ {branch, "master"}}},
{getopt, "",
{git, "https://github.com/jcomellas/getopt.git",
{branch, "master"}}}]}.
@@ -21,12 +24,12 @@
%% EUnit =======================================================================
{eunit_opts,
- [verbose, {report, {eunit_surefire, [{dir, "."}]}}]}.
+ [{report, {eunit_surefire, [{dir, "."}]}}]}.
{cover_enabled, true}.
{cover_print_enabled, true}.
%% Misc =======================================================================
{escript_incl_apps,
- [getopt, erlware_commons]}.
+ [getopt, erlware_commons, erlydtl]}.
{first_files, [rcl_provider]}.
diff --git a/src/rcl_app_discovery.erl b/src/rcl_app_discovery.erl
new file mode 100644
index 0000000..20df1cf
--- /dev/null
+++ b/src/rcl_app_discovery.erl
@@ -0,0 +1,223 @@
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
+%%% 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 This provider uses the lib_dir setting of the state. It searches the
+%%% Lib Dirs looking for all OTP Applications that are available. When it finds
+%%% those OTP Applications it loads the information about them and adds them to
+%%% the state of available apps. This implements the rcl_provider behaviour.
+-module(rcl_app_discovery).
+
+-export([do/2,
+ format_error/1]).
+
+-include_lib("relcool/include/relcool.hrl").
+
+%%============================================================================
+%% API
+%%============================================================================
+
+%% @doc recursively dig down into the library directories specified in the state
+%% looking for OTP Applications
+-spec do(rcl_state:t(), [filename:name()]) -> {ok, [rcl_app_info:t()]} | relcool:error().
+do(State, LibDirs) ->
+ rcl_log:info(rcl_state:log(State),
+ fun() ->
+ ["Resolving OTP Applications from directories:\n",
+ [[rcl_util:indent(1), LibDir, "\n"] || LibDir <- LibDirs]]
+ end),
+ resolve_app_metadata(State, LibDirs).
+
+-spec format_error([ErrorDetail::term()]) -> iolist().
+format_error(ErrorDetails)
+ when erlang:is_list(ErrorDetails) ->
+ [[format_detail(ErrorDetail), "\n"] || ErrorDetail <- ErrorDetails].
+
+%%%===================================================================
+%%% Internal Functions
+%%%===================================================================
+resolve_app_metadata(State, LibDirs) ->
+ AppMeta0 = lists:flatten(ec_plists:map(fun(LibDir) ->
+ discover_dir([], LibDir)
+ end, LibDirs)),
+ AppMeta1 = setup_overrides(State, AppMeta0),
+
+ Errors = [case El of
+ {error, Ret} -> Ret;
+ _ -> El
+ end
+ || El <- AppMeta1,
+ case El of
+ {error, _} ->
+ true;
+ _ ->
+ false
+ end],
+
+ case Errors of
+ [] ->
+ AppMeta2 = lists:flatten(AppMeta1),
+ rcl_log:debug(rcl_state:log(State),
+ fun() ->
+ ["Resolved the following OTP Applications from the system: \n",
+ [[rcl_app_info:format(1, App), "\n"] || App <- AppMeta2]]
+ end),
+ {ok, AppMeta2};
+ _ ->
+ ?RCL_ERROR(Errors)
+ end.
+
+app_name({error, _}) ->
+ undefined;
+app_name(AppMeta) ->
+ rcl_app_info:name(AppMeta).
+
+setup_overrides(State, AppMetas0) ->
+ Overrides = rcl_state:overrides(State),
+ AppMetas1 = [AppMeta || AppMeta <- AppMetas0,
+ not lists:keymember(app_name(AppMeta), 1, Overrides)],
+ [case is_valid_otp_app(filename:join([FileName, "ebin",
+ erlang:atom_to_list(AppName) ++ ".app"])) of
+ [] ->
+ {error, {invalid_override, AppName, FileName}};
+ Error = {error, _} ->
+ Error;
+ App ->
+ rcl_app_info:link(App, true)
+ end || {AppName, FileName} <- Overrides] ++ AppMetas1.
+
+
+
+-spec format_detail(ErrorDetail::term()) -> iolist().
+format_detail({error, {invalid_override, AppName, FileName}}) ->
+ io_lib:format("Override {~p, ~p} is not a valid OTP App. Perhaps you forgot to build it?",
+ [AppName, FileName]);
+format_detail({accessing, File, eaccess}) ->
+ io_lib:format("permission denied accessing file ~s", [File]);
+format_detail({accessing, File, Type}) ->
+ io_lib:format("error (~p) accessing file ~s", [Type, File]);
+format_detail({no_beam_files, EbinDir}) ->
+ io_lib:format("no beam files found in directory ~s", [EbinDir]);
+format_detail({not_a_directory, EbinDir}) ->
+ io_lib:format("~s is not a directory when it should be a directory", [EbinDir]);
+format_detail({unable_to_load_app, AppDir, _}) ->
+ io_lib:format("Unable to load the application metadata from ~s", [AppDir]);
+format_detail({invalid_app_file, File}) ->
+ io_lib:format("Application metadata file exists but is malformed: ~s",
+ [File]);
+format_detail({unversioned_app, AppDir, _AppName}) ->
+ io_lib:format("Application metadata exists but version is not available: ~s",
+ [AppDir]);
+format_detail({app_info_error, {Module, Detail}}) ->
+ Module:format_error(Detail).
+
+-spec discover_dir([file:name()],
+ file:name()) ->
+ [rcl_app_info:t() | {error, Reason::term()}] |
+ rcl_app_info:t() | {error, Reason::term()}.
+discover_dir(IgnoreDirs, File) ->
+ case (not lists:member(File, IgnoreDirs))
+ andalso filelib:is_dir(File) of
+ true ->
+ case file:list_dir(File) of
+ {error, Reason} ->
+ {error, {accessing, File, Reason}};
+ {ok, List} ->
+ ec_plists:map(fun(LibDir) ->
+ discover_dir(IgnoreDirs, LibDir)
+ end, [filename:join([File, Dir]) || Dir <- List])
+ end;
+ false ->
+ is_valid_otp_app(File)
+ end.
+
+-spec is_valid_otp_app(file:name()) -> rcl_app_info:t() | {error, Reason::term()} | [].
+is_valid_otp_app(File) ->
+ %% Is this an ebin dir?
+ EbinDir = filename:dirname(File),
+ case filename:basename(EbinDir) of
+ "ebin" ->
+ case lists:suffix(".app", File) of
+ true ->
+ has_at_least_one_beam(EbinDir, File);
+ false ->
+ []
+ end;
+ _ ->
+ []
+ end.
+
+-spec has_at_least_one_beam(file:name(), file:filename()) ->
+ rcl_app_info:t() | {error, Reason::term()}.
+has_at_least_one_beam(EbinDir, File) ->
+ case file:list_dir(EbinDir) of
+ {ok, List} ->
+ case lists:any(fun(NFile) -> lists:suffix(".beam", NFile) end, List) of
+ true ->
+ gather_application_info(EbinDir, File);
+ false ->
+ {error, {no_beam_files, EbinDir}}
+ end;
+ _ ->
+ {error, {not_a_directory, EbinDir}}
+ end.
+
+-spec gather_application_info(file:name(), file:filename()) ->
+ rcl_app_info:t() | {error, Reason::term()}.
+gather_application_info(EbinDir, File) ->
+ AppDir = filename:dirname(EbinDir),
+ case file:consult(File) of
+ {ok, [{application, AppName, AppDetail}]} ->
+ get_vsn(AppDir, AppName, AppDetail);
+ {error, Reason} ->
+ {error, {unable_to_load_app, AppDir, Reason}};
+ _ ->
+ {error, {invalid_app_file, File}}
+ end.
+
+-spec get_vsn(file:name(), atom(), proplists:proplist()) ->
+ rcl_app_info:t() | {error, Reason::term()}.
+get_vsn(AppDir, AppName, AppDetail) ->
+ case proplists:get_value(vsn, AppDetail) of
+ undefined ->
+ {error, {unversioned_app, AppDir, AppName}};
+ AppVsn ->
+ case get_deps(AppDir, AppName, AppVsn, AppDetail) of
+ {ok, App} ->
+ App;
+ {error, Detail} ->
+ {error, {app_info_error, Detail}}
+ end
+ end.
+
+-spec get_deps(file:name(), atom(), string(), proplists:proplist()) ->
+ {ok, rcl_app_info:t()} | {error, Reason::term()}.
+get_deps(AppDir, AppName, AppVsn, AppDetail) ->
+ ActiveApps = proplists:get_value(applications, AppDetail, []),
+ LibraryApps = proplists:get_value(included_applications, AppDetail, []),
+ rcl_app_info:new(AppName, AppVsn, AppDir, ActiveApps, LibraryApps).
+
+%%%===================================================================
+%%% Test Functions
+%%%===================================================================
+
+-ifndef(NOTEST).
+-include_lib("eunit/include/eunit.hrl").
+
+-endif.
diff --git a/src/rcl_app_info.erl b/src/rcl_app_info.erl
index bc64e30..f2afa0b 100644
--- a/src/rcl_app_info.erl
+++ b/src/rcl_app_info.erl
@@ -1,4 +1,4 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -63,8 +63,8 @@
vsn :: ec_semver:semver(),
dir :: file:name(),
link=false :: boolean(),
- active_deps :: [atom()],
- library_deps :: [atom()]}).
+ active_deps=[]:: [atom()],
+ library_deps=[] :: [atom()]}).
%%============================================================================
%% types
diff --git a/src/rcl_cmd_args.erl b/src/rcl_cmd_args.erl
index 68bd9ce..bfb63b7 100644
--- a/src/rcl_cmd_args.erl
+++ b/src/rcl_cmd_args.erl
@@ -1,4 +1,4 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -34,24 +34,28 @@
relcool:error().
args2state({error, Detail}) ->
?RCL_ERROR({opt_parse, Detail});
-args2state({ok, {Opts, Targets}}) ->
+args2state({ok, {Opts, Target}})
+ when erlang:length(Target) == 0; erlang:length(Target) == 1 ->
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} ->
- case validate_configs(Targets) of
+ case convert_target(Target) of
+ {ok, AtomizedTarget} ->
+ case create_log(Opts, [{relname, RelName},
+ {relvsn, RelVsn}]) of
Error = {error, _} ->
Error;
- {ok, Configs} ->
- {ok, {rcl_state:new(CommandLineConfig, Configs), Configs}}
- end
- end.
+ {ok, CommandLineConfig} ->
+ handle_config(Opts, AtomizedTarget, CommandLineConfig)
+ end;
+ Error ->
+ Error
+ end;
+args2state({ok, {_Opts, Targets}}) ->
+ ?RCL_ERROR({invalid_targets, Targets}).
-spec format_error(Reason::term()) -> iolist().
+format_error({invalid_targets, Targets}) ->
+ io_lib:format("One config must be specified! not ~p~n", [Targets]);
format_error({opt_parse, {invalid_option, Opt}}) ->
io_lib:format("invalid option ~s~n", [Opt]);
format_error({opt_parse, Arg}) ->
@@ -75,39 +79,51 @@ format_error({invalid_config_file, Config}) ->
io_lib:format("Invalid configuration file specified: ~s", [Config]);
format_error({failed_to_parse, Spec}) ->
io_lib:format("Unable to parse spec ~s", [Spec]);
-format_error({unable_to_create_output_dir, OutputDir}) ->
- io_lib:format("Unable to create output directory (possible permissions issue): ~s",
- [OutputDir]);
format_error({not_directory, Dir}) ->
io_lib:format("Library directory does not exist: ~s", [Dir]);
format_error({invalid_log_level, LogLevel}) ->
io_lib:format("Invalid log level specified -V ~p, log level must be in the"
- " range 0..2", [LogLevel]).
+ " range 0..2", [LogLevel]);
+format_error({invalid_target, Target}) ->
+ io_lib:format("Invalid action specified: ~s", [Target]).
+
%%%===================================================================
%%% Internal Functions
%%%===================================================================
--spec validate_configs([file:filename()]) ->
- {ok, [file:filename()]} | relcool:error().
-validate_configs(Configs) ->
- Result =
- lists:foldl(fun(_Config, Err = {error, _}) ->
- Err;
- (Config, Acc) ->
- case filelib:is_regular(Config) of
- true ->
- [filename:absname(Config) | Acc];
- false ->
- ?RCL_ERROR({invalid_config_file, Config})
- end
- end, [], Configs),
- case Result of
- {error, _} ->
- Result;
- _ ->
- %% Order may be important so lets make sure they remain in the same
- %% order they came in as
- {ok, lists:reverse(Result)}
+-spec handle_config([getopt:option()], atom(), proplists:proplist()) ->
+ {ok, {rcl_state:t(), [string()]}} |
+ relcool:error().
+handle_config(Opts, Target, CommandLineConfig) ->
+ case validate_config(proplists:get_value(config, Opts, [])) of
+ Error = {error, _} ->
+ Error;
+ {ok, Config} ->
+ {ok, rcl_state:new([{config, Config} | CommandLineConfig], Target)}
+ end.
+
+-spec convert_target([string()]) -> {ok, release | relup} | relcool:error().
+convert_target([]) ->
+ {ok, release};
+convert_target(["release"]) ->
+ {ok, release};
+convert_target(["relup"]) ->
+ {ok, relup};
+convert_target(Target) ->
+ ?RCL_ERROR({invalid_target, Target}).
+
+-spec validate_config(file:filename() | undefined) ->
+ {ok, file:filename() | undefined} | relcool:error().
+validate_config(undefined) ->
+ {ok, undefined};
+validate_config("") ->
+ {ok, undefined};
+validate_config(Config) ->
+ case filelib:is_regular(Config) of
+ true ->
+ {ok, filename:absname(Config)};
+ false ->
+ ?RCL_ERROR({invalid_config_file, Config})
end.
-spec create_log([getopt:option()], rcl_state:cmd_args()) ->
@@ -148,17 +164,7 @@ convert_goals([RawSpec | Rest], Acc) ->
{ok, rcl_state:cmd_args()} | relcool:error().
create_output_dir(Opts, Acc) ->
OutputDir = proplists:get_value(output_dir, Opts, "./_rel"),
- case filelib:is_dir(OutputDir) of
- false ->
- case rcl_util:mkdir_p(OutputDir) of
- ok ->
- create_lib_dirs(Opts, [{output_dir, OutputDir} | Acc]);
- {error, _} ->
- ?RCL_ERROR({unable_to_create_output_dir, OutputDir})
- end;
- true ->
- create_lib_dirs(Opts, [{output_dir, OutputDir} | Acc])
- end.
+ create_lib_dirs(Opts, [{output_dir, filename:absname(OutputDir)} | Acc]).
-spec create_lib_dirs([getopt:option()], rcl_state:cmd_args()) ->
{ok, rcl_state:cmd_args()} | relcool:error().
@@ -168,9 +174,27 @@ create_lib_dirs(Opts, Acc) ->
Error = {error, _} ->
Error;
ok ->
- {ok, [{lib_dirs, [filename:absname(Dir) || Dir <- Dirs]} | Acc]}
+ create_root_dir(Opts, [{lib_dirs, [filename:absname(Dir) || Dir <- Dirs]} | Acc])
+ end.
+
+-spec create_root_dir([getopt:option()], rcl_state:cmd_args()) ->
+ {ok, rcl_state:cmd_args()} | relcool:error().
+create_root_dir(Opts, Acc) ->
+ Dir = proplists:get_value(root_dir, Opts, undefined),
+ case Dir of
+ undefined ->
+ {ok, Cwd} = file:get_cwd(),
+ create_disable_default_libs(Opts, [{root_dir, Cwd} | Acc]);
+ _ ->
+ create_disable_default_libs(Opts, [{root_dir, Dir} | Acc])
end.
+-spec create_disable_default_libs([getopt:option()], rcl_state:cmd_args()) ->
+ {ok, rcl_state:cmd_args()} | relcool:error().
+create_disable_default_libs(Opts, Acc) ->
+ Def = proplists:get_value(disable_default_libs, Opts, false),
+ {ok, [{disable_default_libs, Def} | Acc]}.
+
-spec check_lib_dirs([string()]) -> ok | relcool:error().
check_lib_dirs([]) ->
ok;
diff --git a/src/rcl_depsolver.erl b/src/rcl_depsolver.erl
index e74cf3b..0cf8c88 100644
--- a/src/rcl_depsolver.erl
+++ b/src/rcl_depsolver.erl
@@ -471,12 +471,13 @@ add_constraint(SrcPkg, SrcVsn, PkgsConstraints, PkgConstraint) ->
{value, {PkgName, Constraints0}} ->
Constraints0
end,
- [{PkgName, [{PkgConstraint, {SrcPkg, SrcVsn}} | Constraints1]} |
- lists:keydelete(PkgName, 1, PkgsConstraints)].
+ [{PkgName, [{PkgConstraint, {SrcPkg, SrcVsn}} | Constraints1]}
+ | lists:keydelete(PkgName, 1, PkgsConstraints)].
%% @doc
%% Extend the currently active constraints correctly for the given constraints.
--spec extend_constraints(pkg_name(), vsn(), constraints(),constraints()) -> [{pkg_name(), constraints()}].
+-spec extend_constraints(pkg_name(), vsn(), constraints(),constraints()) ->
+ [{pkg_name(), constraints()}].
extend_constraints(SrcPkg, SrcVsn, ExistingConstraints0, NewConstraints) ->
lists:foldl(fun (Constraint, ExistingConstraints1) ->
add_constraint(SrcPkg, SrcVsn, ExistingConstraints1, Constraint)
diff --git a/src/rcl_goal_utils.erl b/src/rcl_goal_utils.erl
index 3627b9c..6065e5c 100644
--- a/src/rcl_goal_utils.erl
+++ b/src/rcl_goal_utils.erl
@@ -1,4 +1,4 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
diff --git a/src/rcl_log.erl b/src/rcl_log.erl
index a5fb43a..71c0b5d 100644
--- a/src/rcl_log.erl
+++ b/src/rcl_log.erl
@@ -1,4 +1,4 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -36,6 +36,7 @@
format/1]).
-export_type([int_log_level/0,
+ atom_log_level/0,
log_level/0,
log_fun/0,
t/0]).
@@ -46,10 +47,14 @@
%% types
%%============================================================================
+-type log_level() :: int_log_level() | atom_log_level().
+
-type int_log_level() :: 0..2.
+
%% Why no warn? because for our purposes there is no difference between error
%% and warn
--type log_level() :: error | info | debug.
+-type atom_log_level() :: error | info | debug.
+
-opaque t() :: {?MODULE, int_log_level()}.
-type log_fun() :: fun(() -> iolist()).
@@ -58,7 +63,7 @@
%% API
%%============================================================================
%% @doc Create a new 'log level' for the system
--spec new(int_log_level() | log_level()) -> t().
+-spec new(log_level()) -> t().
new(LogLevel) when LogLevel >= 0, LogLevel =< 2 ->
{?MODULE, LogLevel};
new(AtomLogLevel)
@@ -152,7 +157,7 @@ log_level({?MODULE, DetailLogLevel}) ->
DetailLogLevel.
%% @doc get the current log level as an atom
--spec atom_log_level(t()) -> log_level().
+-spec atom_log_level(t()) -> atom_log_level().
atom_log_level({?MODULE, ?RCL_ERROR}) ->
error;
atom_log_level({?MODULE, ?RCL_INFO}) ->
diff --git a/src/rcl_provider.erl b/src/rcl_provider.erl
index c3ef434..750b96e 100644
--- a/src/rcl_provider.erl
+++ b/src/rcl_provider.erl
@@ -1,3 +1,19 @@
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
+%%% 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 2011 Erlware, LLC.
diff --git a/src/rcl_prv_assembler.erl b/src/rcl_prv_assembler.erl
index c0f65c9..63bd167 100644
--- a/src/rcl_prv_assembler.erl
+++ b/src/rcl_prv_assembler.erl
@@ -1,4 +1,4 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -44,11 +44,16 @@ do(State) ->
{RelName, RelVsn} = rcl_state:default_release(State),
Release = rcl_state:get_release(State, RelName, RelVsn),
OutputDir = rcl_state:output_dir(State),
- case rcl_release:realized(Release) of
- true ->
- copy_app_directories_to_output(State, Release, OutputDir);
- false ->
- ?RCL_ERROR({unresolved_release, RelName, RelVsn})
+ case create_output_dir(OutputDir) of
+ ok ->
+ case rcl_release:realized(Release) of
+ true ->
+ copy_app_directories_to_output(State, Release, OutputDir);
+ false ->
+ ?RCL_ERROR({unresolved_release, RelName, RelVsn})
+ end;
+ Error ->
+ Error
end.
-spec format_error(ErrorDetail::term()) -> iolist().
@@ -69,13 +74,35 @@ format_error({release_script_generation_error, RelFile}) ->
format_error({release_script_generation_warning, Module, Warnings}) ->
["Warnings generating release \s",
rcl_util:indent(1), Module:format_warning(Warnings)];
+format_error({unable_to_create_output_dir, OutputDir}) ->
+ io_lib:format("Unable to create output directory (possible permissions issue): ~s",
+ [OutputDir]);
format_error({release_script_generation_error, Module, Errors}) ->
["Errors generating release \n",
- rcl_util:indent(1), Module:format_error(Errors)].
+ rcl_util:indent(1), Module:format_error(Errors)];
+format_error({unable_to_make_symlink, AppDir, TargetDir, Reason}) ->
+ io_lib:format("Unable to symlink directory ~s to ~s because \n~s~s",
+ [AppDir, TargetDir, rcl_util:indent(1),
+ file:format_error(Reason)]).
%%%===================================================================
%%% Internal Functions
%%%===================================================================
+-spec create_output_dir(file:name()) ->
+ ok | {error, Reason::term()}.
+create_output_dir(OutputDir) ->
+ case filelib:is_dir(OutputDir) of
+ false ->
+ case rcl_util:mkdir_p(OutputDir) of
+ ok ->
+ ok;
+ {error, _} ->
+ ?RCL_ERROR({unable_to_create_output_dir, OutputDir})
+ end;
+ true ->
+ ok
+ end.
+
copy_app_directories_to_output(State, Release, OutputDir) ->
LibDir = filename:join([OutputDir, "lib"]),
ok = ec_file:mkdir_p(LibDir),
@@ -85,9 +112,9 @@ copy_app_directories_to_output(State, Release, OutputDir) ->
(_) ->
false
end,
- ec_plists:map(fun(App) ->
- copy_app(LibDir, App)
- end, Apps)),
+ lists:flatten(ec_plists:map(fun(App) ->
+ copy_app(LibDir, App)
+ end, Apps))),
case Result of
[E | _] ->
E;
@@ -100,35 +127,65 @@ copy_app(LibDir, App) ->
AppVsn = rcl_app_info:vsn_as_string(App),
AppDir = rcl_app_info:dir(App),
TargetDir = filename:join([LibDir, AppName ++ "-" ++ AppVsn]),
+ if
+ AppDir == TargetDir ->
+ %% No need to do anything here, discover found something already in
+ %% a release dir
+ ok;
+ true ->
+ copy_app(App, AppDir, TargetDir)
+ end.
+
+copy_app(App, AppDir, TargetDir) ->
+ remove_symlink_or_directory(TargetDir),
case rcl_app_info:link(App) of
true ->
- file:make_symlink(AppDir, TargetDir);
+ link_directory(AppDir, TargetDir);
+ false ->
+ copy_directory(AppDir, TargetDir)
+ end.
+
+remove_symlink_or_directory(TargetDir) ->
+ case ec_file:is_symlink(TargetDir) of
+ true ->
+ ec_file:remove(TargetDir);
false ->
- ec_plists:map(fun(SubDir) ->
- copy_dir(AppDir, TargetDir, SubDir)
- end, ["ebin",
- "include",
- "priv",
- "src",
- "c_src",
- "README",
- "LICENSE"])
+ case filelib:is_dir(TargetDir) of
+ true ->
+ ok = ec_file:remove(TargetDir, [recursive]);
+ false ->
+ ok
+ end
end.
+link_directory(AppDir, TargetDir) ->
+ case file:make_symlink(AppDir, TargetDir) of
+ {error, Reason} ->
+ ?RCL_ERROR({unable_to_make_symlink, AppDir, TargetDir, Reason});
+ ok ->
+ ok
+ end.
+
+copy_directory(AppDir, TargetDir) ->
+ ec_plists:map(fun(SubDir) ->
+ copy_dir(AppDir, TargetDir, SubDir)
+ end, ["ebin",
+ "include",
+ "priv",
+ "src",
+ "c_src",
+ "README",
+ "LICENSE"]).
+
copy_dir(AppDir, TargetDir, SubDir) ->
SubSource = filename:join(AppDir, SubDir),
SubTarget = filename:join(TargetDir, SubDir),
case filelib:is_dir(SubSource) of
true ->
- case filelib:is_dir(SubTarget) of
- true ->
- ok = ec_file:remove(SubTarget, [recursive]);
- false ->
- ok
- end,
+ ok = rcl_util:mkdir_p(SubTarget),
case ec_file:copy(SubSource, SubTarget, [recursive]) of
{error, E} ->
- ?RCL_ERROR({ec_file_error, AppDir, TargetDir, E});
+ ?RCL_ERROR({ec_file_error, AppDir, SubTarget, E});
ok ->
ok
end;
@@ -144,7 +201,7 @@ create_release_info(State, Release, OutputDir) ->
rcl_release:vsn(Release)]),
ReleaseFile = filename:join([ReleaseDir, RelName ++ ".rel"]),
ok = ec_file:mkdir_p(ReleaseDir),
- case rcl_release:metadata(Release) of
+ case rcl_release:metadata(Release) of
{ok, Meta} ->
ok = ec_file:write_term(ReleaseFile, Meta),
write_bin_file(State, Release, OutputDir, ReleaseDir);
@@ -163,11 +220,18 @@ write_bin_file(State, Release, OutputDir, RelDir) ->
ErlOpts = rcl_state:get(State, erl_opts, ""),
StartFile = bin_file_contents(RelName, RelVsn,
rcl_release:erts(Release),
- ErlOpts),
- ok = file:write_file(VsnRel, StartFile),
- ok = file:change_mode(VsnRel, 8#777),
- ok = file:write_file(BareRel, StartFile),
- ok = file:change_mode(BareRel, 8#777),
+ ErlOpts),
+ %% We generate the start script by default, unless the user
+ %% tells us not too
+ case rcl_state:get(State, generate_start_script, true) of
+ false ->
+ ok;
+ _ ->
+ ok = file:write_file(VsnRel, StartFile),
+ ok = file:change_mode(VsnRel, 8#777),
+ ok = file:write_file(BareRel, StartFile),
+ ok = file:change_mode(BareRel, 8#777)
+ end,
copy_or_generate_sys_config_file(State, Release, OutputDir, RelDir).
%% @doc copy config/sys.config or generate one to releases/VSN/sys.config
diff --git a/src/rcl_prv_config.erl b/src/rcl_prv_config.erl
index ee1c770..60655b1 100644
--- a/src/rcl_prv_config.erl
+++ b/src/rcl_prv_config.erl
@@ -1,3 +1,19 @@
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
+%%% 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 2011 Erlware, LLC.
@@ -29,8 +45,12 @@ init(State) ->
%% populating the state as a result.
-spec do(rcl_state:t()) ->{ok, rcl_state:t()} | relcool:error().
do(State) ->
- ConfigFiles = rcl_state:config_files(State),
- lists:foldl(fun load_config/2, {ok, State}, ConfigFiles).
+ case rcl_state:config_file(State) of
+ undefined ->
+ search_for_dominating_config(State);
+ ConfigFile when erlang:is_list(ConfigFile) ->
+ load_config(ConfigFile, State)
+ end.
-spec format_error(Reason::term()) -> iolist().
format_error({consult, ConfigFile, Reason}) ->
@@ -42,11 +62,51 @@ format_error({invalid_term, Term}) ->
%%%===================================================================
%%% Internal Functions
%%%===================================================================
--spec load_config(file:filename(), {ok, rcl_state:t()} | relcool:error()) ->
+search_for_dominating_config({ok, Cwd}) ->
+ ConfigFile = filename:join(Cwd, "relcool.config"),
+ case ec_file:exists(ConfigFile) of
+ true ->
+ {ok, ConfigFile};
+ false ->
+ search_for_dominating_config(parent_dir(Cwd))
+ end;
+search_for_dominating_config({error, _}) ->
+ no_config;
+search_for_dominating_config(State0) ->
+ {ok, Cwd} = file:get_cwd(),
+ case search_for_dominating_config({ok, Cwd}) of
+ {ok, Config} ->
+ %% we need to set the root dir on state as well
+ {ok, RootDir} = parent_dir(Config),
+ State1 = rcl_state:root_dir(State0, RootDir),
+ load_config(Config, rcl_state:config_file(State1, Config));
+ no_config ->
+ {ok, State0}
+ end.
+
+%% @doc Given a directory returns the name of the parent directory.
+-spec parent_dir(Filename::string()) ->
+ {ok, DirName::string()} | {error, no_parent_dir}.
+parent_dir(Filename) ->
+ parent_dir(filename:split(Filename), []).
+
+%% @doc Given list of directories, splits the list and returns all dirs but the
+%% last as a path.
+-spec parent_dir([string()], [string()]) ->
+ {ok, DirName::string()} | {error, no_parent_dir}.
+parent_dir([_H], []) ->
+ {error, no_parent_dir};
+parent_dir([], []) ->
+ {error, no_parent_dir};
+parent_dir([_H], Acc) ->
+ {ok, filename:join(lists:reverse(Acc))};
+parent_dir([H | T], Acc) ->
+ parent_dir(T, [H | Acc]).
+
+
+-spec load_config(file:filename(), rcl_state:t()) ->
{ok, rcl_state:t()} | relcool:error().
-load_config(_, Err = {error, _}) ->
- Err;
-load_config(ConfigFile, {ok, State}) ->
+load_config(ConfigFile, State) ->
{ok, CurrentCwd} = file:get_cwd(),
ok = file:set_cwd(filename:dirname(ConfigFile)),
Result = case file:consult(ConfigFile) of
diff --git a/src/rcl_prv_discover.erl b/src/rcl_prv_discover.erl
index 0a12d24..2f88354 100644
--- a/src/rcl_prv_discover.erl
+++ b/src/rcl_prv_discover.erl
@@ -1,4 +1,4 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -41,100 +41,60 @@ init(State) ->
%% @doc recursively dig down into the library directories specified in the state
%% looking for OTP Applications
-spec do(rcl_state:t()) -> {ok, rcl_state:t()} | relcool:error().
-do(State) ->
- OutputDir = rcl_state:output_dir(State),
- LibDirs = get_lib_dirs(State),
- rcl_log:info(rcl_state:log(State),
- fun() ->
- ["Resolving OTP Applications from directories:\n",
- [[rcl_util:indent(1), LibDir, "\n"] || LibDir <- LibDirs]]
- end),
- resolve_app_metadata(State, LibDirs, OutputDir).
+do(State0) ->
+ LibDirs = get_lib_dirs(State0),
+ case rcl_app_discovery:do(State0, LibDirs) of
+ {ok, AppMeta} ->
+ case rcl_rel_discovery:do(State0, LibDirs, AppMeta) of
+ {ok, Releases} ->
+ State1 = rcl_state:available_apps(State0, AppMeta),
+ State3 = lists:foldl(fun add_if_not_found/2,
+ State1, Releases),
+ {ok, State3};
+ Error ->
+ Error
+ end;
+ Error ->
+ Error
+ end.
--spec format_error([ErrorDetail::term()]) -> iolist().
-format_error(ErrorDetails)
- when erlang:is_list(ErrorDetails) ->
- [[format_detail(ErrorDetail), "\n"] || ErrorDetail <- ErrorDetails].
+%% @doc this is here to comply with the signature. However, we do not actually
+%% produce any errors and so simply return an empty string.
+-spec format_error(any()) -> iolist().
+format_error(_) ->
+ "".
%%%===================================================================
%%% Internal Functions
%%%===================================================================
-resolve_app_metadata(State, LibDirs, OutputDir) ->
- AppMeta0 = lists:flatten(ec_plists:map(fun(LibDir) ->
- discover_dir([OutputDir],
- LibDir)
- end, LibDirs)),
- AppMeta1 = setup_overrides(State, AppMeta0),
-
- Errors = [case El of
- {error, Ret} -> Ret;
- _ -> El
- end
- || El <- AppMeta1,
- case El of
- {error, _} ->
- true;
- _ ->
- false
- end],
-
- case Errors of
- [] ->
- AppMeta2 = lists:flatten(AppMeta1),
- rcl_log:debug(rcl_state:log(State),
- fun() ->
- ["Resolved the following OTP Applications from the system: \n",
- [[rcl_app_info:format(1, App), "\n"] || App <- AppMeta2]]
- end),
- {ok, rcl_state:available_apps(State, AppMeta2)};
- _ ->
- ?RCL_ERROR(Errors)
+%% @doc only add the release if its not documented in the system
+add_if_not_found(Rel, State) ->
+ RelName = rcl_release:name(Rel),
+ RelVsn = rcl_release:vsn(Rel),
+ try
+ rcl_state:get_release(State, RelName, RelVsn),
+ State
+ catch
+ throw:not_found ->
+ rcl_state:add_release(State, Rel)
end.
-app_name({error, _}) ->
- undefined;
-app_name(AppMeta) ->
- rcl_app_info:name(AppMeta).
-
-setup_overrides(State, AppMetas0) ->
- Overrides = rcl_state:overrides(State),
- AppMetas1 = [AppMeta || AppMeta <- AppMetas0,
- not lists:keymember(app_name(AppMeta), 1, Overrides)],
- [case is_valid_otp_app(filename:join([FileName, "ebin",
- erlang:atom_to_list(AppName) ++ ".app"])) of
- [] ->
- {error, {invalid_override, AppName, FileName}};
- Error = {error, _} ->
- Error;
- App ->
- rcl_app_info:link(App, true)
- end || {AppName, FileName} <- Overrides] ++ AppMetas1.
-
get_lib_dirs(State) ->
LibDirs0 = rcl_state:lib_dirs(State),
- add_rebar_deps_dir(State, LibDirs0).
-
--spec add_rebar_deps_dir(rcl_state:t(), [file:name()]) -> [file:name()].
-add_rebar_deps_dir(State, LibDirs) ->
- ExcludeRebar = rcl_state:get(State, discover_exclude_rebar, false),
- case ExcludeRebar of
+ case rcl_state:get(State, disable_default_libs, false) of
true ->
- add_system_lib_dir(State, LibDirs);
+ LibDirs0;
false ->
- %% Check to see if there is a rebar.config. If so then look for a deps
- %% dir. If both are there then we add that to the lib dirs.
- {ok, Cwd} = file:get_cwd(),
-
- RebarConfig = filename:join([Cwd, "rebar.config"]),
- DepsDir = filename:join([Cwd, "deps"]),
- case filelib:is_regular(RebarConfig) andalso filelib:is_dir(DepsDir) of
- true ->
- add_system_lib_dir(State, [filename:absname(Cwd) | LibDirs]);
- false ->
- add_system_lib_dir(State, LibDirs)
- end
+ add_current_dir(State, LibDirs0)
end.
+-spec add_current_dir(rcl_state:t(), [file:name()]) -> [file:name()].
+add_current_dir(State, LibDirs) ->
+ %% Check to see if there is a rebar.config. If so then look for a deps
+ %% dir. If both are there then we add that to the lib dirs.
+ Root = rcl_state:root_dir(State),
+ add_system_lib_dir(State, [filename:absname(Root) | LibDirs]).
+
-spec add_system_lib_dir(rcl_state:t(), [file:name()]) -> [file:name()].
add_system_lib_dir(State, LibDirs) ->
ExcludeSystem = rcl_state:get(State, discover_exclude_system, false),
@@ -151,121 +111,3 @@ add_system_lib_dir(State, LibDirs) ->
LibDirs
end
end.
-
--spec format_detail(ErrorDetail::term()) -> iolist().
-format_detail({error, {invalid_override, AppName, FileName}}) ->
- io_lib:format("Override {~p, ~p} is not a valid OTP App. Perhaps you forgot to build it?",
- [AppName, FileName]);
-format_detail({accessing, File, eaccess}) ->
- io_lib:format("permission denied accessing file ~s", [File]);
-format_detail({accessing, File, Type}) ->
- io_lib:format("error (~p) accessing file ~s", [Type, File]);
-format_detail({no_beam_files, EbinDir}) ->
- io_lib:format("no beam files found in directory ~s", [EbinDir]);
-format_detail({not_a_directory, EbinDir}) ->
- io_lib:format("~s is not a directory when it should be a directory", [EbinDir]);
-format_detail({unable_to_load_app, AppDir, _}) ->
- io_lib:format("Unable to load the application metadata from ~s", [AppDir]);
-format_detail({invalid_app_file, File}) ->
- io_lib:format("Application metadata file exists but is malformed: ~s",
- [File]);
-format_detail({unversioned_app, AppDir, _AppName}) ->
- io_lib:format("Application metadata exists but version is not available: ~s",
- [AppDir]);
-format_detail({app_info_error, {Module, Detail}}) ->
- Module:format_error(Detail).
-
--spec discover_dir([file:name()],
- file:name()) ->
- [rcl_app_info:t() | {error, Reason::term()}] |
- rcl_app_info:t() | {error, Reason::term()}.
-discover_dir(IgnoreDirs, File) ->
- case (not lists:member(File, IgnoreDirs))
- andalso filelib:is_dir(File) of
- true ->
- case file:list_dir(File) of
- {error, Reason} ->
- {error, {accessing, File, Reason}};
- {ok, List} ->
- ec_plists:map(fun(LibDir) ->
- discover_dir(IgnoreDirs, LibDir)
- end, [filename:join([File, Dir]) || Dir <- List])
- end;
- false ->
- is_valid_otp_app(File)
- end.
-
--spec is_valid_otp_app(file:name()) -> rcl_app_info:t() | {error, Reason::term()} | [].
-is_valid_otp_app(File) ->
- %% Is this an ebin dir?
- EbinDir = filename:dirname(File),
- case filename:basename(EbinDir) of
- "ebin" ->
- case lists:suffix(".app", File) of
- true ->
- has_at_least_one_beam(EbinDir, File);
- false ->
- []
- end;
- _ ->
- []
- end.
-
--spec has_at_least_one_beam(file:name(), file:filename()) ->
- rcl_app_info:t() | {error, Reason::term()}.
-has_at_least_one_beam(EbinDir, File) ->
- case file:list_dir(EbinDir) of
- {ok, List} ->
- case lists:any(fun(NFile) -> lists:suffix(".beam", NFile) end, List) of
- true ->
- gather_application_info(EbinDir, File);
- false ->
- {error, {no_beam_files, EbinDir}}
- end;
- _ ->
- {error, {not_a_directory, EbinDir}}
- end.
-
--spec gather_application_info(file:name(), file:filename()) ->
- rcl_app_info:t() | {error, Reason::term()}.
-gather_application_info(EbinDir, File) ->
- AppDir = filename:dirname(EbinDir),
- case file:consult(File) of
- {ok, [{application, AppName, AppDetail}]} ->
- get_vsn(AppDir, AppName, AppDetail);
- {error, Reason} ->
- {error, {unable_to_load_app, AppDir, Reason}};
- _ ->
- {error, {invalid_app_file, File}}
- end.
-
--spec get_vsn(file:name(), atom(), proplists:proplist()) ->
- rcl_app_info:t() | {error, Reason::term()}.
-get_vsn(AppDir, AppName, AppDetail) ->
- case proplists:get_value(vsn, AppDetail) of
- undefined ->
- {error, {unversioned_app, AppDir, AppName}};
- AppVsn ->
- case get_deps(AppDir, AppName, AppVsn, AppDetail) of
- {ok, App} ->
- App;
- {error, Detail} ->
- {error, {app_info_error, Detail}}
- end
- end.
-
--spec get_deps(file:name(), atom(), string(), proplists:proplist()) ->
- {ok, rcl_app_info:t()} | {error, Reason::term()}.
-get_deps(AppDir, AppName, AppVsn, AppDetail) ->
- ActiveApps = proplists:get_value(applications, AppDetail, []),
- LibraryApps = proplists:get_value(included_applications, AppDetail, []),
- rcl_app_info:new(AppName, AppVsn, AppDir, ActiveApps, LibraryApps).
-
-%%%===================================================================
-%%% Test Functions
-%%%===================================================================
-
--ifndef(NOTEST).
--include_lib("eunit/include/eunit.hrl").
-
--endif.
diff --git a/src/rcl_prv_overlay.erl b/src/rcl_prv_overlay.erl
new file mode 100644
index 0000000..40471ea
--- /dev/null
+++ b/src/rcl_prv_overlay.erl
@@ -0,0 +1,418 @@
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
+%%% 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 Given a complete built release this provider assembles that release
+%%% into a release directory.
+-module(rcl_prv_overlay).
+
+-behaviour(rcl_provider).
+
+-export([init/1,
+ do/1,
+ format_error/1]).
+
+-define(DIRECTORY_RE, ".*(\/|\\\\)$").
+
+-include_lib("relcool/include/relcool.hrl").
+
+%%============================================================================
+%% API
+%%============================================================================
+-spec init(rcl_state:t()) -> {ok, rcl_state:t()}.
+init(State) ->
+ {ok, State}.
+
+%% @doc recursively dig down into the library directories specified in the state
+%% looking for OTP Applications
+-spec do(rcl_state:t()) -> {ok, rcl_state:t()} | relcool:error().
+do(State) ->
+ {RelName, RelVsn} = rcl_state:default_release(State),
+ Release = rcl_state:get_release(State, RelName, RelVsn),
+ case rcl_release:realized(Release) of
+ true ->
+ generate_overlay_vars(State, Release);
+ false ->
+ ?RCL_ERROR({unresolved_release, RelName, RelVsn})
+ end.
+
+-spec format_error(ErrorDetail::term()) -> iolist().
+format_error({unresolved_release, RelName, RelVsn}) ->
+ io_lib:format("The release has not been resolved ~p-~s", [RelName, RelVsn]);
+format_error({ec_file_error, AppDir, TargetDir, E}) ->
+ io_lib:format("Unable to copy OTP App from ~s to ~s due to ~p",
+ [AppDir, TargetDir, E]);
+format_error({unable_to_read_varsfile, FileName, Reason}) ->
+ io_lib:format("Unable to read vars file (~s) for overlay due to: ~p",
+ [FileName, Reason]);
+format_error({overlay_failed, Errors}) ->
+ [[format_error(rcl_util:error_reason(Error)), "\n"] || Error <- Errors];
+format_error({dir_render_failed, Dir, Error}) ->
+ io_lib:format("rendering mkdir path failed ~s with ~p",
+ [Dir, Error]);
+format_error({unable_to_make_symlink, AppDir, TargetDir, Reason}) ->
+ io_lib:format("Unable to symlink directory ~s to ~s because \n~s~s",
+ [AppDir, TargetDir, rcl_util:indent(1),
+ file:format_error(Reason)]);
+format_error({copy_failed, FromFile, ToFile, Err}) ->
+ io_lib:format("Unable to copy from ~s to ~s because of ~p",
+ [FromFile, ToFile, Err]);
+format_error({unable_to_write, ToFile, Reason}) ->
+ io_lib:format("Unable to write to ~s because ~p",
+ [ToFile, Reason]);
+format_error({unable_to_enclosing_dir, ToFile, Reason}) ->
+ io_lib:format("Unable to create enclosing directory for ~s because ~p",
+ [ToFile, Reason]);
+format_error({unable_to_render_template, FromFile, Reason}) ->
+ io_lib:format("Unable to render template ~s because ~p",
+ [FromFile, Reason]);
+format_error({unable_to_compile_template, FromFile, Reason}) ->
+ io_lib:format("Unable to compile template ~s because ~p",
+ [FromFile, Reason]);
+format_error({unable_to_make_dir, Absolute, Error}) ->
+ io_lib:format("Unable to make directory ~s because ~p",
+ [Absolute, Error]).
+
+%%%===================================================================
+%%% Internal Functions
+%%%===================================================================
+-spec generate_overlay_vars(rcl_state:t(), rcl_release:t()) ->
+ {ok, rcl_state:t()} | relcool:error().
+generate_overlay_vars(State, Release) ->
+ StateVars = generate_state_vars(State),
+ ReleaseVars = generate_release_vars(Release),
+ get_overlay_vars_from_file(State, StateVars ++ ReleaseVars).
+
+-spec get_overlay_vars_from_file(rcl_state:t(), proplists:proplist()) ->
+ {ok, rcl_state:t()} | relcool:error().
+get_overlay_vars_from_file(State, OverlayVars) ->
+ case rcl_state:get(State, overlay_vars, undefined) of
+ undefined ->
+ do_overlay(State, OverlayVars);
+ FileName ->
+ read_overlay_vars(State, OverlayVars, FileName)
+ end.
+
+-spec read_overlay_vars(rcl_state:t(), proplists:proplist(), file:name()) ->
+ {ok, rcl_state:t()} | relcool:error().
+read_overlay_vars(State, OverlayVars, FileName) ->
+ RelativeRoot = get_relative_root(State),
+ RelativePath = filename:join(RelativeRoot, erlang:iolist_to_binary(FileName)),
+ case file:consult(RelativePath) of
+ {ok, Terms} ->
+ case render_overlay_vars(OverlayVars, Terms, []) of
+ {ok, NewTerms} ->
+ do_overlay(State, NewTerms ++ OverlayVars);
+ Error ->
+ Error
+ end;
+ {error, Reason} ->
+ ?RCL_ERROR({unable_to_read_varsfile, FileName, Reason})
+ end.
+
+-spec render_overlay_vars(proplists:proplist(), proplists:proplist(),
+ proplists:proplist()) ->
+ {ok, proplists:proplist()} | relcool:error().
+render_overlay_vars(OverlayVars, [{Key, Value} | Rest], Acc)
+ when erlang:is_list(Value) ->
+ case io_lib:printable_list(Value) of
+ true ->
+ case render_template(Acc ++ OverlayVars, erlang:iolist_to_binary(Value)) of
+ {ok, Data} ->
+ %% Adding to the end sucks, but ordering needs to be retained
+ render_overlay_vars(OverlayVars, Rest, Acc ++ [{Key, Data}]);
+ Error ->
+ Error
+ end;
+ false ->
+ case render_overlay_vars(Acc ++ OverlayVars, Value, []) of
+ {ok, NewValue} ->
+ render_overlay_vars(OverlayVars, Rest, Acc ++ [{Key, NewValue}]);
+ Error ->
+ Error
+ end
+ end;
+render_overlay_vars(OverlayVars, [{Key, Value} | Rest], Acc)
+ when erlang:is_binary(Value) ->
+ case render_template(Acc ++ OverlayVars, erlang:iolist_to_binary(Value)) of
+ {ok, Data} ->
+ render_overlay_vars(OverlayVars, Rest, Acc ++ [{Key, erlang:iolist_to_binary(Data)}]);
+ Error ->
+ Error
+ end;
+render_overlay_vars(OverlayVars, [KeyValue | Rest], Acc) ->
+ render_overlay_vars(OverlayVars, Rest, Acc ++ [KeyValue]);
+render_overlay_vars(_OverlayVars, [], Acc) ->
+ {ok, Acc}.
+
+-spec generate_release_vars(rcl_release:t()) -> proplists:proplist().
+generate_release_vars(Release) ->
+ [{erts_vsn, rcl_release:erts(Release)},
+ {release_erts_version, rcl_release:erts(Release)},
+ {release_name, rcl_release:name(Release)},
+ {rel_vsn, rcl_release:vsn(Release)},
+ {release_version, rcl_release:vsn(Release)},
+ {release_applications, lists:map(fun(App) ->
+ rcl_app_info:name(App)
+ end, rcl_release:application_details(Release))},
+ {release, [generate_app_vars(App)|| App <- rcl_release:application_details(Release)]},
+ {release_goals, [if
+ erlang:is_list(Constraint) ->
+ Constraint;
+ true ->
+ rcl_depsolver:format_constraint(Constraint)
+ end || Constraint <- rcl_release:goals(Release)]}].
+
+-spec generate_app_vars(rcl_app_info:t()) -> AppInfo::tuple().
+generate_app_vars(App) ->
+ {rcl_app_info:name(App),
+ [{version, rcl_app_info:vsn_as_string(App)},
+ {dir, rcl_app_info:dir(App)},
+ {active_dependencies, rcl_app_info:active_deps(App)},
+ {library_dependencies, rcl_app_info:library_deps(App)},
+ {link, rcl_app_info:link(App)}]}.
+
+-spec generate_state_vars(rcl_state:t()) -> proplists:proplist().
+generate_state_vars(State) ->
+ [{log, rcl_log:format(rcl_state:log(State))},
+ {output_dir, rcl_state:output_dir(State)},
+ {target_dir, rcl_state:output_dir(State)},
+ {overridden, [AppName || {AppName, _} <- rcl_state:overrides(State)]},
+ {overrides, rcl_state:overrides(State)},
+ {goals, [rcl_depsolver:format_constraint(Constraint) ||
+ Constraint <- rcl_state:goals(State)]},
+ {lib_dirs, rcl_state:lib_dirs(State)},
+ {config_file, rcl_state:config_file(State)},
+ {providers, rcl_state:providers(State)},
+ {sys_config, rcl_state:sys_config(State)},
+ {root_dir, rcl_state:root_dir(State)},
+ {default_release_name, case rcl_state:default_release(State) of
+ {Name0, _} ->
+ Name0
+ end},
+ {default_release_version, case rcl_state:default_release(State) of
+ {_, Vsn0} ->
+ Vsn0
+ end},
+ {default_release, case rcl_state:default_release(State) of
+ {Name1, undefined} ->
+ erlang:atom_to_list(Name1);
+ {Name1, Vsn1} ->
+ erlang:atom_to_list(Name1) ++ "-" ++ Vsn1
+ end}].
+
+-spec do_overlay(rcl_state:t(), proplists:proplist()) ->
+ {ok, rcl_state:t()} | relcool:error().
+do_overlay(State, OverlayVars) ->
+ case rcl_state:get(State, overlay, undefined) of
+ undefined ->
+ {ok, State};
+ Overlays ->
+ handle_errors(State,
+ lists:map(fun(Overlay) ->
+ do_individual_overlay(State, OverlayVars,
+ Overlay)
+ end, Overlays))
+ end.
+
+-spec handle_errors(rcl_state:t(), [ok | relcool:error()]) ->
+ {ok, rcl_state:t()} | relcool:error().
+handle_errors(State, Result) ->
+ case [Error || Error <- Result,
+ rcl_util:is_error(Error)] of
+ Errors = [_|_] ->
+ ?RCL_ERROR({overlay_failed, Errors});
+ [] ->
+ {ok, State}
+ end.
+
+-spec do_individual_overlay(rcl_state:t(), proplists:proplist(),
+ OverlayDirective::term()) ->
+ {ok, rcl_state:t()} | relcool:error().
+do_individual_overlay(State, OverlayVars, {mkdir, Dir}) ->
+ ModuleName = make_template_name("rcl_mkdir_template", Dir),
+ case erlydtl:compile(erlang:iolist_to_binary(Dir), ModuleName) of
+ {ok, ModuleName} ->
+ case render(ModuleName, OverlayVars) of
+ {ok, IoList} ->
+ Absolute = absolutize(State,
+ filename:join(rcl_state:output_dir(State),
+ erlang:iolist_to_binary(IoList))),
+ case rcl_util:mkdir_p(Absolute) of
+ {error, Error} ->
+ ?RCL_ERROR({unable_to_make_dir, Absolute, Error});
+ ok ->
+ ok
+ end;
+ {error, Error} ->
+ ?RCL_ERROR({dir_render_failed, Dir, Error})
+ end;
+ {error, Reason} ->
+ ?RCL_ERROR({unable_to_compile_template, Dir, Reason})
+ end;
+do_individual_overlay(State, OverlayVars, {copy, From, To}) ->
+ FromTemplateName = make_template_name("rcl_copy_from_template", From),
+ ToTemplateName = make_template_name("rcl_copy_to_template", To),
+ file_render_do(OverlayVars, From, FromTemplateName,
+ fun(FromFile) ->
+ file_render_do(OverlayVars, To, ToTemplateName,
+ fun(ToFile) ->
+ copy_to(State, FromFile, ToFile)
+ end)
+ end);
+do_individual_overlay(State, OverlayVars, {template, From, To}) ->
+ FromTemplateName = make_template_name("rcl_template_from_template", From),
+ ToTemplateName = make_template_name("rcl_template_to_template", To),
+ file_render_do(OverlayVars, From, FromTemplateName,
+ fun(FromFile) ->
+ file_render_do(OverlayVars, To, ToTemplateName,
+ fun(ToFile) ->
+ RelativeRoot = get_relative_root(State),
+ FromFile0 = absolutize(State,
+ filename:join(RelativeRoot,
+ erlang:iolist_to_binary(FromFile))),
+ FromFile1 = erlang:binary_to_list(FromFile0),
+ write_template(OverlayVars,
+ FromFile1,
+ absolutize(State,
+ filename:join(rcl_state:output_dir(State),
+ erlang:iolist_to_binary(ToFile))))
+ end)
+ end).
+
+-spec copy_to(rcl_state:t(), file:name(), file:name()) -> ok | relcool:error().
+copy_to(State, FromFile0, ToFile0) ->
+ RelativeRoot = get_relative_root(State),
+ ToFile1 = absolutize(State, filename:join(rcl_state:output_dir(State),
+ erlang:iolist_to_binary(ToFile0))),
+
+ FromFile1 = absolutize(State, filename:join(RelativeRoot,
+ erlang:iolist_to_binary(FromFile0))),
+ ToFile2 = case is_directory(ToFile0, ToFile1) of
+ false ->
+ filelib:ensure_dir(ToFile1),
+ ToFile1;
+ true ->
+ rcl_util:mkdir_p(ToFile1),
+ erlang:iolist_to_binary(filename:join(ToFile1,
+ filename:basename(FromFile1)))
+ end,
+ case ec_file:copy(FromFile1, ToFile2) of
+ ok ->
+ {ok, FileInfo} = file:read_file_info(FromFile1),
+ ok = file:write_file_info(ToFile2, FileInfo),
+ ok;
+ {error, Err} ->
+ ?RCL_ERROR({copy_failed,
+ FromFile1,
+ ToFile1, Err})
+ end.
+
+get_relative_root(State) ->
+ case rcl_state:config_file(State) of
+ [] ->
+ rcl_state:root_dir(State);
+ Config ->
+ filename:dirname(Config)
+ end.
+
+-spec is_directory(file:name(), file:name()) -> boolean().
+is_directory(ToFile0, ToFile1) ->
+ case re:run(ToFile0, ?DIRECTORY_RE) of
+ nomatch ->
+ filelib:is_dir(ToFile1);
+ _ ->
+ true
+ end.
+
+
+-spec render_template(proplists:proplist(), iolist()) ->
+ ok | relcool:error().
+render_template(OverlayVars, Data) ->
+ TemplateName = make_template_name("rcl_template_renderer", Data),
+ case erlydtl:compile(Data, TemplateName) of
+ Good when Good =:= ok; Good =:= {ok, TemplateName} ->
+ case render(TemplateName, OverlayVars) of
+ {ok, IoData} ->
+ {ok, IoData};
+ {error, Reason} ->
+ ?RCL_ERROR({unable_to_render_template, Data, Reason})
+ end;
+ {error, Reason} ->
+ ?RCL_ERROR({unable_to_compile_template, Data, Reason})
+ end.
+
+write_template(OverlayVars, FromFile, ToFile) ->
+ case render_template(OverlayVars, FromFile) of
+ {ok, IoData} ->
+ case filelib:ensure_dir(ToFile) of
+ ok ->
+ case file:write_file(ToFile, IoData) of
+ ok ->
+ {ok, FileInfo} = file:read_file_info(FromFile),
+ ok = file:write_file_info(ToFile, FileInfo),
+ ok;
+ {error, Reason} ->
+ ?RCL_ERROR({unable_to_write, ToFile, Reason})
+ end;
+ {error, Reason} ->
+ ?RCL_ERROR({unable_to_enclosing_dir, ToFile, Reason})
+ end;
+ Error ->
+ Error
+ end.
+
+-spec file_render_do(proplists:proplist(), iolist(), module(),
+ fun((term()) -> {ok, rcl_state:t()} | relcool:error())) ->
+ {ok, rcl_state:t()} | relcool:error().
+file_render_do(OverlayVars, Data, TemplateName, NextAction) ->
+ case erlydtl:compile(erlang:iolist_to_binary(Data), TemplateName) of
+ {ok, TemplateName} ->
+ case render(TemplateName, OverlayVars) of
+ {ok, IoList} ->
+ NextAction(IoList);
+ {error, Error} ->
+ ?RCL_ERROR({render_failed, Data, Error})
+ end;
+ {error, Reason} ->
+ ?RCL_ERROR({unable_to_compile_template, Data, Reason})
+ end.
+
+-spec make_template_name(string(), term()) -> module().
+make_template_name(Base, Value) ->
+ %% Seed so we get different values each time
+ random:seed(erlang:now()),
+ Hash = erlang:phash2(Value),
+ Ran = random:uniform(10000000),
+ erlang:list_to_atom(Base ++ "_" ++
+ erlang:integer_to_list(Hash) ++
+ "_" ++ erlang:integer_to_list(Ran)).
+
+-spec render(module(), proplists:proplist()) -> {ok, iolist()} | {error, Reason::term()}.
+render(ModuleName, OverlayVars) ->
+ try
+ ModuleName:render(OverlayVars)
+ catch
+ _:Reason ->
+ {error, Reason}
+ end.
+
+absolutize(State, FileName) ->
+ filename:absname(filename:join(rcl_state:root_dir(State),
+ erlang:iolist_to_binary(FileName))).
diff --git a/src/rcl_prv_release.erl b/src/rcl_prv_release.erl
index 66f4e11..eac1f20 100644
--- a/src/rcl_prv_release.erl
+++ b/src/rcl_prv_release.erl
@@ -1,4 +1,4 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -47,6 +47,8 @@ do(State) ->
find_default_release(State, DepGraph).
-spec format_error(ErrorDetail::term()) -> iolist().
+format_error(no_goals_specified) ->
+ "No goals specified for this release ~n";
format_error({no_release_name, Vsn}) ->
io_lib:format("A target release version was specified (~s) but no name", [Vsn]);
format_error({invalid_release_info, Info}) ->
@@ -99,36 +101,25 @@ find_default_release(State, DepGraph) ->
end.
resolve_default_release(State0, DepGraph) ->
- %% Here we will just get the lastest version and run that.
+ %% Here we will just get the highest versioned release and run that.
case lists:sort(fun release_sort/2,
ec_dictionary:to_list(rcl_state:releases(State0))) of
- All = [{{RelName, RelVsn}, _} | _] ->
+ [{{RelName, RelVsn}, _} | _] ->
State1 = rcl_state:default_release(State0, RelName, RelVsn),
- lists:foldl(fun({{RN, RV}, _}, {ok, State2}) ->
- solve_release(State2,
- DepGraph, RN, RV);
- (_, E) ->
- E
- end, {ok, State1}, All);
+ solve_release(State1, DepGraph, RelName, RelVsn);
[] ->
?RCL_ERROR(no_releases_in_system)
end.
-
resolve_default_version(State0, DepGraph, RelName) ->
%% Here we will just get the lastest version and run that.
AllReleases = ec_dictionary:to_list(rcl_state:releases(State0)),
SpecificReleases = [Rel || Rel={{PossibleRelName, _}, _} <- AllReleases,
PossibleRelName =:= RelName],
case lists:sort(fun release_sort/2, SpecificReleases) of
- All = [{{RelName, RelVsn}, _} | _] ->
+ [{{RelName, RelVsn}, _} | _] ->
State1 = rcl_state:default_release(State0, RelName, RelVsn),
- lists:foldl(fun({RN, RV}, {ok, State2}) ->
- solve_release(State2,
- DepGraph, RN, RV);
- (_, E) ->
- E
- end, {ok, State1}, All);
+ solve_release(State1, DepGraph, RelName, RelVsn);
[] ->
?RCL_ERROR({no_releases_for, RelName})
end.
@@ -148,14 +139,22 @@ release_sort({{RelNameA, RelVsnA}, _}, {{RelNameB, RelVsnB}, _}) ->
ec_semver:lte(RelVsnA, RelVsnB).
solve_release(State0, DepGraph, RelName, RelVsn) ->
+ rcl_log:debug(rcl_state:log(State0),
+ "Solving Release ~p-~s~n",
+ [RelName, RelVsn]),
try
Release = rcl_state:get_release(State0, RelName, RelVsn),
Goals = rcl_release:goals(Release),
- case rcl_depsolver:solve(DepGraph, Goals) of
- {ok, Pkgs} ->
- set_resolved(State0, Release, Pkgs);
- {error, Error} ->
- ?RCL_ERROR({failed_solve, Error})
+ case Goals of
+ [] ->
+ ?RCL_ERROR(no_goals_specified);
+ _ ->
+ case rcl_depsolver:solve(DepGraph, Goals) of
+ {ok, Pkgs} ->
+ set_resolved(State0, Release, Pkgs);
+ {error, Error} ->
+ ?RCL_ERROR({failed_solve, Error})
+ end
end
catch
throw:not_found ->
@@ -169,7 +168,7 @@ set_resolved(State, Release0, Pkgs) ->
"Resolved ~p-~s~n",
[rcl_release:name(Release1),
rcl_release:vsn(Release1)]),
- rcl_log:info(rcl_state:log(State),
+ rcl_log:debug(rcl_state:log(State),
fun() ->
rcl_release:format(1, Release1)
end),
diff --git a/src/rcl_rel_discovery.erl b/src/rcl_rel_discovery.erl
new file mode 100644
index 0000000..0f923a0
--- /dev/null
+++ b/src/rcl_rel_discovery.erl
@@ -0,0 +1,166 @@
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
+%%% 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 This provider uses the lib_dir setting of the state. It searches the
+%%% Lib Dirs looking for all OTP Applications that are available. When it finds
+%%% those OTP Applications it loads the information about them and adds them to
+%%% the state of available apps. This implements the rcl_provider behaviour.
+-module(rcl_rel_discovery).
+
+-export([do/3,
+ format_error/1]).
+
+-include_lib("relcool/include/relcool.hrl").
+
+%%============================================================================
+%% API
+%%============================================================================
+
+%% @doc recursively dig down into the library directories specified in the state
+%% looking for OTP Applications
+-spec do(rcl_state:t(), [filename:name()], [rcl_app_info:t()]) ->
+ {ok, [rcl_release:t()]} | relcool:error().
+do(State, LibDirs, AppMeta) ->
+ rcl_log:info(rcl_state:log(State),
+ fun() ->
+ ["Resolving available releases from directories:\n",
+ [[rcl_util:indent(1), LibDir, "\n"] || LibDir <- LibDirs]]
+ end),
+ resolve_rel_metadata(State, LibDirs, AppMeta).
+
+-spec format_error([ErrorDetail::term()]) -> iolist().
+format_error(ErrorDetails)
+ when erlang:is_list(ErrorDetails) ->
+ [[format_detail(ErrorDetail), "\n"] || ErrorDetail <- ErrorDetails].
+
+%%%===================================================================
+%%% Internal Functions
+%%%===================================================================
+resolve_rel_metadata(State, LibDirs, AppMeta) ->
+ ReleaseMeta0 =
+ lists:flatten(ec_plists:map(fun(LibDir) ->
+ discover_dir([], LibDir, AppMeta)
+ end, LibDirs)),
+ Errors = [case El of
+ {error, Ret} -> Ret;
+ _ -> El
+ end
+ || El <- ReleaseMeta0,
+ case El of
+ {error, _} ->
+ true;
+ _ ->
+ false
+ end],
+
+ case Errors of
+ [] ->
+ ReleaseMeta1 = lists:flatten(ReleaseMeta0),
+ rcl_log:debug(rcl_state:log(State),
+ fun() ->
+ ["Resolved the following OTP Releases from the system: \n",
+ [[rcl_release:format(1, Rel), "\n"] || Rel <- ReleaseMeta1]]
+ end),
+ {ok, ReleaseMeta1};
+ _ ->
+ ?RCL_ERROR(Errors)
+ end.
+
+-spec format_detail(ErrorDetail::term()) -> iolist().
+format_detail({accessing, File, eaccess}) ->
+ io_lib:format("permission denied accessing file ~s", [File]);
+format_detail({accessing, File, Type}) ->
+ io_lib:format("error (~p) accessing file ~s", [Type, File]).
+
+-spec discover_dir([file:name()], file:name(), [rcl_app_info:t()]) ->
+ [rcl_release:t() | {error, Reason::term()}]
+ | rcl_release:t()
+ | {error, Reason::term()}.
+discover_dir(IgnoreDirs, File, AppMeta) ->
+ case (not lists:member(File, IgnoreDirs))
+ andalso filelib:is_dir(File) of
+ true ->
+ case file:list_dir(File) of
+ {error, Reason} ->
+ {error, {accessing, File, Reason}};
+ {ok, List} ->
+ ec_plists:map(fun(LibDir) ->
+ discover_dir(IgnoreDirs, LibDir, AppMeta)
+ end,
+ [filename:join([File, Dir]) || Dir <- List])
+ end;
+ false ->
+ is_valid_release(File, AppMeta)
+ end.
+
+-spec is_valid_release(file:name(),
+ [rcl_app_info:t()]) ->
+ rcl_release:t()
+ | {error, Reason::term()}
+ | [].
+is_valid_release(File, AppMeta) ->
+ case lists:suffix(".rel", File) of
+ true ->
+ resolve_release(File, AppMeta);
+ false ->
+ []
+ end.
+
+resolve_release(RelFile, AppMeta) ->
+ case file:consult(RelFile) of
+ {ok, [{release, {RelName, RelVsn},
+ {erts, ErtsVsn},
+ Apps}]} ->
+ build_release(RelName, RelVsn, ErtsVsn, Apps, AppMeta);
+ {ok, InvalidRelease} ->
+ ?RCL_ERROR({invalid_release_information, InvalidRelease});
+ {error, Reason} ->
+ ?RCL_ERROR({unable_to_read, RelFile, Reason})
+ end.
+
+build_release(RelName, RelVsn, ErtsVsn, Apps, AppMeta) ->
+ Release = rcl_release:erts(rcl_release:new(RelName, RelVsn),
+ ErtsVsn),
+ resolve_apps(Apps, AppMeta, Release, []).
+
+resolve_apps([], _AppMeta, Release, Acc) ->
+ rcl_release:application_details(Release, Acc);
+resolve_apps([AppInfo | Apps], AppMeta, Release, Acc) ->
+ AppName = erlang:element(1, AppInfo),
+ AppVsn = ec_semver:parse(erlang:element(2, AppInfo)),
+ case find_app(AppName, AppVsn, AppMeta) of
+ Error = {error, _} ->
+ Error;
+ ResolvedApp ->
+ resolve_apps(Apps, AppMeta, Release, [ResolvedApp | Acc])
+ end.
+
+find_app(AppName, AppVsn, AppMeta) ->
+ case ec_lists:find(fun(App) ->
+ NAppName = rcl_app_info:name(App),
+ NAppVsn = rcl_app_info:vsn(App),
+ AppName == NAppName andalso
+ AppVsn == NAppVsn
+ end, AppMeta) of
+ {ok, Head} ->
+ Head;
+ error ->
+ ?RCL_ERROR({could_not_find, {AppName, AppVsn}})
+ end.
diff --git a/src/rcl_release.erl b/src/rcl_release.erl
index aa5de72..97465d0 100644
--- a/src/rcl_release.erl
+++ b/src/rcl_release.erl
@@ -1,4 +1,4 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -32,6 +32,7 @@
realize/3,
applications/1,
application_details/1,
+ application_details/2,
realized/1,
metadata/1,
format/1,
@@ -72,7 +73,7 @@
{app_name(), app_vsn(), app_type() | incl_apps()} |
{app_name(), app_vsn(), app_type(), incl_apps()}.
--type application_constraint() :: rcl_depsolver:constraint() | string() | binary().
+-type application_constraint() :: rcl_depsolver:raw_constraint() | string() | binary().
-type application_goal() :: application_constraint()
| {application_constraint(), app_type() | incl_apps()}
| {application_constraint(), app_type(), incl_apps() | none}.
@@ -88,7 +89,7 @@
%%============================================================================
-spec new(atom(), string()) -> t().
new(ReleaseName, ReleaseVsn) ->
- #release_t{name=ReleaseName, vsn=ReleaseVsn,
+ #release_t{name=to_atom(ReleaseName), vsn=ReleaseVsn,
annotations=ec_dictionary:new(ec_dict)}.
-spec name(t()) -> atom().
@@ -134,12 +135,18 @@ applications(#release_t{applications=Apps}) ->
Apps.
%% @doc this gives the rcl_app_info objects representing the applications in
-%% this release. These can only be populated by the 'realize' call in this
-%% module.
+%% this release. These should only be populated by the 'realize' call in this
+%% module or by reading an existing rel file.
-spec application_details(t()) -> [rcl_app_info:t()].
application_details(#release_t{app_detail=App}) ->
App.
+%% @doc this is only expected to be called by a process building a new release
+%% from an existing rel file.
+-spec application_details(t(), [rcl_app_info:t()]) -> t().
+application_details(Release, AppDetail) ->
+ Release#release_t{app_detail=AppDetail}.
+
-spec realized(t()) -> boolean().
realized(#release_t{realized=Realized}) ->
Realized.
@@ -163,7 +170,7 @@ format(Release) ->
format(Indent, #release_t{name=Name, vsn=Vsn, erts=ErtsVsn, realized=Realized,
goals = Goals, applications=Apps}) ->
BaseIndent = rcl_util:indent(Indent),
- [BaseIndent, "release: ", erlang:atom_to_list(Name), "-", Vsn, "\n",
+ [BaseIndent, "release: ", rcl_util:to_string(Name), "-", Vsn, "\n",
rcl_util:indent(Indent + 1), " erts-", ErtsVsn,
", realized = ", erlang:atom_to_list(Realized), "\n",
BaseIndent, "goals: \n",
@@ -189,7 +196,7 @@ format_error({topo_error, E}) ->
rcl_topo:format_error(E);
format_error({failed_to_parse, Con}) ->
io_lib:format("Failed to parse constraint ~p", [Con]);
-format_error({invalid_constraint, Con}) ->
+format_error({invalid_constraint, _, Con}) ->
io_lib:format("Invalid constraint specified ~p", [Con]);
format_error({not_realized, Name, Vsn}) ->
io_lib:format("Unable to produce metadata release: ~p-~s has not been realized",
@@ -209,10 +216,10 @@ realize_erts(Rel) ->
process_specs(Rel=#release_t{annotations=Annots,
goals=Goals}, World) ->
ActiveApps = lists:flatten([rcl_app_info:active_deps(El) || El <- World] ++
- [case get_app_name(Goal) of
- {error, _} -> [];
- G -> G
- end || Goal <- Goals]),
+ [case get_app_name(Goal) of
+ {error, _} -> [];
+ G -> G
+ end || Goal <- Goals]),
LibraryApps = lists:flatten([rcl_app_info:library_deps(El) || El <- World]),
Specs = [create_app_spec(Annots, App, ActiveApps, LibraryApps) || App <- World],
{ok, Rel#release_t{annotations=Annots,
@@ -308,7 +315,7 @@ parse_goal0(Constraint0, {ok, Release}) ->
parse_goal0(_, E = {error, _}) ->
E;
parse_goal0(Constraint, _) ->
- ?RCL_ERROR({invalid_constraint, Constraint}).
+ ?RCL_ERROR({invalid_constraint, 1, Constraint}).
parse_goal1(Release = #release_t{annotations=Annots, goals=Goals},
Constraint, NewAnnots) ->
@@ -331,25 +338,52 @@ parse_constraint(Constraint0)
{ok, Constraint1} ->
{ok, Constraint1}
end;
-parse_constraint(Constraint)
- when erlang:is_tuple(Constraint);
- erlang:is_atom(Constraint) ->
- case rcl_depsolver:is_valid_constraint(Constraint) of
+parse_constraint(Constraint0)
+ when erlang:is_tuple(Constraint0);
+ erlang:is_atom(Constraint0) ->
+ Constraint1 = parse_version(Constraint0),
+ case rcl_depsolver:is_valid_constraint(Constraint1) of
false ->
- ?RCL_ERROR({invalid_constraint, Constraint});
+ ?RCL_ERROR({invalid_constraint, 2, Constraint0});
true ->
- {ok, Constraint}
+ {ok, Constraint1}
end;
parse_constraint(Constraint) ->
- ?RCL_ERROR({invalid_constraint, Constraint}).
+ ?RCL_ERROR({invalid_constraint, 3, Constraint}).
--spec get_app_name(rcl_depsolver:constraint()) ->
+-spec get_app_name(rcl_depsolver:raw_constraint()) ->
AppName::atom() | relcool:error().
get_app_name(AppName) when erlang:is_atom(AppName) ->
AppName;
+get_app_name({AppName, _}) when erlang:is_atom(AppName) ->
+ AppName;
get_app_name({AppName, _, _}) when erlang:is_atom(AppName) ->
AppName;
get_app_name({AppName, _, _, _}) when erlang:is_atom(AppName) ->
AppName;
get_app_name(V) ->
- ?RCL_ERROR({invalid_constraint, V}).
+ ?RCL_ERROR({invalid_constraint, 4, V}).
+
+-spec parse_version(rcl_depsolver:raw_constraint()) ->
+ rcl_depsolver:constraint().
+parse_version({AppName, Version})
+ when erlang:is_binary(Version);
+ erlang:is_list(Version) ->
+ {AppName, rcl_depsolver:parse_version(Version)};
+parse_version({AppName, Version, Constraint})
+ when erlang:is_binary(Version);
+ erlang:is_list(Version) ->
+ {AppName, rcl_depsolver:parse_version(Version), Constraint};
+parse_version({AppName, Version, Constraint0, Constraint1})
+ when erlang:is_binary(Version);
+ erlang:is_list(Version) ->
+ {AppName, rcl_depsolver:parse_version(Version), Constraint1, Constraint0};
+parse_version(Constraint) ->
+ Constraint.
+
+to_atom(RelName)
+ when erlang:is_list(RelName) ->
+ erlang:list_to_atom(RelName);
+to_atom(Else)
+ when erlang:is_atom(Else) ->
+ Else.
diff --git a/src/rcl_state.erl b/src/rcl_state.erl
index 7397e6f..6fd5655 100644
--- a/src/rcl_state.erl
+++ b/src/rcl_state.erl
@@ -1,4 +1,4 @@
-%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -30,11 +30,14 @@
overrides/1,
overrides/2,
goals/1,
- config_files/1,
+ config_file/1,
+ config_file/2,
providers/1,
providers/2,
sys_config/1,
sys_config/2,
+ root_dir/1,
+ root_dir/2,
add_release/2,
get_release/3,
update_release/2,
@@ -57,10 +60,12 @@
cmd_args/0]).
-record(state_t, {log :: rcl_log:t(),
+ root_dir :: file:name(),
caller :: caller(),
+ action :: atom(),
output_dir :: file:name(),
lib_dirs=[] :: [file:name()],
- config_files=[] :: [file:filename()],
+ config_file=[] :: file:filename() | undefined,
goals=[] :: [rcl_depsolver:constraint()],
providers = [] :: [rcl_provider:t()],
available_apps = [] :: [rcl_app_info:t()],
@@ -88,21 +93,28 @@
%% API
%%============================================================================
%% @doc Create a new 'log level' for the system
--spec new(proplists:proplist(), [file:filename()] | file:filename()) -> t().
-new(PropList, Targets) when erlang:is_list(PropList) ->
+-spec new(proplists:proplist(), atom()) -> t().
+new(PropList, Target)
+ when erlang:is_list(PropList),
+ erlang:is_atom(Target) ->
+ {ok, Root} = file:get_cwd(),
State0 =
#state_t{log = proplists:get_value(log, PropList, rcl_log:new(error)),
- output_dir=filename:absname(proplists:get_value(output_dir, PropList, "")),
- lib_dirs=get_lib_dirs(proplists:get_value(lib_dirs, PropList, [])),
- config_files=process_config_files(Targets),
+ output_dir=proplists:get_value(output_dir, PropList, ""),
+ lib_dirs=proplists:get_value(lib_dirs, PropList, ""),
+ config_file=proplists:get_value(config, PropList, undefined),
+ action = Target,
goals=proplists:get_value(goals, PropList, []),
providers = [],
releases=ec_dictionary:new(ec_dict),
config_values=ec_dictionary:new(ec_dict),
overrides = proplists:get_value(overrides, PropList, []),
+ root_dir = proplists:get_value(root_dir, PropList, Root),
default_release={proplists:get_value(relname, PropList, undefined),
proplists:get_value(relvsn, PropList, undefined)}},
- create_logic_providers(State0).
+ rcl_state:put(create_logic_providers(State0),
+ disable_default_libs,
+ proplists:get_value(disable_default_libs, PropList, false)).
%% @doc the application overrides for the system
-spec overrides(t()) -> [{AppName::atom(), Directory::file:filename()}].
@@ -127,14 +139,18 @@ output_dir(#state_t{output_dir=OutDir}) ->
lib_dirs(#state_t{lib_dirs=LibDir}) ->
LibDir.
--spec goals(t()) -> [rcl_depsolver:constraints()].
+-spec goals(t()) -> [rcl_depsolver:constraint()].
goals(#state_t{goals=TS}) ->
TS.
--spec config_files(t()) -> [file:filename()].
-config_files(#state_t{config_files=ConfigFiles}) ->
+-spec config_file(t()) -> file:filename() | undefined.
+config_file(#state_t{config_file=ConfigFiles}) ->
ConfigFiles.
+-spec config_file(t(), file:filename() | undefined) -> t().
+config_file(State, ConfigFiles) ->
+ State#state_t{config_file=ConfigFiles}.
+
-spec providers(t()) -> [rcl_provider:t()].
providers(#state_t{providers=Providers}) ->
Providers.
@@ -147,6 +163,14 @@ sys_config(#state_t{sys_config=SysConfig}) ->
sys_config(State, SysConfig) ->
State#state_t{sys_config=SysConfig}.
+-spec root_dir(t()) -> file:filename() | undefined.
+root_dir(#state_t{root_dir=RootDir}) ->
+ RootDir.
+
+-spec root_dir(t(), file:filename()) -> t().
+root_dir(State, RootDir) ->
+ State#state_t{root_dir=RootDir}.
+
-spec providers(t(), [rcl_provider:t()]) -> t().
providers(M, NewProviders) ->
M#state_t{providers=NewProviders}.
@@ -225,15 +249,14 @@ format(Mod) ->
-spec format(t(), non_neg_integer()) -> iolist().
format(#state_t{log=LogState, output_dir=OutDir, lib_dirs=LibDirs,
caller=Caller, config_values=Values0,
- goals=Goals, config_files=ConfigFiles,
+ goals=Goals, config_file=ConfigFile,
providers=Providers},
Indent) ->
Values1 = ec_dictionary:to_list(Values0),
[rcl_util:indent(Indent),
<<"state(">>, erlang:atom_to_list(Caller), <<"):\n">>,
rcl_util:indent(Indent + 1), <<"log: ">>, rcl_log:format(LogState), <<",\n">>,
- rcl_util:indent(Indent + 1), "config files: \n",
- [[rcl_util:indent(Indent + 2), ConfigFile, ",\n"] || ConfigFile <- ConfigFiles],
+ rcl_util:indent(Indent + 1), "config file: ", rcl_util:optional_to_string(ConfigFile), "\n",
rcl_util:indent(Indent + 1), "goals: \n",
[[rcl_util:indent(Indent + 2), rcl_depsolver:format_constraint(Goal), ",\n"] || Goal <- Goals],
rcl_util:indent(Indent + 1), "output_dir: ", OutDir, "\n",
@@ -247,36 +270,16 @@ format(#state_t{log=LogState, output_dir=OutDir, lib_dirs=LibDirs,
%%%===================================================================
%%% Internal Functions
%%%===================================================================
--spec get_lib_dirs([file:name()]) -> [file:name()].
-get_lib_dirs(CmdDirs) ->
- case os:getenv("ERL_LIBS") of
- false ->
- CmdDirs;
- EnvString ->
- [Lib || Lib <- re:split(EnvString, ":|;"),
- filelib:is_dir(Lib)] ++ CmdDirs
- end.
-spec create_logic_providers(t()) -> t().
create_logic_providers(State0) ->
{ConfigProvider, {ok, State1}} = rcl_provider:new(rcl_prv_config, State0),
{DiscoveryProvider, {ok, State2}} = rcl_provider:new(rcl_prv_discover, State1),
{ReleaseProvider, {ok, State3}} = rcl_provider:new(rcl_prv_release, State2),
- {AssemblerProvider, {ok, State4}} = rcl_provider:new(rcl_prv_assembler, State3),
- State4#state_t{providers=[ConfigProvider, DiscoveryProvider,
- ReleaseProvider, AssemblerProvider]}.
-
-
-%% @doc config files can come in as either a single file name or as a list of
-%% files. We what to support both where possible.
-process_config_files(File = [Char | _])
- when erlang:is_integer(Char) ->
- [File];
-process_config_files(Files = [File | _])
- when erlang:is_list(File) ->
- Files;
-process_config_files([]) ->
- [].
+ {OverlayProvider, {ok, State4}} = rcl_provider:new(rcl_prv_overlay, State3),
+ {AssemblerProvider, {ok, State5}} = rcl_provider:new(rcl_prv_assembler, State4),
+ State5#state_t{providers=[ConfigProvider, DiscoveryProvider,
+ ReleaseProvider, OverlayProvider, AssemblerProvider]}.
%%%===================================================================
%%% Test Functions
@@ -287,7 +290,7 @@ process_config_files([]) ->
new_test() ->
LogState = rcl_log:new(error),
- RCLState = new([{log, LogState}], []),
+ RCLState = new([{log, LogState}], release),
?assertMatch(LogState, log(RCLState)).
-endif.
diff --git a/src/rcl_topo.erl b/src/rcl_topo.erl
index ec67b56..462b7c5 100644
--- a/src/rcl_topo.erl
+++ b/src/rcl_topo.erl
@@ -1,4 +1,19 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
+%%% 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 Joe Armstrong
%%% @author Eric Merritt
diff --git a/src/rcl_util.erl b/src/rcl_util.erl
index f6427ae..61e1392 100644
--- a/src/rcl_util.erl
+++ b/src/rcl_util.erl
@@ -1,4 +1,4 @@
-%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -23,7 +23,11 @@
-export([mkdir_p/1,
to_binary/1,
- indent/1]).
+ to_string/1,
+ is_error/1,
+ error_reason/1,
+ indent/1,
+ optional_to_string/1]).
-define(ONE_LEVEL_INDENT, " ").
%%============================================================================
@@ -54,7 +58,27 @@ to_binary(String) when erlang:is_list(String) ->
erlang:iolist_to_binary(String);
to_binary(Bin) when erlang:is_binary(Bin) ->
Bin.
+to_string(Atom) when erlang:is_atom(Atom) ->
+ erlang:atom_to_list(Atom);
+to_string(Else) when erlang:is_list(Else) ->
+ Else.
+%% @doc get the reason for a particular relcool error
+-spec error_reason(relcool:error()) -> any().
+error_reason({error, {_, Reason}}) ->
+ Reason.
+%% @doc check to see if the value is a relcool error
+-spec is_error(relcool:error() | any()) -> boolean().
+is_error({error, _}) ->
+ true;
+is_error(_) ->
+ false.
+
+%% @doc convert optional argument to empty string if undefined
+optional_to_string(undefined) ->
+ "";
+optional_to_string(Value) when is_list(Value) ->
+ Value.
%%%===================================================================
%%% Test Functions
diff --git a/src/relcool.erl b/src/relcool.erl
index 0d75c62..c8c0cde 100644
--- a/src/relcool.erl
+++ b/src/relcool.erl
@@ -1,4 +1,4 @@
-%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -23,6 +23,7 @@
-export([main/1,
do/7,
do/8,
+ do/9,
format_error/1,
opt_spec_list/0]).
@@ -35,6 +36,7 @@
%%============================================================================
-type error() :: {error, {Module::module(), Reason::term()}}.
+-type goal() :: string() | binary() | rcl_depsolver:constraint().
%%============================================================================
%% API
@@ -42,12 +44,20 @@
-spec main([string()]) -> ok | error() | {ok, rcl_state:t()}.
main(Args) ->
OptSpecList = opt_spec_list(),
- case rcl_cmd_args:args2state(getopt:parse(OptSpecList, Args)) of
- {ok, {State, _Target}} ->
- run_relcool_process(rcl_state:caller(State, command_line));
- Error={error, _} ->
- report_error(rcl_state:caller(rcl_state:new([], []),
- command_line), Error)
+ Result =
+ case rcl_cmd_args:args2state(getopt:parse(OptSpecList, Args)) of
+ {ok, State} ->
+ run_relcool_process(rcl_state:caller(State, command_line));
+ Error={error, _} ->
+ Error
+ end,
+ case Result of
+ {error, _} ->
+ report_error(rcl_state:caller(rcl_state:new([], undefined),
+ command_line),
+ Result);
+ _ ->
+ Result
end.
%% @doc provides an API to run the Relcool process from erlang applications
@@ -59,11 +69,32 @@ main(Args) ->
%% @param LibDirs - The library dirs that should be used for the system
%% @param OutputDir - The directory where the release should be built to
%% @param Configs - The list of config files for the system
-do(RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, Configs) ->
- do(RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, [], Configs).
+-spec do(atom(), string(), [goal()], [file:name()], rcl_log:log_level(),
+ [file:name()], file:name()) ->
+ ok | error() | {ok, rcl_state:t()}.
+do(RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, Config) ->
+ {ok, Cwd} = file:get_cwd(),
+ do(Cwd, RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, [], Config).
+
+%% @doc provides an API to run the Relcool process from erlang applications
+%%
+%% @param RootDir - The root directory for the project
+%% @param RelName - The release name to build (maybe `undefined`)
+%% @param RelVsn - The release version to build (maybe `undefined`)
+%% @param Goals - The release goals for the system in depsolver or Relcool goal
+%% format
+%% @param LibDirs - The library dirs that should be used for the system
+%% @param OutputDir - The directory where the release should be built to
+%% @param Configs - The list of config files for the system
+-spec do(file:name(), atom(), string(), [goal()], [file:name()],
+ rcl_log:log_level(), [file:name()], file:name()) ->
+ ok | error() | {ok, rcl_state:t()}.
+do(RootDir, RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, Configs) ->
+ do(RootDir, RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, [], Configs).
%% @doc provides an API to run the Relcool process from erlang applications
%%
+%% @param RootDir - The root directory for the system
%% @param RelName - The release name to build (maybe `undefined`)
%% @param RelVsn - The release version to build (maybe `undefined`)
%% @param Goals - The release goals for the system in depsolver or Relcool goal
@@ -72,29 +103,41 @@ do(RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, Configs) ->
%% @param OutputDir - The directory where the release should be built to
%% @param Overrides - A list of overrides for the system
%% @param Configs - The list of config files for the system
-do(RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, Overrides, Configs) ->
+-spec do(file:name(), atom(), string(), [goal()], [file:name()],
+ rcl_log:log_level(), [file:name()], [{atom(), file:name()}], file:name()) ->
+ ok | error() | {ok, rcl_state:t()}.
+do(RootDir, RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, Overrides, Config) ->
State = rcl_state:new([{relname, RelName},
{relvsn, RelVsn},
{goals, Goals},
{overrides, Overrides},
{output_dir, OutputDir},
{lib_dirs, LibDirs},
- {log, rcl_log:new(LogLevel)}],
- Configs),
+ {root_dir, RootDir},
+ {log, rcl_log:new(LogLevel)},
+ {config, Config}],
+ release),
run_relcool_process(rcl_state:caller(State, api)).
-spec opt_spec_list() -> [getopt:option_spec()].
opt_spec_list() ->
- [
- {relname, $n, "relname", string, "Specify the name for the release that will be generated"},
+ [{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"}
- ].
+ {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"},
+ {disable_default_libs, undefined, "disable-default-libs",
+ {boolean, false},
+ "Disable the default system added lib dirs (means you must add them all manually"},
+ {log_level, $V, "verbose", {integer, 1},
+ "Verbosity level, maybe between 0 and 2"},
+ {config, $c, "config", {string, ""}, "The path to a config file"},
+ {root_dir, $r, "root", string, "The project root directory"}].
-spec format_error(Reason::term()) -> iolist().
format_error({invalid_return_value, Provider, Value}) ->
@@ -103,7 +146,6 @@ format_error({invalid_return_value, Provider, Value}) ->
format_error({error, {Module, Reason}}) ->
io_lib:format("~s~n", [Module:format_error(Reason)]).
-
%%============================================================================
%% internal api
%%============================================================================
@@ -126,6 +168,8 @@ run_providers(State0) ->
Err = {error, _} ->
Err;
{ok, State1} ->
+ RootDir = rcl_state:root_dir(State1),
+ ok = file:set_cwd(RootDir),
Providers = rcl_state:providers(State1),
Result = run_providers(ConfigProvider, Providers, State1),
handle_output(State1, rcl_state:caller(State1), Result)
@@ -158,9 +202,12 @@ run_provider(Provider, {ok, State0}) ->
[rcl_provider:impl(Provider)]),
case rcl_provider:do(Provider, State0) of
{ok, State1} ->
+ rcl_log:debug(rcl_state:log(State0), "Provider successfully run: ~p~n",
+ [rcl_provider:impl(Provider)]),
{ok, State1};
E={error, _} ->
-
+ rcl_log:debug(rcl_state:log(State0), "Provider (~p) failed with: ~p~n",
+ [rcl_provider:impl(Provider), E]),
E
end.
@@ -168,7 +215,6 @@ run_provider(Provider, {ok, State0}) ->
usage() ->
getopt:usage(opt_spec_list(), "relcool", "[*release-specification-file*]").
-
-spec report_error(rcl_state:t(), error()) -> none() | error().
report_error(State, Error) ->
io:format(format_error(Error)),
diff --git a/test/rclt_command_SUITE.erl b/test/rclt_command_SUITE.erl
index 46449e9..3d06dad 100644
--- a/test/rclt_command_SUITE.erl
+++ b/test/rclt_command_SUITE.erl
@@ -1,4 +1,4 @@
-%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 92 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -25,7 +25,6 @@
all/0,
normal_passing_case/1,
lib_fail_case/1,
- output_fail_case/1,
spec_parse_fail_case/1,
config_fail_case/1]).
@@ -42,7 +41,7 @@ end_per_suite(_Config) ->
ok.
all() ->
- [normal_passing_case, lib_fail_case, output_fail_case, config_fail_case].
+ [normal_passing_case, lib_fail_case, config_fail_case].
normal_passing_case(Config) ->
DataDir = proplists:get_value(data_dir, Config),
@@ -59,8 +58,7 @@ normal_passing_case(Config) ->
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),
+ {ok, State} = rcl_cmd_args:args2state(getopt:parse(relcool:opt_spec_list(), CmdLine)),
?assertMatch([Lib1, Lib2],
rcl_state:lib_dirs(State)),
?assertMatch(Outdir, rcl_state:output_dir(State)),
@@ -82,18 +80,6 @@ lib_fail_case(Config) ->
?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],
@@ -102,6 +88,6 @@ spec_parse_fail_case(_Config) ->
config_fail_case(_Config) ->
ConfigFile = "does-not-exist",
- CmdLine = [ConfigFile],
+ CmdLine = ["-c", ConfigFile],
?assertMatch({error, {_, {invalid_config_file, ConfigFile}}},
rcl_cmd_args:args2state(getopt:parse(relcool:opt_spec_list(), CmdLine))).
diff --git a/test/rclt_discover_SUITE.erl b/test/rclt_discover_SUITE.erl
index 63d7841..4b6cb2a 100644
--- a/test/rclt_discover_SUITE.erl
+++ b/test/rclt_discover_SUITE.erl
@@ -1,4 +1,4 @@
-%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 92 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -46,7 +46,7 @@ init_per_testcase(_, Config) ->
LibDir2 = filename:join([DataDir, create_random_name("lib_dir2_")]),
ok = rcl_util:mkdir_p(LibDir1),
ok = rcl_util:mkdir_p(LibDir2),
- State = rcl_state:new([{lib_dirs, [LibDir1, LibDir2]}], []),
+ State = rcl_state:new([{lib_dirs, [LibDir1, LibDir2]}], release),
[{lib1, LibDir1},
{lib2, LibDir2},
{state, State} | Config].
@@ -62,8 +62,8 @@ normal_case(Config) ->
end)(App)
||
App <-
- [{create_random_name("lib_app1_"), create_random_vsn()}
- || _ <- lists:seq(1, 100)]],
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
LibDir2 = proplists:get_value(lib2, Config),
Apps2 = [(fun({Name, Vsn}) ->
@@ -71,10 +71,9 @@ normal_case(Config) ->
end)(App)
|| App <-
[{create_random_name("lib_app2_"), create_random_vsn()}
- || _ <- lists:seq(1, 100)]],
- State0 = rcl_state:put(rcl_state:put(proplists:get_value(state, Config),
- discover_exclude_rebar, true),
- discover_exclude_system, true),
+ || _ <- lists:seq(1, 100)]],
+ State0 = rcl_state:put(proplists:get_value(state, Config),
+ disable_default_libs, true),
{DiscoverProvider, {ok, State1}} = rcl_provider:new(rcl_prv_discover, State0),
{ok, State2} = rcl_provider:do(DiscoverProvider, State1),
lists:foreach(fun(App) ->
diff --git a/test/rclt_goal.erl b/test/rclt_goal.erl
index 9bf6028..20fb5e5 100644
--- a/test/rclt_goal.erl
+++ b/test/rclt_goal.erl
@@ -1,4 +1,4 @@
-%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 92 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
diff --git a/test/rclt_release_SUITE.erl b/test/rclt_release_SUITE.erl
index c13672a..1104303 100644
--- a/test/rclt_release_SUITE.erl
+++ b/test/rclt_release_SUITE.erl
@@ -1,4 +1,4 @@
-%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 92 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
@@ -25,11 +25,20 @@
init_per_testcase/2,
all/0,
make_release/1,
- make_overridden_release/1]).
+ make_scriptless_release/1,
+ make_overridden_release/1,
+ make_rerun_overridden_release/1,
+ make_implicit_config_release/1,
+ overlay_release/1,
+ make_goalless_release/1,
+ make_depfree_release/1,
+ make_invalid_config_release/1,
+ make_relup_release/1]).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("kernel/include/file.hrl").
+-include_lib("kernel/include/file.hrl").
suite() ->
[{timetrap,{seconds,30}}].
@@ -44,12 +53,15 @@ init_per_testcase(_, Config) ->
DataDir = proplists:get_value(data_dir, Config),
LibDir1 = filename:join([DataDir, create_random_name("lib_dir1_")]),
ok = rcl_util:mkdir_p(LibDir1),
- State = rcl_state:new([{lib_dirs, [LibDir1]}], []),
+ State = rcl_state:new([{lib_dirs, [LibDir1]}], release),
[{lib1, LibDir1},
{state, State} | Config].
all() ->
- [make_release, make_overridden_release].
+ [make_release, make_scriptless_release, make_overridden_release,
+ make_implicit_config_release, make_rerun_overridden_release,
+ overlay_release, make_goalless_release, make_depfree_release,
+ make_invalid_config_release, make_relup_release].
make_release(Config) ->
LibDir1 = proplists:get_value(lib1, Config),
@@ -75,7 +87,74 @@ make_release(Config) ->
OutputDir = filename:join([proplists:get_value(data_dir, Config),
create_random_name("relcool-output")]),
{ok, State} = relcool:do(undefined, undefined, [], [LibDir1], 2,
- OutputDir, [ConfigFile]),
+ OutputDir, ConfigFile),
+ [{{foo, "0.0.1"}, Release}] = ec_dictionary:to_list(rcl_state:releases(State)),
+ AppSpecs = rcl_release:applications(Release),
+ ?assert(lists:keymember(stdlib, 1, AppSpecs)),
+ ?assert(lists:keymember(kernel, 1, AppSpecs)),
+ ?assert(lists:member({non_goal_1, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({non_goal_2, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({goal_app_1, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({goal_app_2, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({lib_dep_1, "0.0.1", load}, AppSpecs)).
+
+make_invalid_config_release(Config) ->
+ LibDir1 = proplists:get_value(lib1, Config),
+ [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn, [kernel, stdlib], [])
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+
+ create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []),
+ create_app(LibDir1, "lib_dep_1", "0.0.1", [stdlib,kernel], []),
+ create_app(LibDir1, "goal_app_2", "0.0.1", [stdlib,kernel,goal_app_1,non_goal_2], []),
+ create_app(LibDir1, "non_goal_1", "0.0.1", [stdlib,kernel], [lib_dep_1]),
+ create_app(LibDir1, "non_goal_2", "0.0.1", [stdlib,kernel], []),
+
+ ConfigFile = filename:join([LibDir1, "relcool.config"]),
+ ok = ec_file:write(ConfigFile,
+ "{release, {foo, \"0.0.1\"},
+ [goal_app_1,
+ goal_app_2,]}"),
+ OutputDir = filename:join([proplists:get_value(data_dir, Config),
+ create_random_name("relcool-output")]),
+ {error, {rcl_prv_config,
+ {consult, _, _}}} = relcool:do(undefined, undefined, [], [LibDir1], 2,
+ OutputDir, ConfigFile).
+
+make_scriptless_release(Config) ->
+ LibDir1 = proplists:get_value(lib1, Config),
+ [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn, [kernel, stdlib], [])
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+
+ create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []),
+ create_app(LibDir1, "lib_dep_1", "0.0.1", [stdlib,kernel], []),
+ create_app(LibDir1, "goal_app_2", "0.0.1", [stdlib,kernel,goal_app_1,non_goal_2], []),
+ create_app(LibDir1, "non_goal_1", "0.0.1", [stdlib,kernel], [lib_dep_1]),
+ create_app(LibDir1, "non_goal_2", "0.0.1", [stdlib,kernel], []),
+
+ ConfigFile = filename:join([LibDir1, "relcool.config"]),
+ write_config(ConfigFile,
+ [{generate_start_script, false},
+ {release, {foo, "0.0.1"},
+ [goal_app_1,
+ goal_app_2]}]),
+ OutputDir = filename:join([proplists:get_value(data_dir, Config),
+ create_random_name("relcool-output")]),
+ {ok, State} = relcool:do(undefined, undefined, [], [LibDir1], 2,
+ OutputDir, ConfigFile),
+
+ ?assert(not ec_file:exists(filename:join([OutputDir, "bin", "foo"]))),
+ ?assert(not ec_file:exists(filename:join([OutputDir, "bin", "foo-0.0.1"]))),
+
[{{foo, "0.0.1"}, Release}] = ec_dictionary:to_list(rcl_state:releases(State)),
AppSpecs = rcl_release:applications(Release),
?assert(lists:keymember(stdlib, 1, AppSpecs)),
@@ -100,7 +179,7 @@ make_overridden_release(Config) ->
|| _ <- lists:seq(1, 100)]],
OverrideApp = create_random_name("override_app"),
OverrideVsn = create_random_vsn(),
- OverrideAppDir = filename:join(OverrideDir1, OverrideApp),
+ OverrideAppDir = filename:join(OverrideDir1, OverrideApp ++ "-" ++ OverrideVsn),
OverrideAppName = erlang:list_to_atom(OverrideApp),
create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []),
@@ -119,9 +198,10 @@ make_overridden_release(Config) ->
goal_app_2]}]),
OutputDir = filename:join([proplists:get_value(data_dir, Config),
create_random_name("relcool-output")]),
- {ok, State} = relcool:do(undefined, undefined, [], [LibDir1], 2,
+ {ok, Cwd} = file:get_cwd(),
+ {ok, State} = relcool:do(Cwd, undefined, undefined, [], [LibDir1], 2,
OutputDir, [{OverrideAppName, OverrideAppDir}],
- [ConfigFile]),
+ ConfigFile),
[{{foo, "0.0.1"}, Release}] = ec_dictionary:to_list(rcl_state:releases(State)),
AppSpecs = rcl_release:applications(Release),
?assert(lists:keymember(stdlib, 1, AppSpecs)),
@@ -136,13 +216,359 @@ make_overridden_release(Config) ->
OverrideApp ++ "-" ++ OverrideVsn])),
?assertMatch(OverrideAppDir, Real).
+make_implicit_config_release(Config) ->
+ LibDir1 = proplists:get_value(lib1, Config),
+ FooRoot = filename:join([LibDir1, "foodir1", "foodir2"]),
+ filelib:ensure_dir(filename:join([FooRoot, "tmp"])),
+ [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn, [kernel, stdlib], [])
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+
+ create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []),
+ create_app(LibDir1, "lib_dep_1", "0.0.1", [stdlib,kernel], []),
+ create_app(LibDir1, "goal_app_2", "0.0.1", [stdlib,kernel,goal_app_1,non_goal_2], []),
+ create_app(LibDir1, "non_goal_1", "0.0.1", [stdlib,kernel], [lib_dep_1]),
+ create_app(LibDir1, "non_goal_2", "0.0.1", [stdlib,kernel], []),
+
+ ConfigFile = filename:join([LibDir1, "relcool.config"]),
+ write_config(ConfigFile,
+ [{release, {foo, "0.0.1"},
+ [goal_app_1,
+ goal_app_2]}]),
+ OutputDir = filename:join([proplists:get_value(data_dir, Config),
+ create_random_name("relcool-output")]),
+ ok = file:set_cwd(FooRoot),
+ {ok, FooRoot} = file:get_cwd(),
+ {ok, State} = relcool:do(undefined, undefined, [], [LibDir1], 2,
+ OutputDir, []),
+ [{{foo, "0.0.1"}, Release}] = ec_dictionary:to_list(rcl_state:releases(State)),
+ ?assert(ec_file:exists(OutputDir)),
+ AppSpecs = rcl_release:applications(Release),
+ ?assert(lists:keymember(stdlib, 1, AppSpecs)),
+ ?assert(lists:keymember(kernel, 1, AppSpecs)),
+ ?assert(lists:member({non_goal_1, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({non_goal_2, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({goal_app_1, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({goal_app_2, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({lib_dep_1, "0.0.1", load}, AppSpecs)).
+
+make_rerun_overridden_release(Config) ->
+ DataDir = proplists:get_value(data_dir, Config),
+ OverrideDir1 = filename:join([DataDir, create_random_name("override_dir_")]),
+ LibDir1 = proplists:get_value(lib1, Config),
+ [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn, [kernel, stdlib], [])
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+ OverrideApp = create_random_name("override_app"),
+ OverrideVsn = create_random_vsn(),
+ OverrideAppDir = filename:join(OverrideDir1, OverrideApp ++ "-"
+ ++ OverrideVsn),
+ OverrideAppName = erlang:list_to_atom(OverrideApp),
+
+ create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []),
+ create_app(LibDir1, "lib_dep_1", "0.0.1", [stdlib,kernel], []),
+ create_app(LibDir1, "goal_app_2", "0.0.1", [stdlib,kernel,goal_app_1,non_goal_2], []),
+ create_app(LibDir1, "non_goal_1", "0.0.1", [stdlib,kernel], [lib_dep_1]),
+ create_app(LibDir1, "non_goal_2", "0.0.1", [stdlib,kernel], []),
+ create_app(OverrideDir1, OverrideApp, OverrideVsn, [stdlib,kernel], []),
+
+ ConfigFile = filename:join([LibDir1, "relcool.config"]),
+ write_config(ConfigFile,
+ [{release, {foo, "0.0.1"},
+ [goal_app_1,
+ erlang:list_to_atom(OverrideApp),
+ goal_app_2]}]),
+ OutputDir = filename:join([proplists:get_value(data_dir, Config),
+ create_random_name("relcool-output")]),
+ {ok, Cwd} = file:get_cwd(),
+ {ok, State} = relcool:do(Cwd, undefined, undefined, [], [LibDir1], 2,
+ OutputDir, [{OverrideAppName, OverrideAppDir}],
+ ConfigFile),
+
+ %% Now we run it again to see if it failse.
+ {ok, State} = relcool:do(Cwd,undefined, undefined, [], [LibDir1], 2,
+ OutputDir, [{OverrideAppName, OverrideAppDir}],
+ ConfigFile),
+
+ [{{foo, "0.0.1"}, Release}] = ec_dictionary:to_list(rcl_state:releases(State)),
+ AppSpecs = rcl_release:applications(Release),
+ ?assert(lists:keymember(stdlib, 1, AppSpecs)),
+ ?assert(lists:keymember(kernel, 1, AppSpecs)),
+ ?assert(lists:member({non_goal_1, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({non_goal_2, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({goal_app_1, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({goal_app_2, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({OverrideAppName, OverrideVsn}, AppSpecs)),
+ ?assert(lists:member({lib_dep_1, "0.0.1", load}, AppSpecs)),
+ {ok, Real} = file:read_link(filename:join([OutputDir, "lib",
+ OverrideApp ++ "-" ++ OverrideVsn])),
+ ?assertMatch(OverrideAppDir, Real).
+
+overlay_release(Config) ->
+ LibDir1 = proplists:get_value(lib1, Config),
+ [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn, [kernel, stdlib], [])
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+
+ create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []),
+ create_app(LibDir1, "lib_dep_1", "0.0.1", [stdlib,kernel], []),
+ create_app(LibDir1, "goal_app_2", "0.0.1", [stdlib,kernel,goal_app_1,non_goal_2], []),
+ create_app(LibDir1, "non_goal_1", "0.0.1", [stdlib,kernel], [lib_dep_1]),
+ create_app(LibDir1, "non_goal_2", "0.0.1", [stdlib,kernel], []),
+
+ ConfigFile = filename:join([LibDir1, "relcool.config"]),
+ OverlayVars = filename:join([LibDir1, "vars.config"]),
+ Template = filename:join([LibDir1, "test_template"]),
+ write_config(ConfigFile,
+ [{overlay_vars, OverlayVars},
+ {overlay, [{mkdir, "{{target_dir}}/fooo"},
+ {copy, OverlayVars,
+ "{{target_dir}}/{{foo_dir}}/vars.config"},
+ {copy, OverlayVars,
+ "{{target_dir}}/{{yahoo}}/"},
+ {template, Template,
+ "{{target_dir}}/test_template_resolved"}]},
+ {release, {foo, "0.0.1"},
+ [goal_app_1,
+ goal_app_2]}]),
+
+ VarsFile = filename:join([LibDir1, "vars.config"]),
+ write_config(VarsFile, [{yahoo, "yahoo"},
+ {yahoo2, [{foo, "bar"}]},
+ {yahoo3, [{bar, "{{yahoo}}/{{yahoo2.foo}}"}]},
+ {foo_dir, "foodir"}]),
+
+ TemplateFile = filename:join([LibDir1, "test_template"]),
+ ok = file:write_file(TemplateFile, test_template_contents()),
+ {ok, FileInfo} = file:read_file_info(TemplateFile),
+ ok = file:write_file_info(TemplateFile, FileInfo#file_info{mode=8#00777}),
+
+ OutputDir = filename:join([proplists:get_value(data_dir, Config),
+ create_random_name("relcool-output")]),
+
+ {ok, State} = relcool:do(undefined, undefined, [], [LibDir1], 2,
+ OutputDir, ConfigFile),
+
+ [{{foo, "0.0.1"}, Release}] = ec_dictionary:to_list(rcl_state:releases(State)),
+ AppSpecs = rcl_release:applications(Release),
+ ?assert(lists:keymember(stdlib, 1, AppSpecs)),
+ ?assert(lists:keymember(kernel, 1, AppSpecs)),
+ ?assert(lists:member({non_goal_1, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({non_goal_2, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({goal_app_1, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({goal_app_2, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({lib_dep_1, "0.0.1", load}, AppSpecs)),
+
+ ?assert(ec_file:exists(filename:join(OutputDir, "fooo"))),
+ ?assert(ec_file:exists(filename:join([OutputDir, "foodir", "vars.config"]))),
+ ?assert(ec_file:exists(filename:join([OutputDir, "yahoo", "vars.config"]))),
+
+ TemplateData = case file:consult(filename:join([OutputDir, "test_template_resolved"])) of
+ {ok, Details} ->
+ Details;
+ Error ->
+ erlang:throw({failed_to_consult, Error})
+ end,
+ {ok, ReadFileInfo} = file:read_file_info(filename:join([OutputDir, "test_template_resolved"])),
+ ?assertEqual(8#100777, ReadFileInfo#file_info.mode),
+
+ ?assertEqual(erlang:system_info(version),
+ proplists:get_value(erts_vsn, TemplateData)),
+ ?assertEqual(erlang:system_info(version),
+ proplists:get_value(release_erts_version, TemplateData)),
+ ?assertEqual("0.0.1",
+ proplists:get_value(release_version, TemplateData)),
+ ?assertEqual(foo,
+ proplists:get_value(release_name, TemplateData)),
+ ?assertEqual([kernel,stdlib,lib_dep_1,non_goal_2,non_goal_1,
+ goal_app_1,goal_app_2],
+ proplists:get_value(release_applications, TemplateData)),
+ ?assert(proplists:is_defined(std_version, TemplateData)),
+ ?assert(proplists:is_defined(kernel_version, TemplateData)),
+ ?assertEqual("0.0.1",
+ proplists:get_value(non_goal_1_version, TemplateData)),
+ ?assertEqual("0.0.1",
+ proplists:get_value(non_goal_2_version, TemplateData)),
+ ?assertEqual("0.0.1",
+ proplists:get_value(goal_app_1_version, TemplateData)),
+ ?assertEqual("0.0.1",
+ proplists:get_value(goal_app_2_version, TemplateData)),
+ ?assertEqual("0.0.1",
+ proplists:get_value(lib_dep_1, TemplateData)),
+ ?assert(proplists:is_defined(lib_dep_1_dir, TemplateData)),
+ ?assertEqual([stdlib,kernel],
+ proplists:get_value(lib_dep_1_active, TemplateData)),
+ ?assertEqual([],
+ proplists:get_value(lib_dep_1_library, TemplateData)),
+ ?assertEqual("false",
+ proplists:get_value(lib_dep_1_link, TemplateData)),
+ ?assertEqual("(2:debug)",
+ proplists:get_value(log, TemplateData)),
+ ?assertEqual(OutputDir,
+ proplists:get_value(output_dir, TemplateData)),
+ ?assertEqual(OutputDir,
+ proplists:get_value(target_dir, TemplateData)),
+ ?assertEqual([],
+ proplists:get_value(overridden, TemplateData)),
+ ?assertEqual([""],
+ proplists:get_value(goals, TemplateData)),
+ ?assert(proplists:is_defined(lib_dirs, TemplateData)),
+ ?assert(proplists:is_defined(config_file, TemplateData)),
+ ?assertEqual([""],
+ proplists:get_value(goals, TemplateData)),
+ ?assertEqual("undefined",
+ proplists:get_value(sys_config, TemplateData)),
+ ?assert(proplists:is_defined(root_dir, TemplateData)),
+ ?assertEqual(foo,
+ proplists:get_value(default_release_name, TemplateData)),
+ ?assertEqual("0.0.1",
+ proplists:get_value(default_release_version, TemplateData)),
+ ?assertEqual("foo-0.0.1",
+ proplists:get_value(default_release, TemplateData)),
+ ?assertEqual("yahoo",
+ proplists:get_value(yahoo, TemplateData)),
+ ?assertEqual("bar",
+ proplists:get_value(yahoo2_foo, TemplateData)),
+ ?assertEqual("foodir",
+ proplists:get_value(foo_dir, TemplateData)),
+ ?assertEqual("yahoo/bar",
+ proplists:get_value(yahoo3, TemplateData)).
+
+make_goalless_release(Config) ->
+ LibDir1 = proplists:get_value(lib1, Config),
+ [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn, [kernel, stdlib], [])
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+
+ create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []),
+ create_app(LibDir1, "lib_dep_1", "0.0.1", [], []),
+ create_app(LibDir1, "goal_app_2", "0.0.1", [stdlib,kernel,goal_app_1,non_goal_2], []),
+ create_app(LibDir1, "non_goal_1", "0.0.1", [stdlib,kernel], [lib_dep_1]),
+ create_app(LibDir1, "non_goal_2", "0.0.1", [stdlib,kernel], []),
+
+ ConfigFile = filename:join([LibDir1, "relcool.config"]),
+ write_config(ConfigFile,
+ [{release, {foo, "0.0.1"},
+ []}]),
+ OutputDir = filename:join([proplists:get_value(data_dir, Config),
+ create_random_name("relcool-output")]),
+ ?assertMatch({error,{rcl_prv_release,no_goals_specified}},
+ relcool:do(undefined, undefined, [], [LibDir1], 2,
+ OutputDir, ConfigFile)).
+
+make_depfree_release(Config) ->
+ LibDir1 = proplists:get_value(lib1, Config),
+ [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn, [kernel, stdlib], [])
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+
+ create_app(LibDir1, "goal_app_1", "0.0.1", [kernel,stdlib], []),
+ create_app(LibDir1, "lib_dep_1", "0.0.1", [kernel,stdlib], []),
+ create_app(LibDir1, "goal_app_2", "0.0.1", [kernel,stdlib], []),
+ create_app(LibDir1, "non_goal_1", "0.0.1", [kernel,stdlib], []),
+ create_app(LibDir1, "non_goal_2", "0.0.1", [kernel,stdlib], []),
+
+ ConfigFile = filename:join([LibDir1, "relcool.config"]),
+ write_config(ConfigFile,
+ [{release, {foo, "0.0.1"},
+ [goal_app_1]}]),
+ OutputDir = filename:join([proplists:get_value(data_dir, Config),
+ create_random_name("relcool-output")]),
+ {ok, State} = relcool:do(undefined, undefined, [], [LibDir1], 2,
+ OutputDir, ConfigFile),
+ [{{foo, "0.0.1"}, Release}] = ec_dictionary:to_list(rcl_state:releases(State)),
+ AppSpecs = rcl_release:applications(Release),
+ ?assert(lists:keymember(stdlib, 1, AppSpecs)),
+ ?assert(lists:keymember(kernel, 1, AppSpecs)).
+
+make_relup_release(Config) ->
+ LibDir1 = proplists:get_value(lib1, Config),
+ [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn, [kernel, stdlib], [])
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+
+ create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []),
+ create_app(LibDir1, "goal_app_1", "0.0.2", [stdlib,kernel,non_goal_1], []),
+ create_app(LibDir1, "goal_app_1", "0.0.3", [stdlib,kernel,non_goal_1], []),
+ create_app(LibDir1, "lib_dep_1", "0.0.1", [stdlib,kernel], []),
+ create_app(LibDir1, "goal_app_2", "0.0.1", [stdlib,kernel,goal_app_1,non_goal_2], []),
+ create_app(LibDir1, "goal_app_2", "0.0.2", [stdlib,kernel,goal_app_1,non_goal_2], []),
+ create_app(LibDir1, "goal_app_2", "0.0.3", [stdlib,kernel,goal_app_1,non_goal_2], []),
+ create_app(LibDir1, "non_goal_1", "0.0.1", [stdlib,kernel], [lib_dep_1]),
+ create_app(LibDir1, "non_goal_2", "0.0.1", [stdlib,kernel], []),
+
+ ConfigFile = filename:join([LibDir1, "relcool.config"]),
+ write_config(ConfigFile,
+ [{release, {foo, "0.0.1"},
+ [{goal_app_1, "0.0.1"},
+ {goal_app_2, "0.0.1"}]},
+ {release, {foo, "0.0.2"},
+ [{goal_app_1, "0.0.2"},
+ {goal_app_2, "0.0.2"}]},
+ {release, {foo, "0.0.3"},
+ [{goal_app_1, "0.0.3"},
+ {goal_app_2, "0.0.3"}]}]),
+ OutputDir = filename:join([proplists:get_value(data_dir, Config),
+ create_random_name("relcool-output")]),
+ {ok, _} = relcool:do(foo, "0.0.1", [], [LibDir1], 2,
+ OutputDir, ConfigFile),
+ {ok, _} = relcool:do(foo, "0.0.2", [], [LibDir1], 2,
+ OutputDir, ConfigFile),
+ {ok, State} = relcool:do(foo, "0.0.3", [], [LibDir1], 2,
+ OutputDir, ConfigFile),
+
+ %% we should have one 'resolved' release and three discovered releases.
+ ?assertMatch([{foo, "0.0.1"},
+ {foo, "0.0.2"},
+ {foo, "0.0.3"}],
+ lists:sort(ec_dictionary:keys(rcl_state:releases(State)))),
+ Release = ec_dictionary:get({foo, "0.0.3"}, rcl_state:releases(State)),
+ ?assert(rcl_release:realized(Release)),
+ ?assert(not rcl_release:realized(ec_dictionary:get({foo, "0.0.2"},
+ rcl_state:releases(State)))),
+ ?assert(not rcl_release:realized(ec_dictionary:get({foo, "0.0.1"},
+ rcl_state:releases(State)))),
+ ?assertMatch(foo, rcl_release:name(Release)),
+ ?assertMatch("0.0.3", rcl_release:vsn(Release)),
+ AppSpecs = rcl_release:applications(Release),
+ ?assert(lists:keymember(stdlib, 1, AppSpecs)),
+ ?assert(lists:keymember(kernel, 1, AppSpecs)),
+ ?assert(lists:member({non_goal_1, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({non_goal_2, "0.0.1"}, AppSpecs)),
+ ?assert(lists:member({goal_app_1, "0.0.3"}, AppSpecs)),
+ ?assert(lists:member({goal_app_2, "0.0.3"}, AppSpecs)),
+ ?assert(lists:member({lib_dep_1, "0.0.1", load}, AppSpecs)).
%%%===================================================================
%%% Helper Functions
%%%===================================================================
create_app(Dir, Name, Vsn, Deps, LibDeps) ->
- AppDir = filename:join([Dir, Name]),
+ AppDir = filename:join([Dir, Name ++ "-" ++ Vsn]),
write_app_file(AppDir, Name, Vsn, Deps, LibDeps),
write_beam_file(AppDir, Name),
rcl_app_info:new(erlang:list_to_atom(Name), Vsn, AppDir,
@@ -180,3 +606,39 @@ create_random_vsn() ->
write_config(Filename, Values) ->
ok = ec_file:write(Filename,
[io_lib:format("~p.\n", [Val]) || Val <- Values]).
+
+test_template_contents() ->
+ "{erts_vsn, \"{{erts_vsn}}\"}.\n"
+ "{release_erts_version, \"{{release_erts_version}}\"}.\n"
+ "{release_name, {{release_name}}}.\n"
+ "{rel_vsn, \"{{release_version}}\"}.\n"
+ "{release_version, \"{{release_version}}\"}.\n"
+ "{release_applications, [{{ release_applications|join:\", \" }}]}.\n"
+ "{std_version, \"{{release.stdlib.version}}\"}.\n"
+ "{kernel_version, \"{{release.kernel.version}}\"}.\n"
+ "{non_goal_1_version, \"{{release.non_goal_1.version}}\"}.\n"
+ "{non_goal_2_version, \"{{release.non_goal_2.version}}\"}.\n"
+ "{goal_app_1_version, \"{{release.goal_app_1.version}}\"}.\n"
+ "{goal_app_2_version, \"{{release.goal_app_2.version}}\"}.\n"
+ "{lib_dep_1, \"{{release.lib_dep_1.version}}\"}.\n"
+ "{lib_dep_1_dir, \"{{release.lib_dep_1.dir}}\"}.\n"
+ "{lib_dep_1_active, [{{ release.lib_dep_1.active_dependencies|join:\", \" }}]}.\n"
+ "{lib_dep_1_library, [{{ release.lib_dep_1.library_dependencies|join:\", \" }}]}.\n"
+ "{lib_dep_1_link, \"{{release.lib_dep_1.link}}\"}.\n"
+ "{log, \"{{log}}\"}.\n"
+ "{output_dir, \"{{output_dir}}\"}.\n"
+ "{target_dir, \"{{target_dir}}\"}.\n"
+ "{overridden, [{{ overridden|join:\", \" }}]}.\n"
+ "{goals, [\"{{ goals|join:\", \" }}\"]}.\n"
+ "{lib_dirs, [\"{{ lib_dirs|join:\", \" }}\"]}.\n"
+ "{config_file, \"{{ config_file }}\"}.\n"
+ "{providers, [{{ providers|join:\", \" }}]}.\n"
+ "{sys_config, \"{{sys_config}}\"}.\n"
+ "{root_dir, \"{{root_dir}}\"}.\n"
+ "{default_release_name, {{default_release_name}}}.\n"
+ "{default_release_version, \"{{default_release_version}}\"}.\n"
+ "{default_release, \"{{default_release}}\"}.\n"
+ "{yahoo, \"{{yahoo}}\"}.\n"
+ "{yahoo2_foo, \"{{yahoo2.foo}}\"}.\n"
+ "{foo_dir, \"{{foo_dir}}\"}.\n"
+ "{yahoo3, \"{{yahoo3.bar}}\"}.\n".