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