diff options
author | Tristan Sloughter <[email protected]> | 2013-02-07 12:19:10 -0800 |
---|---|---|
committer | Tristan Sloughter <[email protected]> | 2013-02-07 12:19:10 -0800 |
commit | 6d27a1af18f8337bbcfd3994787bf8725b94868a (patch) | |
tree | 93feb4e317d35d9d69443551b83175d21a0a0808 /src | |
parent | aac84f08f2b557ae2a7bf45001c47aec503203ab (diff) | |
parent | 6734e9ec4e0dd4c0678d10e8b8e48552ffb4e995 (diff) | |
download | relx-6d27a1af18f8337bbcfd3994787bf8725b94868a.tar.gz relx-6d27a1af18f8337bbcfd3994787bf8725b94868a.tar.bz2 relx-6d27a1af18f8337bbcfd3994787bf8725b94868a.zip |
Merge pull request #28 from erlware/next
Next
Diffstat (limited to 'src')
-rw-r--r-- | src/rcl_app_discovery.erl | 223 | ||||
-rw-r--r-- | src/rcl_app_info.erl | 6 | ||||
-rw-r--r-- | src/rcl_cmd_args.erl | 124 | ||||
-rw-r--r-- | src/rcl_depsolver.erl | 7 | ||||
-rw-r--r-- | src/rcl_goal_utils.erl | 2 | ||||
-rw-r--r-- | src/rcl_log.erl | 13 | ||||
-rw-r--r-- | src/rcl_provider.erl | 16 | ||||
-rw-r--r-- | src/rcl_prv_assembler.erl | 130 | ||||
-rw-r--r-- | src/rcl_prv_config.erl | 72 | ||||
-rw-r--r-- | src/rcl_prv_discover.erl | 242 | ||||
-rw-r--r-- | src/rcl_prv_overlay.erl | 418 | ||||
-rw-r--r-- | src/rcl_prv_release.erl | 45 | ||||
-rw-r--r-- | src/rcl_rel_discovery.erl | 166 | ||||
-rw-r--r-- | src/rcl_release.erl | 76 | ||||
-rw-r--r-- | src/rcl_state.erl | 83 | ||||
-rw-r--r-- | src/rcl_topo.erl | 17 | ||||
-rw-r--r-- | src/rcl_util.erl | 28 | ||||
-rw-r--r-- | src/relcool.erl | 92 |
18 files changed, 1350 insertions, 410 deletions
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)), |