285 lines
9.4 KiB
Erlang
285 lines
9.4 KiB
Erlang
%% -*- 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 <ericbmerritt@gmail.com>
|
|
%%% @copyright (C) 2012 Erlware, LLC.
|
|
%%%
|
|
%%% @doc Trivial utility file to help handle common tasks
|
|
-module(rlx_util).
|
|
|
|
-export([is_sasl_gte/0,
|
|
parse_vsn/1,
|
|
parsed_vsn_lte/2,
|
|
get_code_paths/2,
|
|
release_output_dir/2,
|
|
list_search/2,
|
|
to_binary/1,
|
|
to_string/1,
|
|
is_error/1,
|
|
error_reason/1,
|
|
indent/1,
|
|
render/2,
|
|
load_file/3,
|
|
template_files/0,
|
|
sh/1]).
|
|
|
|
-export([os_type/1]).
|
|
|
|
-define(ONE_LEVEL_INDENT, " ").
|
|
|
|
-include("rlx_log.hrl").
|
|
|
|
is_sasl_gte() ->
|
|
%% default check is for sasl 3.5 and above
|
|
%% version 3.5 of sasl has systools with changes for relx
|
|
%% related to `make_script', `make_tar' and the extended start script
|
|
is_sasl_gte({{3, 5, 0}, {[], []}}).
|
|
|
|
is_sasl_gte(Version) ->
|
|
application:load(sasl),
|
|
case application:get_key(sasl, vsn) of
|
|
{ok, SaslVsn} ->
|
|
not(parsed_vsn_lt(parse_vsn(SaslVsn), Version));
|
|
_ ->
|
|
false
|
|
end.
|
|
|
|
%% parses a semver into a tuple {{Major, Minor, Patch}, {PreRelease, Build}}
|
|
-spec parse_vsn(string()) -> {{integer(), integer(), integer()}, {string(), string()}}.
|
|
parse_vsn(Vsn) ->
|
|
case re:run(Vsn, "^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?"
|
|
"(-(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*)?"
|
|
"(\\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$", [{capture, [1,2,4,6,8], list}]) of
|
|
%% OTP application's leave out patch version when it is .0
|
|
%% this regex currently drops prerelease and build if the patch version is left out
|
|
%% so 3.11-0+meta would return {{3,11,0},{[], []}} intsead of {{3,1,0},{"0","meta"}}
|
|
{match, [Major, Minor, [], PreRelease, Build]} ->
|
|
{{list_to_integer(Major), list_to_integer(Minor), 0}, {PreRelease, Build}};
|
|
{match, [Major, Minor, Patch, PreRelease, Build]} ->
|
|
{{list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)}, {PreRelease, Build}};
|
|
_ ->
|
|
0
|
|
end.
|
|
|
|
%% less than or equal to comparison for versions parsed with `parse_vsn'
|
|
parsed_vsn_lte(VsnA, VsnB) ->
|
|
parsed_vsn_lt(VsnA, VsnB) orelse VsnA =:= VsnB.
|
|
|
|
parsed_vsn_lt({MMPA, {AlphaA, PatchA}}, {MMPB, {AlphaB, PatchB}}) ->
|
|
((MMPA < MMPB)
|
|
orelse
|
|
((MMPA =:= MMPB)
|
|
andalso
|
|
((AlphaB =:= [] andalso AlphaA =/= [])
|
|
orelse
|
|
((not (AlphaA =:= [] andalso AlphaB =/= []))
|
|
andalso
|
|
(AlphaA < AlphaB))))
|
|
orelse
|
|
((MMPA =:= MMPB)
|
|
andalso
|
|
(AlphaA =:= AlphaB)
|
|
andalso
|
|
((PatchA =:= [] andalso PatchB =/= [])
|
|
orelse
|
|
PatchA < PatchB))).
|
|
|
|
%% @doc Generates the correct set of code paths for the system.
|
|
-spec get_code_paths(rlx_release:t(), file:name()) -> [filename:filename_all()].
|
|
get_code_paths(Release, OutDir) ->
|
|
LibDir = filename:join(OutDir, "lib"),
|
|
[filename:join([LibDir, [rlx_app_info:name(App), "-", rlx_app_info:vsn(App)], "ebin"]) ||
|
|
App <- rlx_release:applications(Release)].
|
|
|
|
-spec release_output_dir(rlx_state:t(), rlx_release:t()) -> string().
|
|
release_output_dir(State, Release) ->
|
|
OutputDir = rlx_state:base_output_dir(State),
|
|
filename:join([OutputDir,
|
|
rlx_release:name(Release),
|
|
"releases",
|
|
rlx_release:vsn(Release)]).
|
|
|
|
%% @doc ident to the level specified
|
|
-spec indent(non_neg_integer()) -> iolist().
|
|
indent(Amount) when erlang:is_integer(Amount) ->
|
|
[?ONE_LEVEL_INDENT || _ <- lists:seq(1, Amount)].
|
|
|
|
list_search(Pred, [Hd|Tail]) ->
|
|
case Pred(Hd) of
|
|
{true, Value} -> {value, Value};
|
|
true -> {value, Hd};
|
|
false -> list_search(Pred, Tail)
|
|
end;
|
|
list_search(Pred, []) when is_function(Pred, 1) ->
|
|
false.
|
|
|
|
-spec to_binary(iolist() | binary()) -> binary().
|
|
to_binary(String) when erlang:is_list(String) ->
|
|
erlang:iolist_to_binary(String);
|
|
to_binary(Atom) when erlang:is_atom(Atom) ->
|
|
erlang:atom_to_binary(Atom, utf8);
|
|
to_binary(Bin) when erlang:is_binary(Bin) ->
|
|
Bin.
|
|
|
|
-spec to_string(binary() | string() | atom()) -> string().
|
|
to_string(Binary) when erlang:is_binary(Binary) ->
|
|
erlang:binary_to_list(Binary);
|
|
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 relx error
|
|
-spec error_reason(relx:error()) -> any().
|
|
error_reason({error, {_, Reason}}) ->
|
|
Reason.
|
|
%% @doc check to see if the value is a relx error
|
|
-spec is_error(relx:error() | any()) -> boolean().
|
|
is_error({error, _}) ->
|
|
true;
|
|
is_error(_) ->
|
|
false.
|
|
|
|
-spec render(binary() | iolist(), proplists:proplist()) ->
|
|
{ok, binary()} | {error, render_failed}.
|
|
render(Template, Data) when is_list(Template) ->
|
|
render(rlx_util:to_binary(Template), Data);
|
|
render(Template, Data) when is_binary(Template) ->
|
|
case catch bbmustache:render(Template, Data,
|
|
[{key_type, atom},
|
|
{escape_fun, fun(X) -> X end}]) of
|
|
Bin when is_binary(Bin) -> {ok, Bin};
|
|
_ -> {error, render_failed}
|
|
end.
|
|
|
|
load_file(Files, escript, Name) ->
|
|
{Name, Bin} = lists:keyfind(Name, 1, Files),
|
|
Bin;
|
|
load_file(_Files, file, Name) ->
|
|
{ok, Bin} = file:read_file(Name),
|
|
Bin.
|
|
|
|
template_files() ->
|
|
find_priv_templates() ++ escript_files().
|
|
|
|
find_priv_templates() ->
|
|
Files = filelib:wildcard(filename:join([code:priv_dir(relx), "templates", "*"])),
|
|
lists:map(fun(File) ->
|
|
{ok, Bin} = file:read_file(File),
|
|
{filename:basename(File), Bin}
|
|
end, Files).
|
|
|
|
%% Scan the current escript for available files
|
|
escript_files() ->
|
|
try
|
|
{ok, Files} = escript_foldl(
|
|
fun(Name, _, GetBin, Acc) ->
|
|
[{filename:basename(Name), GetBin()} | Acc]
|
|
end, [], filename:absname(escript:script_name())),
|
|
Files
|
|
catch
|
|
_:_ ->
|
|
[]
|
|
end.
|
|
|
|
escript_foldl(Fun, Acc, File) ->
|
|
case escript:extract(File, []) of
|
|
{ok, [_Shebang, _Comment, _EmuArgs, Body]} ->
|
|
case Body of
|
|
{source, BeamCode} ->
|
|
GetInfo = fun() -> file:read_file_info(File) end,
|
|
GetBin = fun() -> BeamCode end,
|
|
{ok, Fun(".", GetInfo, GetBin, Acc)};
|
|
{beam, BeamCode} ->
|
|
GetInfo = fun() -> file:read_file_info(File) end,
|
|
GetBin = fun() -> BeamCode end,
|
|
{ok, Fun(".", GetInfo, GetBin, Acc)};
|
|
{archive, ArchiveBin} ->
|
|
zip:foldl(Fun, Acc, {File, ArchiveBin})
|
|
end;
|
|
{error, _} = Error ->
|
|
Error
|
|
end.
|
|
|
|
os_type(State) ->
|
|
case include_erts_is_win32(State) of
|
|
true -> {win32,nt};
|
|
false -> os:type()
|
|
end.
|
|
|
|
include_erts_is_win32(State) ->
|
|
case rlx_state:include_erts(State) of
|
|
true -> false;
|
|
false -> false;
|
|
Path -> is_win32_erts(Path)
|
|
end.
|
|
|
|
is_win32_erts(Path) ->
|
|
case filelib:wildcard(filename:join([Path,"bin","erl.exe"])) of
|
|
[] ->
|
|
false;
|
|
_ ->
|
|
?log_info("Including Erts is win32"),
|
|
true
|
|
end.
|
|
|
|
sh(Command0) ->
|
|
Command = lists:flatten(patch_on_windows(Command0)),
|
|
PortSettings = [exit_status, {line, 16384}, use_stdio, stderr_to_stdout, hide, eof, binary],
|
|
|
|
Port = open_port({spawn, Command}, PortSettings),
|
|
try
|
|
case sh_loop(Port, []) of
|
|
{ok, Output} ->
|
|
Output;
|
|
{error, {_Rc, _Output}=Err} ->
|
|
error(Err)
|
|
end
|
|
after
|
|
port_close(Port)
|
|
end.
|
|
|
|
sh_loop(Port, Acc) ->
|
|
receive
|
|
{Port, {data, {eol, Line}}} ->
|
|
sh_loop(Port, [unicode:characters_to_list(Line) ++ "\n" | Acc]);
|
|
{Port, {data, {noeol, Line}}} ->
|
|
sh_loop(Port, [unicode:characters_to_list(Line) | Acc]);
|
|
{Port, eof} ->
|
|
Data = lists:flatten(lists:reverse(Acc)),
|
|
receive
|
|
{Port, {exit_status, 0}} ->
|
|
{ok, Data};
|
|
{Port, {exit_status, Rc}} ->
|
|
{error, {Rc, Data}}
|
|
end
|
|
end.
|
|
|
|
%% We do the shell variable substitution ourselves on Windows and hope that the
|
|
%% command doesn't use any other shell magic.
|
|
patch_on_windows(Cmd) ->
|
|
case os:type() of
|
|
{win32,nt} ->
|
|
Cmd1 = "cmd /q /c " ++ Cmd,
|
|
%% Remove left-over vars
|
|
re:replace(Cmd1, "\\\$\\w+|\\\${\\w+}", "",
|
|
[global, {return, list}, unicode]);
|
|
_ ->
|
|
Cmd
|
|
end.
|
|
|