%% -*- 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 %%% @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.