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 This module represents a release and its metadata and is used to
22%%% manipulate the release metadata.
23-module(rlx_release).
24
25-export([new/2,
26         relfile/2,
27         erts/2,
28         erts/1,
29         parse_goals/1,
30         goals/2,
31         goals/1,
32         parsed_goals/2,
33         name/1,
34         vsn/1,
35         realize/2,
36         applications/1,
37         app_specs/1,
38         metadata/1,
39         start_clean_metadata/1,
40         no_dot_erlang_metadata/1,
41         canonical_name/1,
42         config/1,
43         config/2,
44         format/1,
45         format_error/1]).
46
47-export_type([t/0,
48              name/0,
49              vsn/0,
50              type/0,
51              incl_apps/0,
52              application_spec/0,
53              release_spec/0,
54              parsed_goal/0]).
55
56-include("relx.hrl").
57
58-record(release_t, {name :: atom(),
59                    vsn :: string(),
60                    erts :: undefined | string(),
61                    goals = undefined :: parsed_goals() | undefined,
62
63                    %% when `realized' is `true' `applications' must be a list of all
64                    %% `app_info's needed to fulfill the `goals' and `app_specs'
65                    %% must be the full list that goes in the `.rel' file.
66                    realized = false :: boolean(),
67
68                    app_specs = [] ::  [application_spec()],
69                    applications = [] :: [rlx_app_info:t()],
70
71                    relfile :: undefined | string(),
72                    config = []}).
73
74%%============================================================================
75%% types
76%%============================================================================
77-type name() :: atom().
78-type vsn() :: string().
79-type type() :: permanent | transient | temporary | load | none.
80-type incl_apps() :: [name()].
81
82-type parsed_goal() :: #{name := name(),
83                         vsn => vsn() | undefined,
84                         type => type(),
85                         included_applications => incl_apps()}.
86
87-type parsed_goals() :: [{name(), parsed_goal()}].
88
89-type application_spec() :: {name(), vsn()} |
90                            {name(), vsn(), type() | incl_apps()} |
91                            {name(), vsn(), type(), incl_apps()}.
92
93-type release_spec() :: {release, {string(), vsn()}, {erts, vsn()},
94                         [application_spec()]}.
95
96-type t() :: #release_t{}.
97
98-spec new(atom(), string(), undefined | file:name()) -> t().
99new(ReleaseName, ReleaseVsn, Relfile) ->
100    #release_t{name=ReleaseName,
101               vsn=ReleaseVsn,
102               relfile = Relfile}.
103
104-spec new(atom(), string()) -> t().
105new(ReleaseName, ReleaseVsn) ->
106    new(ReleaseName, ReleaseVsn, undefined).
107
108-spec relfile(t(), file:name()) -> t().
109relfile(Release, Relfile) ->
110    Release#release_t{relfile=Relfile}.
111
112-spec name(t()) -> atom().
113name(#release_t{name=Name}) ->
114    Name.
115
116-spec vsn(t()) -> string().
117vsn(#release_t{vsn=Vsn}) ->
118    Vsn.
119
120-spec erts(t(), vsn()) -> t().
121erts(Release, Vsn) ->
122    Release#release_t{erts=Vsn}.
123
124-spec erts(t()) -> vsn().
125erts(#release_t{erts=Vsn}) ->
126    Vsn.
127
128-spec goals(t(), [relx:goal()]) -> t().
129goals(Release, ConfigGoals) ->
130    Release#release_t{goals=parse_goals(ConfigGoals)}.
131
132-spec goals(t()) -> parsed_goals().
133goals(#release_t{goals=Goals}) ->
134    Goals.
135
136-spec parsed_goals(t(), parsed_goals()) -> t().
137parsed_goals(Release, ParsedGoals) ->
138    Release#release_t{goals=ParsedGoals}.
139
140-spec realize(t(), [rlx_app_info:t()]) ->
141                     {ok, t()}.
142realize(Rel, Pkgs0) ->
143    process_specs(realize_erts(Rel), Pkgs0).
144
145applications(#release_t{applications=Apps}) ->
146    Apps.
147
148app_specs(#release_t{app_specs=AppSpecs}) ->
149    AppSpecs.
150
151-spec metadata(t()) -> release_spec().
152metadata(#release_t{name=Name,
153                    vsn=Vsn,
154                    erts=ErtsVsn,
155                    app_specs=Apps,
156                    realized=Realized}) ->
157    case Realized of
158        true ->
159            {release, {rlx_util:to_string(Name), Vsn}, {erts, ErtsVsn}, Apps};
160        false ->
161            erlang:error(?RLX_ERROR({not_realized, Name, Vsn}))
162    end.
163
164%% Include all apps in the release as `none' type so they are not
165%% loaded or started but are available in the path when a the
166%% `start_clean' is used, like is done with command `console_clean'
167-spec start_clean_metadata(t()) -> release_spec().
168start_clean_metadata(#release_t{erts=ErtsVsn,
169                                app_specs=Apps}) ->
170    {value, Kernel, Apps1} = lists:keytake(kernel, 1, Apps),
171    {value, StdLib, Apps2} = lists:keytake(stdlib, 1, Apps1),
172    {release, {"start_clean", "1.0"}, {erts, ErtsVsn}, [Kernel, StdLib | none_type_apps(Apps2)]}.
173
174none_type_apps([]) ->
175    [];
176none_type_apps([{Name, Version} | Rest]) ->
177    [{Name, Version, none} | none_type_apps(Rest)];
178none_type_apps([{Name, Version, _} | Rest]) ->
179    [{Name, Version, none} | none_type_apps(Rest)];
180none_type_apps([{Name, Version, _, _} | Rest]) ->
181    [{Name, Version, none} | none_type_apps(Rest)].
182
183%% no_dot_erlang.boot goes in the root bin dir of the release_handler
184%% so should not have anything specific to the release version in it
185%% Here it only has kernel and stdlib.
186-spec no_dot_erlang_metadata(t()) -> release_spec().
187no_dot_erlang_metadata(#release_t{erts=ErtsVsn,
188                                  app_specs=Apps}) ->
189    {value, Kernel, Apps1} = lists:keytake(kernel, 1, Apps),
190    {value, StdLib, _Apps2} = lists:keytake(stdlib, 1, Apps1),
191    {release, {"no_dot_erlang", "1.0"}, {erts, ErtsVsn}, [Kernel, StdLib]}.
192
193%% @doc produce the canonical name `<name>-<vsn>' for this release
194-spec canonical_name(t()) -> string().
195canonical_name(#release_t{name=Name, vsn=Vsn}) ->
196    erlang:binary_to_list(erlang:iolist_to_binary([erlang:atom_to_list(Name), "-", Vsn])).
197
198
199-spec config(t(), list()) -> t().
200config(Release, Config) ->
201    Release#release_t{config=Config}.
202
203-spec config(t()) -> list().
204config(#release_t{config=Config}) ->
205    Config.
206
207-spec format(t()) -> iolist().
208format(Release) ->
209    format(0, Release).
210
211-spec format(non_neg_integer(), t()) -> iolist().
212format(Indent, #release_t{name=Name,
213                          vsn=Vsn,
214                          erts=ErtsVsn,
215                          realized=Realized,
216                          goals = Goals,
217                          app_specs=Apps}) ->
218    BaseIndent = rlx_util:indent(Indent),
219    [BaseIndent, "release: ", rlx_util:to_string(Name), "-", Vsn, "\n",
220     rlx_util:indent(Indent + 1), "erts: ", ErtsVsn, "\n",
221     rlx_util:indent(Indent + 1), "goals: \n",
222     [[rlx_util:indent(Indent + 2),  format_goal(Goal), "\n"] || {_, Goal} <- Goals],
223     case Realized of
224         true ->
225             [rlx_util:indent(Indent + 1), "applications: \n",
226              [[rlx_util:indent(Indent + 2),  io_lib:format("~p", [App]), "\n"] ||
227                  App <- Apps]];
228         false ->
229             []
230     end].
231
232-spec format_goal(parsed_goal()) -> iolist().
233format_goal(#{name := Name,
234              vsn := Vsn}) when Vsn =/= undefined ->
235    io_lib:format("{~p, ~s}", [Name, Vsn]);
236format_goal(#{name := Name}) ->
237    io_lib:format("~p", [Name]).
238
239-spec format_error(Reason::term()) -> iolist().
240format_error({failed_to_parse, Con}) ->
241    io_lib:format("Failed to parse constraint ~p", [Con]);
242format_error({invalid_constraint, _, Con}) ->
243    io_lib:format("Invalid constraint specified ~p", [Con]);
244format_error({not_realized, Name, Vsn}) ->
245    io_lib:format("Unable to produce metadata release: ~p-~s has not been realized",
246                  [Name, Vsn]).
247
248%%%===================================================================
249%%% Internal Functions
250%%%===================================================================
251-spec realize_erts(t()) -> t().
252realize_erts(Rel=#release_t{erts=undefined}) ->
253    Rel#release_t{erts=erlang:system_info(version)};
254realize_erts(Rel) ->
255    Rel.
256
257-spec process_specs(t(), [rlx_app_info:t()]) -> {ok, t()}.
258process_specs(Rel=#release_t{goals=Goals}, World) ->
259    IncludedApps = lists:foldl(fun(#{included_applications := I}, Acc) ->
260                                       sets:union(sets:from_list(I), Acc)
261                               end, sets:new(), World),
262    Specs = [create_app_spec(App, Goals, IncludedApps) || App <- World],
263    {ok, Rel#release_t{goals=Goals,
264                       app_specs=Specs,
265                       applications=World,
266                       realized=true}}.
267
268-spec create_app_spec(rlx_app_info:t(), parsed_goals(), sets:set(atom())) -> application_spec().
269create_app_spec(App, Goals, WorldIncludedApps) ->
270    %% If the app only exists as a dependency in an included app then it should
271    %% get the 'load' annotation unless the release spec has set something
272    AppName = rlx_app_info:name(App),
273    Vsn = rlx_app_info:vsn(App),
274
275    TypeAnnot = case sets:is_element(AppName, WorldIncludedApps) of
276                true ->
277                    load;
278                false ->
279                    permanent
280            end,
281
282    #{type := Type,
283      included_applications := IncludedApplications} =
284        list_find(AppName, Goals, #{type => TypeAnnot,
285                                    included_applications => undefined}),
286
287    case {Type, IncludedApplications} of
288        {undefined, undefined} ->
289            {AppName, Vsn};
290        {Type, undefined} when Type =:= permanent ;
291                               Type =:= transient ;
292                               Type =:= temporary ;
293                               Type =:= load ;
294                               Type =:= none ->
295            maybe_with_type({AppName, Vsn}, Type);
296        {undefined, IncludedApplications} ->
297            {AppName, Vsn, IncludedApplications};
298        {Type, IncludedApplications} ->
299            maybe_with_type({AppName, Vsn, Type, IncludedApplications}, Type);
300        _ ->
301            error(?RLX_ERROR({bad_app_goal, {AppName, Vsn, Type, IncludedApplications}}))
302    end.
303
304list_find(Key, List, Default) ->
305    case lists:keyfind(Key, 1, List) of
306        {Key, Value} ->
307            Value;
308        false ->
309            Default
310    end.
311
312%% keep a clean .rel file by only included non-defaults
313maybe_with_type(Tuple, permanent) ->
314    Tuple;
315maybe_with_type(Tuple, Type) ->
316    erlang:insert_element(3, Tuple, Type).
317
318-spec parse_goals([application_spec()]) -> parsed_goals().
319parse_goals(ConfigGoals) ->
320    lists:map(fun(ConfigGoal) ->
321                      Goal = #{name := Name} = parse_goal(ConfigGoal),
322                      {Name, maps:merge(#{vsn=> undefined,
323                                          type => undefined,
324                                          included_applications => undefined}, Goal)}
325              end, ConfigGoals).
326
327-spec parse_goal(relx:goal()) -> parsed_goal().
328parse_goal(AppName) when is_atom(AppName) ->
329    #{name => AppName};
330parse_goal({AppName, Type}) when Type =:= permanent ;
331                                 Type =:= transient ;
332                                 Type =:= temporary ;
333                                 Type =:= load ;
334                                 Type =:= none ->
335    #{name => AppName,
336      type => Type};
337parse_goal({AppName, IncludedApplications=[H|_]}) when is_atom(H) ->
338    #{name => AppName,
339      included_applications => IncludedApplications};
340parse_goal({AppName, []}) when is_atom(AppName) ->
341    #{name => AppName,
342      included_applications => []};
343parse_goal({AppName, Vsn}) when is_list(Vsn) ->
344    #{name => AppName,
345      vsn => Vsn};
346parse_goal({AppName, Vsn, Type})
347  when is_list(Vsn) andalso (Type =:= permanent orelse
348                             Type =:= transient orelse
349                             Type =:= temporary orelse
350                             Type =:= load orelse
351                             Type =:= none) ->
352    #{name => AppName,
353      vsn => Vsn,
354      type => Type};
355parse_goal({AppName, Vsn, IncludedApplications}) when is_list(Vsn) ,
356                                                      is_list(IncludedApplications) ->
357    #{name => AppName,
358      vsn => Vsn,
359      included_applications => IncludedApplications};
360parse_goal({AppName, Vsn, Type, IncludedApplications})
361  when is_list(Vsn) andalso is_list(IncludedApplications) andalso (Type =:= permanent orelse
362                                                                   Type =:= transient orelse
363                                                                   Type =:= temporary orelse
364                                                                   Type =:= load orelse
365                                                                   Type =:= none) ->
366    #{name => AppName,
367      vsn => Vsn,
368      type => Type,
369      included_applications => IncludedApplications};
370parse_goal(Goal) ->
371    error(?RLX_ERROR({bad_goal, Goal})).
372