aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEric <[email protected]>2012-09-18 10:08:30 -0700
committerEric <[email protected]>2012-09-18 10:08:30 -0700
commit6f06c48a9ce7ff9b5a01b9210b7314ba4a3b24e9 (patch)
tree30bb7e7a5ce91c4f2626124617cdcf3ef869cf1f
parentb89161601b3b23b22f4624c5eb2ff0d6644c10c6 (diff)
downloadrelx-6f06c48a9ce7ff9b5a01b9210b7314ba4a3b24e9.tar.gz
relx-6f06c48a9ce7ff9b5a01b9210b7314ba4a3b24e9.tar.bz2
relx-6f06c48a9ce7ff9b5a01b9210b7314ba4a3b24e9.zip
support release generation in the system
-rw-r--r--src/rcl_prv_release.erl189
-rw-r--r--src/rcl_release.erl147
-rw-r--r--test/rclt_release_SUITE.erl131
3 files changed, 452 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) ->
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 <[email protected]>
+%%% @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]).