diff options
author | Eric <[email protected]> | 2012-09-18 10:08:30 -0700 |
---|---|---|
committer | Eric <[email protected]> | 2012-09-18 10:08:30 -0700 |
commit | 6f06c48a9ce7ff9b5a01b9210b7314ba4a3b24e9 (patch) | |
tree | 30bb7e7a5ce91c4f2626124617cdcf3ef869cf1f /src | |
parent | b89161601b3b23b22f4624c5eb2ff0d6644c10c6 (diff) | |
download | relx-6f06c48a9ce7ff9b5a01b9210b7314ba4a3b24e9.tar.gz relx-6f06c48a9ce7ff9b5a01b9210b7314ba4a3b24e9.tar.bz2 relx-6f06c48a9ce7ff9b5a01b9210b7314ba4a3b24e9.zip |
support release generation in the system
Diffstat (limited to 'src')
-rw-r--r-- | src/rcl_prv_release.erl | 189 | ||||
-rw-r--r-- | src/rcl_release.erl | 147 |
2 files changed, 321 insertions, 15 deletions
diff --git a/src/rcl_prv_release.erl b/src/rcl_prv_release.erl new file mode 100644 index 0000000..7429e47 --- /dev/null +++ b/src/rcl_prv_release.erl @@ -0,0 +1,189 @@ +%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- +%%% 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 provider uses the lib_dir setting of the state. It searches the +%%% Lib Dirs looking for all OTP Applications that are available. When it finds +%%% those OTP Applications it loads the information about them and adds them to +%%% the state of available apps. This implements the rcl_provider behaviour. +-module(rcl_prv_release). + +-behaviour(rcl_provider). + +-export([init/1, + do/1, + format_error/1]). + +%%============================================================================ +%% API +%%============================================================================ +-spec init(rcl_state:t()) -> {ok, rcl_state:t()}. +init(State) -> + {ok, State}. + +%% @doc recursively dig down into the library directories specified in the state +%% looking for OTP Applications +-spec do(rcl_state:t()) -> {error, Reason::term()} | {ok, rcl_state:t()}. +do(State) -> + DepGraph = create_dep_graph(State), + find_default_release(State, DepGraph). + +-spec format_error({error, ErrorDetail::term()}) -> iolist(). +format_error({error, {no_release_name, Vsn}}) -> + io_lib:format("A target release version was specified (~s) but no name", [Vsn]); +format_error({error, {invalid_release_info, Info}}) -> + io_lib:format("Target release information is in an invalid format ~p", [Info]); +format_error({error, {multiple_release_names, RelA, RelB}}) -> + io_lib:format("No default release name was specified and there are multiple " + "releases in the config: ~s, ~s", + [RelA, RelB]); +format_error({error, no_releases_in_system}) -> + "No releases have been specified in the system!"; +format_error({error, {no_releases_for, RelName}}) -> + io_lib:format("No releases exist in the system for ~s!", [RelName]); +format_error({release_not_found, {RelName, RelVsn}}) -> + io_lib:format("No releases exist in the system for ~p:~s!", [RelName, RelVsn]); +format_error({failed_solve, Error}) -> + io_lib:format("Failed to solve release:\n ~s", + [depsolver:format_error({error, Error})]). + +%%%=================================================================== +%%% Internal Functions +%%%=================================================================== +-spec create_dep_graph(rcl_state:t()) -> depsolver:t(). +create_dep_graph(State) -> + Apps = rcl_state:available_apps(State), + Graph0 = depsolver:new_graph(), + lists:foldl(fun(App, Graph1) -> + AppName = rcl_app_info:name(App), + AppVsn = rcl_app_info:vsn(App), + Deps = rcl_app_info:active_deps(App) ++ + rcl_app_info:library_deps(App), + depsolver:add_package_version(Graph1, + AppName, + AppVsn, + Deps) + end, Graph0, Apps). + + +-spec find_default_release(rcl_state:t(), depsolver:t()) -> + {ok, rcl_state:t()} | {error, Reason::term()}. +find_default_release(State, DepGraph) -> + case rcl_state:default_release(State) of + {undefined, undefined} -> + resolve_default_release(State, DepGraph); + {RelName, undefined} -> + resolve_default_version(State, DepGraph, RelName); + {undefined, Vsn} -> + {error, {no_release_name, Vsn}}; + {RelName, RelVsn} -> + solve_release(State, DepGraph, RelName, RelVsn) + end. + +resolve_default_release(State0, DepGraph) -> + %% Here we will just get the lastest version and run that. + case lists:sort(fun release_sort/2, + ec_dictionary:to_list(rcl_state:releases(State0))) of + All = [{{RelName, RelVsn}, _} | _] -> + State1 = rcl_state:default_release(State0, RelName, RelVsn), + lists:foldl(fun({{RN, RV}, _}, {ok, State2}) -> + solve_release(State2, + DepGraph, RN, RV); + (_, E) -> + E + end, {ok, State1}, All); + [] -> + ?RCL_ERROR(no_releases_in_system) + end. + +resolve_default_version(State0, DepGraph, RelName) -> + %% Here we will just get the lastest version and run that. + AllReleases = ec_dictionary:to_list(rcl_state:releases(State0)), + SpecificReleases = [Rel || Rel={{PossibleRelName, _}, _} <- AllReleases, + PossibleRelName =:= RelName], + case lists:sort(fun release_sort/2, SpecificReleases) of + All = [{{RelName, RelVsn}, _} | _] -> + State1 = rcl_state:default_release(State0, RelName, RelVsn), + lists:foldl(fun({RN, RV}, {ok, State2}) -> + solve_release(State2, + DepGraph, RN, RV); + (_, E) -> + E + end, {ok, State1}, All); + [] -> + ?RCL_ERROR({no_releases_for, RelName}) + end. + + +-spec release_sort({{rcl_release:name(),rcl_release:vsn()}, term()}, + {{rcl_release:name(),rcl_release:vsn()}, term()}) -> + boolean(). +release_sort({{RelName, RelVsnA}, _}, + {{RelName, RelVsnB}, _}) -> + ec_semver:lte(RelVsnA, RelVsnB); +release_sort({{RelNameA, RelVsnA}, _}, {{RelNameB, RelVsnB}, _}) -> + %% The release names are different. When the releases are named differently + %% we can not just take the lastest version. You *must* provide a default + %% release name at least. So we throw an error here that the top can catch + %% and return + erlang:atom_to_list(RelNameA) =< erlang:atom_to_list(RelNameB) andalso + ec_semver:lte(RelVsnA, RelVsnB). + + + +solve_release(State0, DepGraph, RelName, RelVsn) -> + try + Release = rcl_state:get_release(State0, RelName, RelVsn), + Goals = rcl_release:goals(Release), + case depsolver:solve(DepGraph, Goals) of + {ok, Pkgs} -> + set_resolved(State0, Release, Pkgs); + {error, Error} -> + {error, {failed_solve, Error}} + end + catch + throw:not_found -> + {error, {release_not_found, RelName, RelVsn}} + end. + +set_resolved(State, Release0, Pkgs) -> + case rcl_release:realize(Release0, Pkgs, rcl_state:available_apps(State)) of + {ok, Release1} -> + rcl_log:info(rcl_state:log(State), + "Resolved ~p-~s", + [rcl_release:name(Release1), + rcl_release:vsn(Release1)]), + rcl_log:debug(rcl_state:log(State), + fun() -> + rcl_release:format(1, Release1) + end), + {ok, rcl_state:update_release(State, Release1)}; + {error, E} -> + {error, {release_error, E}} + end. + + +%%%=================================================================== +%%% Test Functions +%%%=================================================================== + +-ifndef(NOTEST). +-include_lib("eunit/include/eunit.hrl"). + +-endif. diff --git a/src/rcl_release.erl b/src/rcl_release.erl index 889c0ee..4d3b768 100644 --- a/src/rcl_release.erl +++ b/src/rcl_release.erl @@ -29,8 +29,12 @@ goals/1, name/1, vsn/1, + realize/3, + applications/1, + application_details/1, format/1, - format/2]). + format/2, + format_error/1]). -export_type([t/0, name/0, @@ -46,11 +50,9 @@ erts :: ec_semver:any_version(), goals = [] :: [depsolver:constraint()], realized = false :: boolean(), - annotations = undefined :: ec_dictionary:dictionary(app_name(), - app_type() | - incl_apps() | - {app_type(), incl_apps()}), - applications = []:: [application_spec()]}). + annotations = undefined :: annotations(), + applications = [] :: [application_spec()], + app_detail = [] :: [rcl_app_info:t()]}). %%============================================================================ %% types @@ -70,6 +72,10 @@ {depsolver:constraint(), app_type() | incl_apps()} | {depsolver:constraint(), app_type(), incl_apps()}. +-type annotations() :: ec_dictionary:dictionary(app_name(), + {app_type(), incl_apps() | none}). + + -opaque t() :: record(release_t). %%============================================================================ @@ -105,23 +111,49 @@ goals(Release, Goals0) -> goals(#release_t{goals=Goals}) -> Goals. +-spec realize(t(), [{app_name(), app_vsn()}], [rcl_app_info:t()]) -> + {ok, t()} | {error, Reason::term()}. +realize(Rel, Pkgs0, World0) -> + World1 = subset_world(Pkgs0, World0), + case rcl_topo:sort_apps(World1) of + {ok, Pkgs1} -> + process_specs(realize_erts(Rel), Pkgs1); + {error, E} -> + {error, {topo_error, E}} + 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 rcl_app_info objects representing the applications in +%% this release. These can only be populated by the 'realize' call in this +%% module. +-spec application_details(t()) -> [rcl_app_info:t()]. +application_details(#release_t{app_detail=App}) -> + App. + + -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}) -> + goals = Goals, applications=Apps}) -> BaseIndent = rcl_util:indent(Indent), - [BaseIndent, "release: ", erlang:atom_to_list(Name), "-", Vsn, - " erts-", ErtsVsn, ", realized = ", erlang:atom_to_list(Realized), "\n", + [BaseIndent, "release: ", erlang:atom_to_list(Name), "-", Vsn, "\n", + rcl_util:indent(Indent + 1), " erts-", ErtsVsn, + ", realized = ", erlang:atom_to_list(Realized), "\n", BaseIndent, "goals: \n", [[rcl_util:indent(Indent + 1), format_goal(Goal), ",\n"] || Goal <- Goals], case Realized of true -> [BaseIndent, "applications: \n", - [[rcl_util:indent(Indent + 1), io_lib:format("~p", [App]), ",\n"] - || App <- Apps]]; + [[rcl_util:indent(Indent + 1), io_lib:format("~p", [App]), ",\n"] || + App <- Apps]]; false -> [] end]. @@ -133,16 +165,101 @@ format_goal({Constraint, AppType, AppInc}) -> format_goal(Constraint) -> depsolver:format_constraint(Constraint). +format_error({error, {topo_error, E}}) -> + rcl_topo:format_error({error, E}). %%%=================================================================== %%% 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(), [rcl_app_info:t()]) -> + {ok, t()} | {error, Reason::term()}. +process_specs(Rel=#release_t{annotations=Annots, + goals=Goals}, World) -> + ActiveApps = lists:flatten([rcl_app_info:active_deps(El) || El <- World] ++ + [case get_app_name(Goal) of + {error, _} -> []; + G -> G + end || Goal <- Goals]), + LibraryApps = lists:flatten([rcl_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(), rcl_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 = rcl_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 = rcl_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()}], [rcl_app_info:t()]) -> [rcl_app_info:t()]. +subset_world(Pkgs, World) -> + [get_app_info(Pkg, World) || Pkg <- Pkgs]. + +-spec get_app_info({app_name(), app_vsn()}, [rcl_app_info:t()]) -> rcl_app_info:t(). +get_app_info({PkgName, PkgVsn}, World) -> + {ok, WorldEl} = + ec_lists:find(fun(El) -> + rcl_app_info:name(El) =:= PkgName andalso + rcl_app_info:vsn(El) =:= PkgVsn + end, World), + WorldEl. + -spec parse_goal0(application_goal(), {ok, t()} | {error, Reason::term()}) -> {ok, t()} | {error, Reason::term()}. -parse_goal0({Constraint0, Annots}, {ok, Release}) -> - parse_goal1(Release, Constraint0, Annots); +parse_goal0({Constraint0, Annots}, {ok, Release}) + when erlang:is_atom(Annots) -> + parse_goal1(Release, Constraint0, {Annots, none}); +parse_goal0({Constraint0, Annots}, {ok, Release}) + when erlang:is_list(Annots) -> + parse_goal1(Release, Constraint0, {none, Annots}); parse_goal0({Constraint0, AnnotsA, AnnotsB}, {ok, Release}) -> parse_goal1(Release, Constraint0, {AnnotsA, AnnotsB}); -parse_goal0(Constraint0, {ok, Release = #release_t{goals=Goals}}) -> +parse_goal0(Constraint0, {ok, Release = #release_t{goals=Goals}}) + when erlang:is_atom(Constraint0) -> + {ok, Release#release_t{goals = [Constraint0 | Goals]}}; +parse_goal0(Constraint0, {ok, Release = #release_t{goals=Goals}}) + when erlang:is_list(Constraint0) -> case rcl_goal:parse(Constraint0) of E = {error, _} -> E; @@ -153,7 +270,7 @@ parse_goal0(_, E = {error, _}) -> E. -spec parse_goal1(t(), depsolver:constraint() | string(), - app_type() | incl_apps() | {app_type(), incl_apps()}) -> + app_type() | incl_apps() | {app_type(), incl_apps() | none}) -> {ok, t()} | {error, Reason::term()}. parse_goal1(Release = #release_t{annotations=Annots, goals=Goals}, Constraint0, NewAnnots) -> |