diff options
Diffstat (limited to 'src/relx.erl')
-rw-r--r-- | src/relx.erl | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/src/relx.erl b/src/relx.erl new file mode 100644 index 0000000..30ee77f --- /dev/null +++ b/src/relx.erl @@ -0,0 +1,278 @@ +%% -*- 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 +-module(relx). + +-export([main/1, + do/2, + do/7, + do/8, + do/9, + format_error/1, + opt_spec_list/0]). + +-export_type([error/0]). + +-include_lib("relx/include/relx.hrl"). + +%%============================================================================ +%% types +%%============================================================================ + +-type error() :: {error, {Module::module(), Reason::term()}}. +-type goal() :: string() | binary() | rlx_depsolver:constraint(). + +%%============================================================================ +%% API +%%============================================================================ +-spec main([string()]) -> ok | error() | {ok, rlx_state:t()}. +main(Args) -> + OptSpecList = opt_spec_list(), + Result = case getopt:parse(OptSpecList, Args) of + {ok, {Options, NonOptions}} -> + do([{caller, command_line} | Options], NonOptions); + {error, Detail} -> + ?RLX_ERROR({opt_parse, Detail}) + end, + case Result of + {error, _} -> + report_error(rlx_state:caller(rlx_state:new([], undefined), + command_line), + Result); + _ -> + Result + end. + +%% @doc provides an API to run the Relx process from erlang applications +%% +%% @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 Relx 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(atom(), string(), [goal()], [file:name()], rlx_log:log_level(), + [file:name()], file:name() | undefined) -> + ok | error() | {ok, rlx_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 Relx 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 Relx 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()], + rlx_log:log_level(), [file:name()], file:name() | undefined) -> + ok | error() | {ok, rlx_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 Relx 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 Relx 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 Overrides - A list of overrides for the system +%% @param Configs - The list of config files for the system +-spec do(file:name(), atom(), string(), [goal()], [file:name()], + rlx_log:log_level(), [file:name()], [{atom(), file:name()}], file:name() | undefined) -> + ok | error() | {ok, rlx_state:t()}. +do(RootDir, RelName, RelVsn, Goals, LibDirs, LogLevel, OutputDir, Overrides, Config) -> + do([{relname, RelName}, + {relvsn, RelVsn}, + {goals, Goals}, + {overrides, Overrides}, + {output_dir, OutputDir}, + {lib_dirs, LibDirs}, + {root_dir, RootDir}, + {log_level, LogLevel}, + {config, Config}], + ["release"]). + +%% @doc provides an API to run the Relx process from erlang applications +%% +%% @param Opts - A proplist of options. There are good defaults for each of +%% these entries, so any or all may be omitted. Individual options may be: +%% +%% <dl> +%% <dt><pre>{relname, RelName}</pre></dt> +%% <dd>The release name to build </dd> +%% <dt><pre>{relvsn, RelVsn}</pre></dt> +%% <dd>The release version to build</dd> +%% <dt><pre>{goals, Goals}</pre></dt> +%% <dd>The release goals for the system in depsolver or Relx goal +%% format (@see goals())</dd> +%% <dt><pre>{lib_dirs, LibDirs}</pre></dt> +%% <dd>A list of library dirs that should be used for the system</dd> +%% <dt><pre>{lib_dir, LibDir}</pre></dt> +%% <dd>A single lib dir that should be used for the system, may appear any +%% number of times and may be used in conjunction with lib_dirs</dd> +%% <dt><pre>{output_dir, OutputDir}</pre></dt> +%% <dd>The directory where the release should be built to</dd> +%% <dt><pre>{root_dir, RootDir}</pre></dt> +%% <dd>The base directory for this run of relx. </dd> +%% <dt><pre>{config, Config}</pre></dt> +%% <dd>The path to a relx config file</dd> +%% <dt><pre>{log_level, LogLevel}</pre></dt> +%% <dd>Defines the verbosity of output. Maybe a number between 0 and 2, with +%% with higher values being more verbose</dd> +%% <dt><pre>{overrides, Overrides}</pre></dt> +%% <dd>A list of app overrides for the system in the form of [{AppName:atom(), +%% Dir:string() | binary()} | string() | binary()] where the string or binary +%% is in the form "AppName:AppDir"</dd> +%% <dt><pre>{override, Override}</pre></dt> +%% <dd>A single of app override for the system in the same form as entries for +%% Overrides</dd> +%% </dl> +-spec do(proplists:proplist(), [string()]) -> + ok | error() | {ok, rlx_state:t()}. +do(Opts, NonOpts) -> + case rlx_cmd_args:args2state(Opts, NonOpts) of + {ok, State} -> + run_relx_process(State); + Error={error, _} -> + Error + end. + +-spec opt_spec_list() -> [getopt:option_spec()]. +opt_spec_list() -> + [{relname, $n, "relname", string, + "Specify the name for the release that will be generated"}, + {relvsn, $v, "relvsn", string, "Specify the version for the release"}, + {goal, $g, "goal", string, + "Specify a target constraint on the system. These are usually the OTP"}, + {upfrom, $u, "upfrom", string, + "Only valid with relup target, specify the release to upgrade from"}, + {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"}, + {override_app, $a, "override_app", string, + "Provide an app name and a directory to override in the form <appname>:<app directory>"}, + {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}) -> + [rlx_provider:format(Provider), " returned an invalid value ", + io_lib:format("~p", [Value])]; +format_error({opt_parse, {invalid_option, Opt}}) -> + io_lib:format("invalid option ~s~n", [Opt]); +format_error({opt_parse, Arg}) -> + io_lib:format("~p~n", [Arg]); +format_error({error, {Module, Reason}}) -> + io_lib:format("~s~n", [Module:format_error(Reason)]). + +%%============================================================================ +%% internal api +%%============================================================================ +run_relx_process(State) -> + rlx_log:info(rlx_state:log(State), "Starting relx build process ..."), + rlx_log:debug(rlx_state:log(State), + fun() -> + rlx_state:format(State) + end), + run_providers(State). + +%% @doc for now the 'config' provider is special in that it generates the +%% providers used by the rest of the system. We expect the config provider to be +%% the first provider in the system. Once the config provider is run, we get the +%% providers again and run the rest of them (because they could have been +%% updated by the config process). +run_providers(State0) -> + [ConfigProvider | _] = rlx_state:providers(State0), + case run_provider(ConfigProvider, {ok, State0}) of + Err = {error, _} -> + Err; + {ok, State1} -> + RootDir = rlx_state:root_dir(State1), + ok = file:set_cwd(RootDir), + Providers = rlx_state:providers(State1), + Result = run_providers(ConfigProvider, Providers, State1), + handle_output(State1, rlx_state:caller(State1), Result) + end. + +handle_output(State, command_line, E={error, _}) -> + report_error(State, E), + init:stop(127); +handle_output(_State, command_line, _) -> + init:stop(0); +handle_output(_State, api, Result) -> + Result. + +run_providers(ConfigProvider, Providers, State0) -> + case Providers of + [ConfigProvider | Rest] -> + %% IF the config provider is still the first provider do not run it + %% again just run the rest. + lists:foldl(fun run_provider/2, {ok, State0}, Rest); + _ -> + lists:foldl(fun run_provider/2, {ok, State0}, Providers) + end. + +-spec run_provider(rlx_provider:t(), {ok, rlx_state:t()} | error()) -> + {ok, rlx_state:t()} | error(). +run_provider(_Provider, Error = {error, _}) -> + Error; +run_provider(Provider, {ok, State0}) -> + rlx_log:debug(rlx_state:log(State0), "Running provider ~p~n", + [rlx_provider:impl(Provider)]), + case rlx_provider:do(Provider, State0) of + {ok, State1} -> + rlx_log:debug(rlx_state:log(State0), "Provider successfully run: ~p~n", + [rlx_provider:impl(Provider)]), + {ok, State1}; + E={error, _} -> + rlx_log:debug(rlx_state:log(State0), "Provider (~p) failed with: ~p~n", + [rlx_provider:impl(Provider), E]), + E + end. + +-spec usage() -> ok. +usage() -> + getopt:usage(opt_spec_list(), "relx", "[*release-specification-file*]"). + +-spec report_error(rlx_state:t(), error()) -> none() | error(). +report_error(State, Error) -> + io:format(format_error(Error)), + usage(), + case rlx_state:caller(State) of + command_line -> + erlang:halt(127); + api -> + Error + end. |