From 6f06c48a9ce7ff9b5a01b9210b7314ba4a3b24e9 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 18 Sep 2012 10:08:30 -0700 Subject: support release generation in the system --- src/rcl_prv_release.erl | 189 ++++++++++++++++++++++++++++++++++++++++++++ src/rcl_release.erl | 147 ++++++++++++++++++++++++++++++---- test/rclt_release_SUITE.erl | 131 ++++++++++++++++++++++++++++++ 3 files changed, 452 insertions(+), 15 deletions(-) create mode 100644 src/rcl_prv_release.erl create mode 100644 test/rclt_release_SUITE.erl 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 +%%% @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) -> diff --git a/test/rclt_release_SUITE.erl b/test/rclt_release_SUITE.erl new file mode 100644 index 0000000..ae19b29 --- /dev/null +++ b/test/rclt_release_SUITE.erl @@ -0,0 +1,131 @@ +%%% -*- 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 Merrit +%%% @copyright (C) 2012, Eric Merrit +-module(rclt_release_SUITE). + +-export([suite/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + all/0, + normal_case/1]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +suite() -> + [{timetrap,{seconds,30}}]. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_testcase(_, Config) -> + DataDir = proplists:get_value(data_dir, Config), + LibDir1 = filename:join([DataDir, create_random_name("lib_dir1_")]), + ok = rcl_util:mkdir_p(LibDir1), + State = rcl_state:new([{lib_dirs, [LibDir1]}], []), + [{lib1, LibDir1}, + {state, State} | Config]. + +all() -> + [normal_case]. + +normal_case(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + [(fun({Name, Vsn}) -> + create_app(LibDir1, Name, Vsn, [kernel, stdlib], []) + end)(App) + || + App <- + [{create_random_name("lib_app1_"), create_random_vsn()} + || _ <- lists:seq(1, 100)]], + + create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []), + create_app(LibDir1, "lib_dep_1", "0.0.1", [stdlib,kernel], []), + create_app(LibDir1, "goal_app_2", "0.0.1", [stdlib,kernel,goal_app_1,non_goal_2], []), + create_app(LibDir1, "non_goal_1", "0.0.1", [stdlib,kernel], [lib_dep_1]), + create_app(LibDir1, "non_goal_2", "0.0.1", [stdlib,kernel], []), + + + ConfigFile = filename:join([LibDir1, "relcool.config"]), + write_config(ConfigFile, + [{release, {foo, "0.0.1"}, + [goal_app_1, + goal_app_2]}]), + OutputDir = filename:join([proplists:get_value(data_dir, Config), + create_random_name("relcool-output")]), + {ok, State} = relcool:do(undefined, undefined, [], [LibDir1], 2, + OutputDir, [ConfigFile]), + [{{foo, "0.0.1"}, Release}] = ec_dictionary:to_list(rcl_state:releases(State)), + AppSpecs = rcl_release:applications(Release), + ?assert(lists:keymember(stdlib, 1, AppSpecs)), + ?assert(lists:keymember(kernel, 1, AppSpecs)), + ?assert(lists:member({non_goal_1, "0.0.1"}, AppSpecs)), + ?assert(lists:member({non_goal_2, "0.0.1"}, AppSpecs)), + ?assert(lists:member({goal_app_1, "0.0.1"}, AppSpecs)), + ?assert(lists:member({goal_app_2, "0.0.1"}, AppSpecs)), + ?assert(lists:member({lib_dep_1, "0.0.1", load}, AppSpecs)). + + + + +%%%=================================================================== +%%% Helper Functions +%%%=================================================================== +create_app(Dir, Name, Vsn, Deps, LibDeps) -> + AppDir = filename:join([Dir, Name]), + write_app_file(AppDir, Name, Vsn, Deps, LibDeps), + write_beam_file(AppDir, Name), + rcl_app_info:new(erlang:list_to_atom(Name), Vsn, AppDir, + Deps, []). + +write_beam_file(Dir, Name) -> + Beam = filename:join([Dir, "ebin", "not_a_real_beam" ++ Name ++ ".beam"]), + ok = filelib:ensure_dir(Beam), + ok = ec_file:write_term(Beam, testing_purposes_only). + +write_app_file(Dir, Name, Version, Deps, LibDeps) -> + Filename = filename:join([Dir, "ebin", Name ++ ".app"]), + ok = filelib:ensure_dir(Filename), + ok = ec_file:write_term(Filename, get_app_metadata(Name, Version, Deps, LibDeps)). + +get_app_metadata(Name, Vsn, Deps, LibDeps) -> + {application, erlang:list_to_atom(Name), + [{description, ""}, + {vsn, Vsn}, + {modules, []}, + {included_applications, LibDeps}, + {applications, Deps}]}. + +create_random_name(Name) -> + random:seed(erlang:now()), + Name ++ erlang:integer_to_list(random:uniform(1000000)). + +create_random_vsn() -> + random:seed(erlang:now()), + lists:flatten([erlang:integer_to_list(random:uniform(100)), + ".", erlang:integer_to_list(random:uniform(100)), + ".", erlang:integer_to_list(random:uniform(100))]). + +write_config(Filename, Values) -> + ok = ec_file:write(Filename, + [io_lib:format("~p.\n", [Val]) || Val <- Values]). -- cgit v1.2.3