%% -*- 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 <[email protected]>
%%% @copyright (C) 2012 Erlware, LLC.
%%%
%%% @doc This module represents a release and its metadata and is used to
%%% manipulate the release metadata.
-module(rlx_release).
-export([new/2,
new/3,
relfile/1,
relfile/2,
erts/2,
erts/1,
goals/2,
goals/1,
constraints/1,
merge_application_goals/2,
name/1,
vsn/1,
realize/3,
applications/1,
application_details/1,
application_details/2,
realized/1,
metadata/1,
start_clean_metadata/1,
no_dot_erlang_metadata/1,
canonical_name/1,
config/1,
config/2,
format/1,
format/2,
format_error/1]).
-export_type([t/0,
name/0,
vsn/0,
app_name/0,
app_vsn/0,
app_type/0,
application_spec/0,
application_goal/0]).
-include("relx.hrl").
-record(release_t, {name :: atom(),
vsn :: ec_semver:any_version(),
erts :: undefined | ec_semver:any_version(),
goals = [] :: [rlx_depsolver:constraint()],
realized = false :: boolean(),
annotations = undefined :: annotations(),
applications = [] :: [application_spec()],
relfile :: undefined | string(),
app_detail = [] :: [rlx_app_info:t()],
config = []}).
%%============================================================================
%% types
%%============================================================================
-type name() :: atom().
-type vsn() :: string().
-type app_name() :: atom().
-type app_vsn() :: string().
-type app_type() :: permanent | transient | temporary | load | none.
-type incl_apps() :: [app_name()].
-type application_spec() :: {app_name(), app_vsn()} |
{app_name(), app_vsn(), app_type() | incl_apps()} |
{app_name(), app_vsn(), app_type(), incl_apps()}.
-type application_constraint() :: rlx_depsolver:raw_constraint() | string() | binary().
-type application_goal() :: application_constraint()
| {application_constraint(), app_type() | incl_apps()}
| {application_constraint(), app_type(), incl_apps() | void}.
-type annotations() :: ec_dictionary:dictionary(app_name(),
{app_type(), incl_apps() | void}).
-opaque t() :: #release_t{}.
%%============================================================================
%% API
%%============================================================================
-spec new(atom(), string(), undefined | file:name()) -> t().
new(ReleaseName, ReleaseVsn, Relfile) ->
#release_t{name=to_atom(ReleaseName), vsn=ReleaseVsn,
relfile = Relfile,
annotations=ec_dictionary:new(ec_dict)}.
-spec new(atom(), string()) -> t().
new(ReleaseName, ReleaseVsn) ->
new(ReleaseName, ReleaseVsn, undefined).
-spec relfile(t()) -> file:name() | undefined.
relfile(#release_t{relfile=Relfile}) ->
Relfile.
-spec relfile(t(), file:name()) -> t().
relfile(Release, Relfile) ->
Release#release_t{relfile=Relfile}.
-spec name(t()) -> atom().
name(#release_t{name=Name}) ->
Name.
-spec vsn(t()) -> string().
vsn(#release_t{vsn=Vsn}) ->
Vsn.
-spec erts(t(), app_vsn()) -> t().
erts(Release, Vsn) ->
Release#release_t{erts=Vsn}.
-spec erts(t()) -> app_vsn().
erts(#release_t{erts=Vsn}) ->
Vsn.
-spec goals(t(), [application_goal()]) -> {ok, t()} | relx:error().
goals(Release, Goals0) ->
lists:foldl(fun parse_goal0/2,
{ok, Release}, Goals0).
-spec goals(t()) -> [application_goal()].
goals(#release_t{goals=Goals, annotations=Annots}) ->
[application_goal(Goal, Annots) || Goal <- Goals].
-spec constraints(t()) -> [rlx_depsolver:raw_constraint()].
constraints(#release_t{goals=Goals}) ->
Goals.
-spec realize(t(), [{app_name(), app_vsn()}], [rlx_app_info:t()]) ->
{ok, t()}.
realize(Rel, Pkgs0, World0) ->
World1 = subset_world(Pkgs0, World0),
case rlx_topo:sort_apps(World1) of
{ok, Pkgs1} ->
process_specs(realize_erts(Rel), Pkgs1);
Error={error, _} ->
Error
end.
%% @doc this gives the application specs for the release. This can only be
%% populated by the 'realize' call in this module.
-spec applications(t()) -> [application_spec()].
applications(#release_t{applications=Apps}) ->
Apps.
%% @doc this gives the rlx_app_info objects representing the applications in
%% this release. These should only be populated by the 'realize' call in this
%% module or by reading an existing rel file.
-spec application_details(t()) -> [rlx_app_info:t()].
application_details(#release_t{app_detail=App}) ->
App.
%% @doc this is only expected to be called by a process building a new release
%% from an existing rel file.
-spec application_details(t(), [rlx_app_info:t()]) -> t().
application_details(Release, AppDetail) ->
Release#release_t{app_detail=AppDetail}.
-spec realized(t()) -> boolean().
realized(#release_t{realized=Realized}) ->
Realized.
-spec metadata(t()) -> term().
metadata(#release_t{name=Name, vsn=Vsn, erts=ErtsVsn, applications=Apps,
realized=Realized}) ->
case Realized of
true ->
{ok, {release, {erlang:atom_to_list(Name), Vsn}, {erts, ErtsVsn},
Apps}};
false ->
?RLX_ERROR({not_realized, Name, Vsn})
end.
-spec start_clean_metadata(t()) -> term().
start_clean_metadata(#release_t{name=Name, vsn=Vsn, erts=ErtsVsn, applications=Apps,
realized=Realized}) ->
case Realized of
true ->
{value, Kernel, Apps1} = lists:keytake(kernel, 1, Apps),
{value, StdLib, Apps2} = lists:keytake(stdlib, 1, Apps1),
{ok, {release, {erlang:atom_to_list(Name), Vsn}, {erts, ErtsVsn},
[Kernel, StdLib | none_type_apps(Apps2)]}};
false ->
?RLX_ERROR({not_realized, Name, Vsn})
end.
none_type_apps([]) ->
[];
none_type_apps([{Name, Version} | Rest]) ->
[{Name, Version, none} | none_type_apps(Rest)];
none_type_apps([{Name, Version, _} | Rest]) ->
[{Name, Version, none} | none_type_apps(Rest)];
none_type_apps([{Name, Version, _, _} | Rest]) ->
[{Name, Version, none} | none_type_apps(Rest)].
%% The no_dot_erlang.rel.src file is a literal copy of start_clean.rel.src
%% in Erlang/OTP itself.
-spec no_dot_erlang_metadata(t()) -> term().
no_dot_erlang_metadata(T) ->
start_clean_metadata(T).
%% @doc produce the canonical name (<name>-<vsn>) for this release
-spec canonical_name(t()) -> string().
canonical_name(#release_t{name=Name, vsn=Vsn}) ->
erlang:binary_to_list(erlang:iolist_to_binary([erlang:atom_to_list(Name), "-",
Vsn])).
-spec config(t(), list()) -> t().
config(Release, Config) ->
Release#release_t{config=Config}.
-spec config(t()) -> list().
config(#release_t{config=Config}) ->
Config.
-spec format(t()) -> iolist().
format(Release) ->
format(0, Release).
-spec format(non_neg_integer(), t()) -> iolist().
format(Indent, #release_t{name=Name, vsn=Vsn, erts=ErtsVsn, realized=Realized,
goals = Goals, applications=Apps}) ->
BaseIndent = rlx_util:indent(Indent),
[BaseIndent, "release: ", rlx_util:to_string(Name), "-", Vsn, "\n",
rlx_util:indent(Indent + 2), " erts-", ErtsVsn,
", realized = ", erlang:atom_to_list(Realized), "\n",
rlx_util:indent(Indent + 1), "goals: \n",
[[rlx_util:indent(Indent + 2), format_goal(Goal), ",\n"] || Goal <- Goals],
case Realized of
true ->
[rlx_util:indent(Indent + 1), "applications: \n",
[[rlx_util:indent(Indent + 2), io_lib:format("~p", [App]), ",\n"] ||
App <- Apps]];
false ->
[]
end].
-spec format_goal(application_goal()) -> iolist().
format_goal({Constraint, AppType}) ->
io_lib:format("~p", [{rlx_depsolver:format_constraint(Constraint), AppType}]);
format_goal({Constraint, AppType, AppInc}) ->
io_lib:format("~p", [{rlx_depsolver:format_constraint(Constraint), AppType, AppInc}]);
format_goal(Constraint) ->
rlx_depsolver:format_constraint(Constraint).
-spec format_error(Reason::term()) -> iolist().
format_error({topo_error, E}) ->
rlx_topo:format_error(E);
format_error({failed_to_parse, Con}) ->
io_lib:format("Failed to parse constraint ~p", [Con]);
format_error({invalid_constraint, _, Con}) ->
io_lib:format("Invalid constraint specified ~p", [Con]);
format_error({not_realized, Name, Vsn}) ->
io_lib:format("Unable to produce metadata release: ~p-~s has not been realized",
[Name, Vsn]).
%%%===================================================================
%%% Internal Functions
%%%===================================================================
-spec realize_erts(t()) -> t().
realize_erts(Rel=#release_t{erts=undefined}) ->
Rel#release_t{erts=erlang:system_info(version)};
realize_erts(Rel) ->
Rel.
-spec process_specs(t(), [rlx_app_info:t()]) ->
{ok, t()}.
process_specs(Rel=#release_t{annotations=Annots,
goals=Goals}, World) ->
ActiveApps = lists:flatten([rlx_app_info:active_deps(El) || El <- World] ++
[case get_app_name(Goal) of
{error, _} -> [];
G -> G
end || Goal <- Goals]),
LibraryApps = lists:flatten([rlx_app_info:library_deps(El) || El <- World]),
Specs = [create_app_spec(Annots, App, ActiveApps, LibraryApps) || App <- World],
{ok, Rel#release_t{annotations=Annots,
applications=Specs,
app_detail=World,
realized=true}}.
-spec create_app_spec(annotations(), rlx_app_info:t(), [app_name()],
[app_name()]) ->
application_spec().
create_app_spec(Annots, App, ActiveApps, LibraryApps) ->
%% If the app only exists as a dependency in a library app then it should
%% get the 'load' annotation unless the release spec has provided something
%% else
AppName = rlx_app_info:name(App),
TypeAnnot =
case (lists:member(AppName, LibraryApps) and
(not lists:member(AppName, ActiveApps))) of
true ->
load;
false ->
void
end,
BaseAnnots =
try
case ec_dictionary:get(AppName, Annots) of
{void, Incld} ->
{TypeAnnot, Incld};
Else ->
Else
end
catch
throw:not_found ->
{TypeAnnot, void}
end,
Vsn = rlx_app_info:original_vsn(App),
case BaseAnnots of
{void, void} ->
{AppName, Vsn};
{Type, void} ->
{AppName, Vsn, Type};
{void, Incld0} ->
{AppName, Vsn, Incld0};
{Type, Incld1} ->
{AppName, Vsn, Type, Incld1}
end.
-spec subset_world([{app_name(), app_vsn()}], [rlx_app_info:t()]) -> [rlx_app_info:t()].
subset_world(Pkgs, World) ->
[get_app_info(Pkg, World) || Pkg <- Pkgs].
-spec get_app_info({app_name(), app_vsn()}, [rlx_app_info:t()]) -> rlx_app_info:t().
get_app_info({PkgName, PkgVsn}, World) ->
{ok, WorldEl} =
ec_lists:find(fun(El) ->
rlx_app_info:name(El) =:= PkgName andalso
rlx_app_info:vsn(El) =:= PkgVsn
end, World),
WorldEl.
parse_goal0({Constraint0, Annots}, {ok, Release})
when Annots =:= permanent;
Annots =:= transient;
Annots =:= temporary;
Annots =:= load;
Annots =:= none ->
case parse_constraint(Constraint0) of
{ok, Constraint1} ->
parse_goal1(Release, Constraint1, {Annots, void});
Error ->
Error
end;
parse_goal0({Constraint0, Annots, Incls}, {ok, Release})
when (Annots =:= permanent orelse
Annots =:= transient orelse
Annots =:= temporary orelse
Annots =:= load orelse
Annots =:= none),
erlang:is_list(Incls) ->
case parse_constraint(Constraint0) of
{ok, Constraint1} ->
parse_goal1(Release, Constraint1, {Annots, Incls});
Error ->
Error
end;
parse_goal0({Constraint0, Incls}, {ok, Release})
when erlang:is_list(Incls), Incls == [] orelse is_atom(hd(Incls)) ->
case parse_constraint(Constraint0) of
{ok, Constraint1} ->
parse_goal1(Release, Constraint1, {void, Incls});
Error ->
Error
end;
parse_goal0(Constraint0, {ok, Release}) ->
case parse_constraint(Constraint0) of
{ok, Constraint1} ->
parse_goal1(Release, Constraint1, {void, void});
Error ->
Error
end;
parse_goal0(_, E = {error, _}) ->
E;
parse_goal0(Constraint, _) ->
?RLX_ERROR({invalid_constraint, 1, Constraint}).
parse_goal1(Release = #release_t{annotations=Annots, goals=Goals},
Constraint, NewAnnots) ->
case get_app_name(Constraint) of
E1 = {error, _} ->
E1;
AppName ->
{ok,
Release#release_t{annotations=ec_dictionary:add(AppName, NewAnnots, Annots),
goals = Goals++[Constraint]}}
end.
-spec parse_constraint(application_constraint()) ->
rlx_depsolver:constraint() | relx:error().
parse_constraint(Constraint0)
when erlang:is_list(Constraint0); erlang:is_binary(Constraint0) ->
case rlx_goal:parse(Constraint0) of
{fail, _} ->
?RLX_ERROR({failed_to_parse, Constraint0});
{ok, Constraint1} ->
{ok, Constraint1}
end;
parse_constraint(Constraint0)
when erlang:is_tuple(Constraint0);
erlang:is_atom(Constraint0) ->
case rlx_depsolver:is_valid_raw_constraint(Constraint0) of
false ->
?RLX_ERROR({invalid_constraint, 2, Constraint0});
true ->
{ok, Constraint0}
end;
parse_constraint(Constraint) ->
?RLX_ERROR({invalid_constraint, 3, Constraint}).
-spec get_app_name(rlx_depsolver:raw_constraint()) ->
AppName::atom() | relx:error().
get_app_name(AppName) when erlang:is_atom(AppName) ->
AppName;
get_app_name({AppName, _}) when erlang:is_atom(AppName) ->
AppName;
get_app_name({AppName, _, _}) when erlang:is_atom(AppName) ->
AppName;
get_app_name({AppName, _, _, _}) when erlang:is_atom(AppName) ->
AppName;
get_app_name(V) ->
?RLX_ERROR({invalid_constraint, 4, V}).
-spec get_goal_app_name(application_goal()) -> atom() | relx:error().
get_goal_app_name({Constraint, Annots})
when Annots =:= permanent;
Annots =:= transient;
Annots =:= temporary;
Annots =:= load;
Annots =:= none ->
get_app_name(Constraint);
get_goal_app_name({Constraint, Annots, Incls})
when (Annots =:= permanent orelse
Annots =:= transient orelse
Annots =:= temporary orelse
Annots =:= load orelse
Annots =:= none),
erlang:is_list(Incls) ->
get_app_name(Constraint);
get_goal_app_name({Constraint, Incls})
when erlang:is_list(Incls), Incls == [] orelse is_atom(hd(Incls)) ->
get_app_name(Constraint);
get_goal_app_name(Constraint) ->
get_app_name(Constraint).
-spec application_goal(rlx_depsolver:raw_constraint(), annotations()) -> application_goal().
application_goal(Constraint, Annots) ->
AppName = get_app_name(Constraint),
try ec_dictionary:get(AppName, Annots) of
{void, void} ->
Constraint;
{void, Incls} ->
{Constraint, Incls};
{Type, void} ->
{Constraint, Type};
{Type, Incls} ->
{Constraint, Type, Incls}
catch
throw:not_found ->
Constraint
end.
to_atom(RelName)
when erlang:is_list(RelName) ->
erlang:list_to_atom(RelName);
to_atom(Else)
when erlang:is_atom(Else) ->
Else.
-spec merge_application_goals([application_goal()], [application_goal()]) -> [application_goal()].
merge_application_goals(Goals, BaseGoals) ->
Goals ++ lists:foldl(fun filter_goal_by_name/2, BaseGoals, Goals).
filter_goal_by_name(AppGoal, GoalList) when is_list(GoalList) ->
case get_goal_app_name(AppGoal) of
AppName when is_atom(AppName) ->
lists:filter(fun(Goal) -> get_goal_app_name(Goal) /= AppName end, GoalList);
_Error ->
GoalList
end.