aboutsummaryrefslogtreecommitdiffstats
path: root/src/rlx_release.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/rlx_release.erl')
-rw-r--r--src/rlx_release.erl415
1 files changed, 415 insertions, 0 deletions
diff --git a/src/rlx_release.erl b/src/rlx_release.erl
new file mode 100644
index 0000000..68193fa
--- /dev/null
+++ b/src/rlx_release.erl
@@ -0,0 +1,415 @@
+%% -*- 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,
+ name/1,
+ vsn/1,
+ realize/3,
+ applications/1,
+ application_details/1,
+ application_details/2,
+ realized/1,
+ metadata/1,
+ canonical_name/1,
+ 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_lib("relx/include/relx.hrl").
+
+-record(release_t, {name :: atom(),
+ vsn :: ec_semver:any_version(),
+ erts :: 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()]}).
+
+%%============================================================================
+%% 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() | none}.
+
+-type annotations() :: ec_dictionary:dictionary(app_name(),
+ {app_type(), incl_apps() | none}).
+
+
+-opaque t() :: record(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}) ->
+ Goals.
+
+-spec realize(t(), [{app_name(), app_vsn()}], [rlx_app_info:t()]) ->
+ {ok, t()} | relx:error().
+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.
+
+%% @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 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 + 1), " erts-", ErtsVsn,
+ ", realized = ", erlang:atom_to_list(Realized), "\n",
+ BaseIndent, "goals: \n",
+ [[rlx_util:indent(Indent + 1), format_goal(Goal), ",\n"] || Goal <- Goals],
+ case Realized of
+ true ->
+ [BaseIndent, "applications: \n",
+ [[rlx_util:indent(Indent + 1), 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 ->
+ none
+ end,
+ BaseAnnots =
+ try
+ case ec_dictionary:get(AppName, Annots) of
+ {none, Incld} ->
+ {TypeAnnot, Incld};
+ Else ->
+ Else
+ end
+ catch
+ throw:not_found ->
+ {TypeAnnot, none}
+ end,
+ Vsn = rlx_app_info:vsn_as_string(App),
+ case BaseAnnots of
+ {none, none} ->
+ {AppName, Vsn};
+ {Type, none} ->
+ {AppName, Vsn, Type};
+ {none, 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, none});
+ 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, {ok, Release}) ->
+ case parse_constraint(Constraint0) of
+ {ok, Constraint1} ->
+ parse_goal1(Release, Constraint1, {none, none});
+ 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 = [Constraint | Goals]}}
+ 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) ->
+ Constraint1 = parse_version(Constraint0),
+ case rlx_depsolver:is_valid_constraint(Constraint1) of
+ false ->
+ ?RLX_ERROR({invalid_constraint, 2, Constraint0});
+ true ->
+ {ok, Constraint1}
+ 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 parse_version(rlx_depsolver:raw_constraint()) ->
+ rlx_depsolver:constraint().
+parse_version({AppName, Version})
+ when erlang:is_binary(Version);
+ erlang:is_list(Version) ->
+ {AppName, rlx_depsolver:parse_version(Version)};
+parse_version({AppName, Version, Constraint})
+ when erlang:is_binary(Version);
+ erlang:is_list(Version) ->
+ {AppName, rlx_depsolver:parse_version(Version), Constraint};
+parse_version({AppName, Version, Constraint0, Constraint1})
+ when erlang:is_binary(Version);
+ erlang:is_list(Version) ->
+ {AppName, rlx_depsolver:parse_version(Version), Constraint1, Constraint0};
+parse_version(Constraint) ->
+ Constraint.
+
+to_atom(RelName)
+ when erlang:is_list(RelName) ->
+ erlang:list_to_atom(RelName);
+to_atom(Else)
+ when erlang:is_atom(Else) ->
+ Else.