1%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
2%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
3%%%
4%%% This file is provided to you under the Apache License,
5%%% Version 2.0 (the "License"); you may not use this file
6%%% except in compliance with the License.  You may obtain
7%%% a copy of the License at
8%%%
9%%%   http://www.apache.org/licenses/LICENSE-2.0
10%%%
11%%% Unless required by applicable law or agreed to in writing,
12%%% software distributed under the License is distributed on an
13%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14%%% KIND, either express or implied.  See the License for the
15%%% specific language governing permissions and limitations
16%%% under the License.
17%%%---------------------------------------------------------------------------
18%%% @author Eric Merritt <ericbmerritt@gmail.com>
19%%% @copyright (C) 2012 Erlware, LLC.
20%%%
21%%% @doc Trivial utility file to help handle common tasks
22-module(rlx_util).
23
24-export([is_sasl_gte/0,
25         parse_vsn/1,
26         parsed_vsn_lte/2,
27         get_code_paths/2,
28         release_output_dir/2,
29         list_search/2,
30         to_binary/1,
31         to_string/1,
32         is_error/1,
33         error_reason/1,
34         indent/1,
35         render/2,
36         load_file/3,
37         template_files/0,
38         sh/1]).
39
40-export([os_type/1]).
41
42-define(ONE_LEVEL_INDENT, "     ").
43
44-include("rlx_log.hrl").
45
46is_sasl_gte() ->
47    %% default check is for sasl 3.5 and above
48    %% version 3.5 of sasl has systools with changes for relx
49    %% related to `make_script', `make_tar' and the extended start script
50    is_sasl_gte({{3, 5, 0}, {[], []}}).
51
52is_sasl_gte(Version) ->
53    application:load(sasl),
54    case application:get_key(sasl, vsn) of
55        {ok, SaslVsn} ->
56            not(parsed_vsn_lt(parse_vsn(SaslVsn), Version));
57        _ ->
58            false
59    end.
60
61%% parses a semver into a tuple {{Major, Minor, Patch}, {PreRelease, Build}}
62-spec parse_vsn(string()) -> {{integer(), integer(), integer()}, {string(), string()}}.
63parse_vsn(Vsn) ->
64    case re:run(Vsn, "^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?"
65                "(-(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-]*))*)?"
66                "(\\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$", [{capture, [1,2,4,6,8], list}]) of
67        %% OTP application's leave out patch version when it is .0
68        %% this regex currently drops prerelease and build if the patch version is left out
69        %% so 3.11-0+meta would reutrn {{3,11,0},{[], []}} intsead of {{3,1,0},{"0","meta"}}
70        {match, [Major, Minor, [], PreRelease, Build]} ->
71            {{list_to_integer(Major), list_to_integer(Minor), 0}, {PreRelease, Build}};
72        {match, [Major, Minor, Patch, PreRelease, Build]} ->
73            {{list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)}, {PreRelease, Build}};
74        _ ->
75            0
76    end.
77
78%% less than or equal to comparison for versions parsed with `parse_vsn'
79parsed_vsn_lte(VsnA, VsnB) ->
80    parsed_vsn_lt(VsnA, VsnB) orelse VsnA =:= VsnB.
81
82parsed_vsn_lt({MMPA, {AlphaA, PatchA}}, {MMPB, {AlphaB, PatchB}}) ->
83    ((MMPA < MMPB)
84     orelse
85       ((MMPA =:= MMPB)
86        andalso
87          ((AlphaB =:= [] andalso AlphaA =/= [])
88           orelse
89             ((not (AlphaA =:= [] andalso AlphaB =/= []))
90              andalso
91                (AlphaA < AlphaB))))
92     orelse
93       ((MMPA =:= MMPB)
94        andalso
95          (AlphaA =:= AlphaB)
96        andalso
97          ((PatchA =:= [] andalso PatchB =/= [])
98           orelse
99           PatchA < PatchB))).
100
101%% @doc Generates the correct set of code paths for the system.
102-spec get_code_paths(rlx_release:t(), file:name()) -> [filename:filename_all()].
103get_code_paths(Release, OutDir) ->
104    LibDir = filename:join(OutDir, "lib"),
105    [filename:join([LibDir, [rlx_app_info:name(App), "-", rlx_app_info:vsn(App)], "ebin"]) ||
106        App <- rlx_release:applications(Release)].
107
108-spec release_output_dir(rlx_state:t(), rlx_release:t()) -> string().
109release_output_dir(State, Release) ->
110    OutputDir = rlx_state:base_output_dir(State),
111    filename:join([OutputDir,
112                   rlx_release:name(Release),
113                   "releases",
114                   rlx_release:vsn(Release)]).
115
116%% @doc ident to the level specified
117-spec indent(non_neg_integer()) -> iolist().
118indent(Amount) when erlang:is_integer(Amount) ->
119    [?ONE_LEVEL_INDENT || _ <- lists:seq(1, Amount)].
120
121list_search(Pred, [Hd|Tail]) ->
122    case Pred(Hd) of
123        {true, Value} -> {value, Value};
124        true -> {value, Hd};
125        false -> list_search(Pred, Tail)
126    end;
127list_search(Pred, []) when is_function(Pred, 1) ->
128    false.
129
130-spec to_binary(iolist() | binary()) -> binary().
131to_binary(String) when erlang:is_list(String) ->
132    erlang:iolist_to_binary(String);
133to_binary(Atom) when erlang:is_atom(Atom) ->
134    erlang:atom_to_binary(Atom, utf8);
135to_binary(Bin) when erlang:is_binary(Bin) ->
136    Bin.
137
138-spec to_string(binary() | string() | atom()) -> string().
139to_string(Binary) when erlang:is_binary(Binary) ->
140    erlang:binary_to_list(Binary);
141to_string(Atom) when erlang:is_atom(Atom) ->
142    erlang:atom_to_list(Atom);
143to_string(Else) when erlang:is_list(Else) ->
144    Else.
145
146%% @doc get the reason for a particular relx error
147-spec error_reason(relx:error()) -> any().
148error_reason({error, {_, Reason}}) ->
149    Reason.
150%% @doc check to see if the value is a relx error
151-spec is_error(relx:error() | any()) -> boolean().
152is_error({error, _}) ->
153    true;
154is_error(_) ->
155    false.
156
157-spec render(binary() | iolist(), proplists:proplist()) ->
158                {ok, binary()} | {error, render_failed}.
159render(Template, Data) when is_list(Template) ->
160    render(rlx_util:to_binary(Template), Data);
161render(Template, Data) when is_binary(Template) ->
162    case catch bbmustache:render(Template, Data,
163                                 [{key_type, atom},
164                                  {escape_fun, fun(X) -> X end}]) of
165        Bin when is_binary(Bin) -> {ok, Bin};
166        _ -> {error, render_failed}
167    end.
168
169load_file(Files, escript, Name) ->
170    {Name, Bin} = lists:keyfind(Name, 1, Files),
171    Bin;
172load_file(_Files, file, Name) ->
173    {ok, Bin} = file:read_file(Name),
174    Bin.
175
176template_files() ->
177    find_priv_templates() ++ escript_files().
178
179find_priv_templates() ->
180    Files = filelib:wildcard(filename:join([code:priv_dir(relx), "templates", "*"])),
181    lists:map(fun(File) ->
182                      {ok, Bin} = file:read_file(File),
183                      {filename:basename(File), Bin}
184              end, Files).
185
186%% Scan the current escript for available files
187escript_files() ->
188    try
189        {ok, Files} = escript_foldl(
190                        fun(Name, _, GetBin, Acc) ->
191                                [{filename:basename(Name), GetBin()} | Acc]
192                        end, [], filename:absname(escript:script_name())),
193        Files
194    catch
195        _:_ ->
196            []
197    end.
198
199escript_foldl(Fun, Acc, File) ->
200    case escript:extract(File, []) of
201        {ok, [_Shebang, _Comment, _EmuArgs, Body]} ->
202            case Body of
203                {source, BeamCode} ->
204                    GetInfo = fun() -> file:read_file_info(File) end,
205                    GetBin = fun() -> BeamCode end,
206                    {ok, Fun(".", GetInfo, GetBin, Acc)};
207                {beam, BeamCode} ->
208                    GetInfo = fun() -> file:read_file_info(File) end,
209                    GetBin = fun() -> BeamCode end,
210                    {ok, Fun(".", GetInfo, GetBin, Acc)};
211                {archive, ArchiveBin} ->
212                    zip:foldl(Fun, Acc, {File, ArchiveBin})
213            end;
214        {error, _} = Error ->
215            Error
216    end.
217
218os_type(State) ->
219  case include_erts_is_win32(State) of
220    true -> {win32,nt};
221    false -> os:type()
222  end.
223
224include_erts_is_win32(State) ->
225  case rlx_state:include_erts(State) of
226    true -> false;
227    false -> false;
228    Path -> is_win32_erts(Path)
229  end.
230
231is_win32_erts(Path) ->
232  case filelib:wildcard(filename:join([Path,"bin","erl.exe"])) of
233    [] ->
234          false;
235    _ ->
236      ?log_info("Including Erts is win32"),
237      true
238  end.
239
240sh(Command0) ->
241    Command = lists:flatten(patch_on_windows(Command0)),
242    PortSettings = [exit_status, {line, 16384}, use_stdio, stderr_to_stdout, hide, eof, binary],
243
244    Port = open_port({spawn, Command}, PortSettings),
245    try
246        case sh_loop(Port, []) of
247            {ok, Output} ->
248                Output;
249            {error, {_Rc, _Output}=Err} ->
250                error(Err)
251        end
252    after
253        port_close(Port)
254    end.
255
256sh_loop(Port, Acc) ->
257    receive
258        {Port, {data, {eol, Line}}} ->
259            sh_loop(Port, [unicode:characters_to_list(Line) ++ "\n" | Acc]);
260        {Port, {data, {noeol, Line}}} ->
261            sh_loop(Port, [unicode:characters_to_list(Line) | Acc]);
262        {Port, eof} ->
263            Data = lists:flatten(lists:reverse(Acc)),
264            receive
265                {Port, {exit_status, 0}} ->
266                    {ok, Data};
267                {Port, {exit_status, Rc}} ->
268                    {error, {Rc, Data}}
269            end
270    end.
271
272%% We do the shell variable substitution ourselves on Windows and hope that the
273%% command doesn't use any other shell magic.
274patch_on_windows(Cmd) ->
275    case os:type() of
276        {win32,nt} ->
277            Cmd1 = "cmd /q /c " ++ Cmd,
278            %% Remove left-over vars
279            re:replace(Cmd1, "\\\$\\w+|\\\${\\w+}", "",
280                       [global, {return, list}, unicode]);
281        _ ->
282            Cmd
283    end.
284
285