--- src/sin_dep_resolver.erl | 62 +++++ src/sin_dep_solver.erl | 622 +++++++++++++++++++++++++++++++++++++++++++++ src/sin_fs_resolver.erl | 233 +++++++++++++++++ src/sin_resolver.erl | 138 ---------- src/sin_test_resolver.erl | 26 ++ 5 files changed, 943 insertions(+), 138 deletions(-) create mode 100644 src/sin_dep_resolver.erl create mode 100644 src/sin_dep_solver.erl create mode 100644 src/sin_fs_resolver.erl delete mode 100644 src/sin_resolver.erl create mode 100644 src/sin_test_resolver.erl
diff --git a/src/sin_dep_resolver.erl b/src/sin_dep_resolver.erl new file mode 100644 index 0000000..9122290 --- /dev/null +++ b/src/sin_dep_resolver.erl @@ -0,0 +1,62 @@ +%%%------------------------------------------------------------------- +%%% @author Eric Merritt <[email protected]> +%%% @copyright (C) 2011, Erlware, LLC. +%%% @doc +%%% A signature definition for resolver providers +%%% @end +%%%------------------------------------------------------------------- +-module(sin_dep_resolver). + +-export([new/3, + app_dependencies/3, + app_versions/2, + resolve/3, + behaviour_info/1]). + +-record(dep_resolver, {impl, state}). + +%%============================================================================ +%% Types +%%============================================================================ +-opaque impl() :: record(dep_resolver). + +%%============================================================================ +%% API +%%============================================================================ + +%% @doc create a new dep resolver with the build config +-spec new(module(), sin_config:matcher(), sin_state:state()) -> impl(). +new(Impl, Config, State) -> + #dep_resolver{impl=Impl, state=Impl:new(Config, State)}. + +%% @doc return the dependency specs for a particular version of +%% an app +-spec app_dependencies(impl(), sin_dep_solver:app(), + sin_dep_solver:version()) -> + {impl(), [sin_dep_solver:spec()]}. +app_dependencies(Resolver = #dep_resolver{impl=Impl, state=State0}, App, Ver) -> + {State1, Deps} = Impl:app_dependencies(State0, App, Ver), + {Resolver#dep_resolver{state=State1}, Deps}. + +%% @doc get all available versions of an app +-spec app_versions(impl(), sin_dep_solver:app()) -> + {impl(), [sin_dep_solver:version()]}. +app_versions(Resolver = #dep_resolver{impl=Impl, state=State0}, App) -> + {State1, Versions} = Impl:app_versions(State0, App), + {Resolver#dep_resolver{state=State1}, Versions}. + +%% @doc resolve the actual app and version and return a path and +%% updated state to the resolved app. This will only be called once per dependency. +-spec resolve(impl(), sin_dep_solver:app(), sin_dep_solver:version()) -> + {impl(), string()}. +resolve(Resolver = #dep_resolver{impl=Impl, state=State0}, App, Vsn) -> + {State1, Path} = Impl:resolve(State0, App, Vsn), + {Resolver#dep_resolver{state=State1}, Path}. + +%% @doc define the behaviour for tasks. +behaviour_info(callbacks) -> + [{new, 2}, + {app_dependencies, 3}, + {app_versions, 2}]; +behaviour_info(_) -> + undefined. diff --git a/src/sin_dep_solver.erl b/src/sin_dep_solver.erl new file mode 100644 index 0000000..14cda97 --- /dev/null +++ b/src/sin_dep_solver.erl @@ -0,0 +1,622 @@ +-module(sin_dep_solver). + +-export([new/1, deps/3, all_deps/3, extract_resolver_state/1]). + +-export_type([spec/0, app_path/0, + state/0, app/0, version/0]). + +-include("internal.hrl"). + +-record(state, {r :: sin_dep_resolver:impl(), + deps :: set(), + dc :: dict(), + vc :: dict()}). + +%%============================================================================ +%% Types +%%============================================================================ + +-opaque state() :: record(state). +-type app() :: atom(). +-type directive() :: gte | lte | lt | gt | eql | between. +-type version() :: string(). +-type spec() :: spec_part() + | {excluded, app()}. +-type spec_part() :: app() + | {app(), version()} + | {app(), version(), directive()} + | {app(), version(), version(), directive()}. +-type app_path() :: [{app(), version()}]. +-type mixed_spec() :: {app, app()} | spec(). +-type internal_spec() :: {app, [spec()]}. + +%%============================================================================ +%% API +%%============================================================================ +-spec new(sin_dep_resolver:impl()) -> state(). +new(R0) -> + #state{r=R0, dc=dict:new(), vc=dict:new(), deps=sets:new()}. + +-spec deps(state(), app(), version()) -> + {state(), [{app(), version()}]}. +deps(State0, App, Ver) -> + Limits = add_limit(State0, '__top_level__', new_limits(), {App, Ver}), + deps(State0#state{deps=sets:add_element(App, sets:new())}, + [], App, Limits, []). + +-spec all_deps(state(), [app()], [{app(), spec()}]) -> + {state(), [{app(), version()}]}. +all_deps(State0, Apps, Limits0) -> + Limits2 = lists:foldl(fun(Dep, Limits1) -> + add_limit(State0, '__top_level__', Limits1, Dep) + end, new_limits(), Limits0), + D2 = lists:foldl(fun(App, D1) -> + sets:add_element(App, D1) + end, sets:new(), Apps), + case all_deps(State0#state{deps=D2}, [], Apps, Limits2, []) of + {_, fail} -> + shrink_deps(State0#state{deps=D2}, Apps, Limits2); + Res -> + Res + end. + +-spec extract_resolver_state(state()) -> sin_dep_resolver:impl(). +extract_resolver_state(#state{r=R0}) -> + R0. + +%%============================================================================ +%% Internal Functions +%% ============================================================================ + +%% @doc tries to shrink the limits provided by the user. Provides a +%% powerset of those limits. Sorts them to broadest first, then runs +%% the resolver on each set of limits until it finds one that +%% passes. Then the possible culprits are the inverse of those limits. +-spec shrink_deps(state(), [app()], [spec()]) -> + {failed, {possible_culprit, [ app() | spec()]}}. +shrink_deps(State0, Apps0, Limits0) -> + AdjustedLimits = lists:map(fun(App) -> {app, App} end, Apps0) ++ Limits0, + BroadestFirst = broadest_first(remove_empty(powerset(AdjustedLimits))), + FirstPassingSet = + case ec_lists:find(fun([]) -> + false; + (LimitSet) -> + {Apps1, Limits1} = disambiguate_set(LimitSet), + case all_deps(State0, [], Apps1, Limits1, []) of + {_, fail} -> + false; + _ -> + true + end + end, BroadestFirst) of + {ok, List} -> + List; + error -> + [] + end, + {failed, {possible_culprit, invert(FirstPassingSet, AdjustedLimits)}}. + +-spec remove_empty([mixed_spec()]) -> [mixed_spec()]. +remove_empty(Limits) -> + %% Every limit needs at least one {app, _} considering that this a + %% powers set we need to roll through the possibilities and remove + %% the ones that only contain limits without apps + lists:filter(fun(LimitSet) -> + lists:any(fun({app, _}) -> + true; + (_) -> + false + end, LimitSet) + end, Limits). + +-spec invert([mixed_spec()], [mixed_spec()]) -> + [app() | spec()]. +invert(FirstPassingSet, PossibleLimits) -> + lists:map(fun({app, App}) -> + App; + (El) -> + El + end, + lists:filter(fun(Element) -> + not lists:member(Element, FirstPassingSet) + end, PossibleLimits)). + +-spec disambiguate_set([mixed_spec]) -> {[app()], [spec()]}. +disambiguate_set(LimitSet) -> + lists:foldl(fun({app, App}, {Apps, Limits}) -> + {[App | Apps], Limits}; + (Limit, {Apps, Limits}) -> + {Apps, [Limit | Limits]} + end, {[], []}, LimitSet). + + +-spec broadest_first([mixed_spec()]) -> + [mixed_spec()]. +broadest_first(Limits) -> + lists:sort(fun(L1, L2) -> + erlang:length(L1) > erlang:length(L2) + end, Limits). + +-spec all_deps(state(), app_path(), [app()], [internal_spec()], + [{app_path(), [app()]}]) -> + {state(), [{app(), version()}]}. +all_deps(State0, _, [], Limits, []) -> + lists:foldl(fun({App0, Lims, _Sources}, {State1=#state{deps=D0}, AppVsns}) -> + {State2, AppVsn} = + lists_some(fun (State3, App1) + when is_atom(App1) -> + {State3, fail}; + (State3, {_App1, Ver} = PV) + when is_list(Ver) -> + {State3, PV}; + (State3, {exclude, App1}) + when is_atom(App1) -> + {State3, {excluded, App1}}; + (State3, {_App2, _Ver, _}) -> + {State3, fail}; + (State3, {_App2, _Ver1, _Ver2, _}) -> + {State3, fail} + end, {State1, sets:to_list(Lims)}), + case AppVsn of + fail -> + case sets:is_element(App0, D0) of + true -> + {State2, fail}; + false -> + {State2, AppVsns} + end; + _ -> + {State2, [AppVsn | AppVsns]} + end + end, {State0, []}, Limits); +all_deps(State0, _OldAppPath, [], Limits, [{AppPath, Apps} | OtherApps]) -> + all_deps(State0, AppPath, Apps, Limits, OtherApps); +all_deps(State0, AppPath, [App | Apps], Limits, OtherApps) -> + deps(State0, AppPath, App, Limits, [{AppPath, Apps} | OtherApps]). + +-spec deps(state(), [app()], app(), [spec()], [app()]) -> + {state(), [{app(), version()}]}. +deps(State0, AppPath, App, Limits, OtherApps) -> + F = fun (State1=#state{deps=D0}, Ver) -> + check_circular(App, Ver, AppPath), + {State2, Deps} = get_deps(State1, App, Ver, Limits), + + ULimits = extend_limits(State0, App, Limits, [{App, Ver} | Deps]), + DepApps = lists:map(fun dep_app/1, Deps), + D2 = lists:foldl(fun(Dep, D1) -> + sets:add_element(Dep, D1) + end, D0, DepApps), + all_deps(State2#state{deps=D2}, + [{App, Ver} | AppPath], DepApps, ULimits, OtherApps) + end, + lists_some(F, limited_app_versions(State0, App, Limits), fail). + +-spec get_deps(state(), app(), version(), [spec()]) -> {state(), [spec()]}. +get_deps(State0 = #state{r=R0, dc=D0}, App, Ver, AppsLimits) -> + case dict:find({App, Ver}, D0) of + {ok, Deps} -> + {State0, Deps}; + error -> + {R2, Deps} = + sin_dep_resolver:app_dependencies(R0, App, Ver), + FilteredDeps = remove_excluded(Deps, AppsLimits), + {State0#state{r=R2, dc=dict:store({App, Ver}, FilteredDeps, D0)}, + FilteredDeps} + end. +-spec remove_excluded([spec()], [spec()]) -> [spec()]. +remove_excluded(Deps, AppsLimits) -> + lists:filter(fun (Dep) -> + DepApp = dep_app(Dep), + not lists:any(fun(Limit) -> + is_excluded(DepApp, Limit) + end, get_limits(AppsLimits, DepApp)) + end, Deps). + +%% @dev given a spec() return the app name +-spec dep_app(spec()) -> app(). +dep_app({exclude, App}) when is_atom(App) -> App; +dep_app({App, _Ver}) -> App; +dep_app({App, _Ver, _}) -> App; +dep_app({App, _Ver1, _Ver2, _}) -> App; +dep_app(App) when is_atom(App) -> App. + +%% @doc limits is an associated list keeping track of all the limits that have +%% been placed on a app +-spec new_limits() -> [spec()]. +new_limits() -> []. + +-spec add_limit(sin_state:state(), app(), [spec()], spec()) -> [spec()]. +add_limit(State, Source, AppsLimits, NewLimit) -> + case is_valid_limit(NewLimit) of + false -> + ewl_talk:say("Invalid constraint ~p found originating at ~p", + [NewLimit, Source]), + ?SIN_RAISE(State, {invalid_constraint, NewLimit, Source}); + true -> + ok + end, + App = dep_app(NewLimit), + {Limits, Sources1} = case lists:keysearch(App, 1, AppsLimits) of + false -> {sets:new(), sets:new()}; + {value, {App, Ls, Sources0}} -> {Ls, Sources0} + end, + [{App, sets:add_element(NewLimit, Limits), + sets:add_element(Source, Sources1)} | + lists:keydelete(App, 1, AppsLimits)]. + +-spec get_limits([internal_spec()], app()) -> [spec()]. +get_limits(AppsLimits, App) -> + case lists:keysearch(App, 1, AppsLimits) of + false -> []; + {value, {App, Limits, _sources}} -> sets:to_list(Limits) + end. + +-spec extend_limits(sin_state:state(), app(), [internal_spec()], [spec()]) -> [internal_spec()]. +extend_limits(State, Source, AppsLimits, Deps) -> + lists:foldl(fun (Dep, PLs) -> add_limit(State, Source, PLs, Dep) end, + AppsLimits, Deps). + +-spec limited_app_versions(state(), app(), [internal_spec()]) -> + {state(), [version()]}. +limited_app_versions(State0, App, AppsLimits) -> + {State1, RawVersions} = get_versions(State0, App), + Versions = lists:reverse(lists:sort(RawVersions)), + {State1, + lists:filter(fun (Ver) -> + lists:all(fun (L) -> + is_version_within_limit(Ver, L) + end, + get_limits(AppsLimits, App)) + end, Versions)}. +-spec get_versions(state(), app()) -> {state(), [version()]}. +get_versions(State0 = #state{r=R0, vc=V0}, App) -> + case dict:find(App, V0) of + {ok, Versions} -> + {State0, Versions}; + error -> + {R1, Versions} = sin_dep_resolver:app_versions(R0, App), + {State0#state{r=R1, vc=dict:store(App, Versions, V0)}, Versions} + end. + +-spec is_excluded(atom(), spec()) -> boolean(). +is_excluded(App, {exclude, App}) -> true; +is_excluded(_App, _) -> false. + +-spec is_valid_limit(spec()) -> boolean(). +is_valid_limit(App) when is_atom(App) -> true; +is_valid_limit({exclude, App}) when is_atom(App) -> true; +is_valid_limit({_App, Ver}) when is_list(Ver) -> true; +is_valid_limit({_App, _LVer, gte}) -> true; +is_valid_limit({_App, _LVer, lte}) -> true; +is_valid_limit({_App, _LVer, gt}) -> true; +is_valid_limit({_App, _LVer, lt}) -> true; +is_valid_limit({_App, _LVer1, _LVer2, between}) -> true; +is_valid_limit(_InvalidLimit) -> false. + +-spec is_version_within_limit(version(), spec()) -> boolean(). +is_version_within_limit(_Ver, App) when is_atom(App) -> true; +is_version_within_limit(_Ver, {exclude, App}) when is_atom(App) -> false; +is_version_within_limit(Ver, {_App, Ver}) when is_list(Ver) -> true; +is_version_within_limit(Ver, {_App, LVer, gte}) when Ver >= LVer -> true; +is_version_within_limit(Ver, {_App, LVer, lte}) when Ver =< LVer -> true; +is_version_within_limit(Ver, {_App, LVer, gt}) when Ver > LVer -> true; +is_version_within_limit(Ver, {_App, LVer, lt}) when Ver < LVer -> true; +is_version_within_limit(Ver, {_App, LVer1, LVer2, between}) + when Ver >= LVer1 andalso Ver =< LVer2 -> true; +is_version_within_limit(_Ver, _App) -> false. + +%% @doc return the first Res = F(el) for El in List that is not False +-spec lists_some(fun(), {state(), [spec()]}) -> + {state(), [{app(), version()}] | fail}. +lists_some(F, L) -> + lists_some(F, L, fail). + +-spec lists_some(fun(), {state(), [spec()]}, boolean()) -> + {state(), [{app(), version()}]}. +lists_some(_, {State0, []}, False) -> + {State0, False}; +lists_some(F, {State0, [H | T]}, False) -> + case F(State0, H) of + {State1, False} -> + lists_some(F, {State1, T}, False); + Res -> + Res + end. + +-spec check_circular(app(), version(), app_path()) -> ok. +check_circular(App, Ver, AppPath) -> + case lists:member({App, Ver}, AppPath) of + true -> + throw({circular_dependency, + {dependency_path, + lists:reverse([{App, Ver} | AppPath])}}); + false -> + ok + end. + +-spec powerset(list()) -> list(). +powerset(Lst) -> + N = length(Lst), + Max = trunc(math:pow(2,N)), + [[lists:nth(Pos+1,Lst) || Pos <- lists:seq(0,N-1), I band (1 bsl Pos) =/= 0] + || I <- lists:seq(0,Max-1)]. + +%%============================================================================ +%% Tests +%%============================================================================ +-ifndef(NO_TESTS). +-include_lib("eunit/include/eunit.hrl"). + +passing_test() -> + + App1Deps = [{app2, "0.1.0", gte}, + {app3, "0.1.1", "0.1.5", between}], + + App2Deps = [{app4, "5.0.0", gte}], + App3Deps = [{app5, "2.0.0", gte}], + App4Deps = [app5], + Config = sin_config:new_from_terms([{{dependencies, app1, "0.1.0"}, App1Deps}, + {{dependencies, app1, "0.2"}, App1Deps}, + {{dependencies, app1, "3.0"}, App1Deps}, + {{dependencies, app2, "0.0.0.1"}, App2Deps}, + {{dependencies, app2, "0.1"}, App2Deps}, + {{dependencies, app2, "1.0"}, App2Deps}, + {{dependencies, app2, "3.0"}, App2Deps}, + {{dependencies, app3, "0.1.0"}, App3Deps}, + {{dependencies, app3, "0.1.3"}, App3Deps}, + {{dependencies, app3, "2.0.0"}, App3Deps}, + {{dependencies, app3, "3.0.0"}, App3Deps}, + {{dependencies, app3, "4.0.0"}, App3Deps}, + {{dependencies, app4, "0.1.0"}, App4Deps}, + {{dependencies, app4, "0.3.0"}, App4Deps}, + {{dependencies, app4, "5.0.0"}, App4Deps}, + {{dependencies, app4, "6.0.0"}, App4Deps}, + {{dependencies, app5, "0.1.0"}, []}, + {{dependencies, app5, "0.3.0"}, []}, + {{dependencies, app5, "2.0.0"}, []}, + {{dependencies, app5, "6.0.0"}, []}, + {{versions, app1}, ["0.1.1", + "0.2", + "3.0"]}, + {{versions, app2}, ["0.0.1", + "0.1", + "1.0", + "3.0"]}, + {{versions, app3}, ["0.1.0", + "0.1.3", + "2.0.0", + "3.0.0", + "4.0.0"]}, + {{versions, app4}, ["0.1.0", + "0.3.0", + "5.0.0", + "6.0.0"]}, + {{versions, app5}, ["0.1.0", + "0.3.0", + "2.0.0", + "6.0.0"]}], []), + + Res = sin_dep_resolver:new(sin_test_resolver, + sin_config:create_matcher([], Config), + sin_state:new()), + State = new(Res), + ?assertMatch({_, [{app1,"3.0"}, + {app2,"3.0"}, + {app4,"6.0.0"}, + {app3,"0.1.3"}, + {app5,"6.0.0"}]}, deps(State, app1, "3.0")), + + ?assertMatch({_, [{app1,"3.0"}, + {app3,"0.1.3"}, + {app2,"3.0"}, + {app4,"6.0.0"}, + {app5,"6.0.0"}]}, all_deps(State, [app1, app2, app5], [])). + + +conflicting_passing_test() -> + App1Deps = [{app2, "0.1.0", gte}, + {app5, "2.0.0"}, + {app4, "0.3.0", "5.0.0", between}, + {app3, "0.1.1", "0.1.5", between}], + + App2Deps = [{app4, "3.0.0", gte}], + App3Deps = [{app5, "2.0.0", gte}], + + Config = sin_config:new_from_terms([{{dependencies, app1, "0.1.0"}, App1Deps}, + {{dependencies, app1, "0.1.0"}, App1Deps}, + {{dependencies, app1, "0.2"}, App1Deps}, + {{dependencies, app1, "3.0"}, App1Deps}, + {{dependencies, app2, "0.0.0.1"}, App2Deps}, + {{dependencies, app2, "0.1"}, App2Deps}, + {{dependencies, app2, "1.0"}, App2Deps}, + {{dependencies, app2, "3.0"}, App2Deps}, + {{dependencies, app3, "0.1.0"}, App3Deps}, + {{dependencies, app3, "0.1.3"}, App3Deps}, + {{dependencies, app3, "2.0.0"}, App3Deps}, + {{dependencies, app3, "3.0.0"}, App3Deps}, + {{dependencies, app3, "4.0.0"}, App3Deps}, + {{dependencies, app4, "0.1.0"}, [{app5, "0.1.0"}]}, + {{dependencies, app4, "0.3.0"}, [{app5, "0.3.0"}]}, + {{dependencies, app4, "5.0.0"}, [{app5, "2.0.0"}]}, + {{dependencies, app4, "6.0.0"}, [{app5, "6.0.0"}]}, + {{dependencies, app5, "0.1.0"}, []}, + {{dependencies, app5, "0.3.0"}, []}, + {{dependencies, app5, "2.0.0"}, []}, + {{dependencies, app5, "6.0.0"}, []}, + {{versions, app1}, ["0.1.1", + "0.2", + "3.0"]}, + {{versions, app2}, ["0.0.1", + "0.1", + "1.0", + "3.0"]}, + {{versions, app3}, ["0.1.0", + "0.1.3", + "2.0.0", + "3.0.0", + "4.0.0"]}, + {{versions, app4}, ["0.1.0", + "0.3.0", + "5.0.0", + "6.0.0"]}, + {{versions, app5}, ["0.1.0", + "0.3.0", + "2.0.0", + "6.0.0"]}], []), + + Res = sin_dep_resolver:new(sin_test_resolver, + sin_config:create_matcher([], Config), + sin_state:new()), + State = new(Res), + ?assertMatch({_, [{app1,"3.0"}, + {app2,"3.0"}, + {app4,"5.0.0"}, + {app3,"0.1.3"}, + {app5,"2.0.0"}]}, deps(State, app1, "3.0")), + + ?assertMatch({_, [{app1,"3.0"}, + {app3,"0.1.3"}, + {app2,"3.0"}, + {app4,"5.0.0"}, + {app5,"2.0.0"}]}, all_deps(State, [app1, app2, app5], [])). + + + +circular_dependencies_test() -> + Config = sin_config:new_from_terms([{{dependencies, app1, "0.1.0"}, + [app2]}, + {{dependencies, app2, "0.0.1"}, + [app1]}, + {{versions, app1}, ["0.1.0"]}, + {{versions, app2}, ["0.0.1"]}], []), + + + Res = sin_dep_resolver:new(sin_test_resolver, + sin_config:create_matcher([], Config), + sin_state:new()), + State = new(Res), + ?assertThrow({circular_dependency, + {dependency_path, [{app1,"0.1.0"}, + {app2,"0.0.1"}, + {app1,"0.1.0"}]}}, + deps(State, app1, "0.1.0")). + +conflicting_failing_test() -> + App1Deps = [app2, + {app5, "2.0.0"}, + {app4, "0.3.0", "5.0.0", between}], + + App2Deps = [{app4, "5.0.0", gte}], + App3Deps = [{app5, "6.0.0"}], + + Config = sin_config:new_from_terms([{{dependencies, app1, "0.1.1"}, App1Deps}, + {{dependencies, app1, "0.2"}, App1Deps}, + {{dependencies, app1, "3.0"}, App1Deps}, + {{dependencies, app2, "0.0.1"}, App2Deps}, + {{dependencies, app2, "0.1"}, App2Deps}, + {{dependencies, app2, "1.0"}, App2Deps}, + {{dependencies, app2, "3.0"}, App2Deps}, + {{dependencies, app3, "0.1.0"}, App3Deps}, + {{dependencies, app3, "0.1.3"}, App3Deps}, + {{dependencies, app3, "2.0.0"}, App3Deps}, + {{dependencies, app3, "3.0.0"}, App3Deps}, + {{dependencies, app3, "4.0.0"}, App3Deps}, + {{dependencies, app4, "0.1.0"}, [{app5, "0.1.0"}]}, + {{dependencies, app4, "0.3.0"}, [{app5, "0.3.0"}]}, + {{dependencies, app4, "5.0.0"}, [{app5, "2.0.0"}]}, + {{dependencies, app4, "6.0.0"}, [{app5, "6.0.0"}]}, + {{dependencies, app5, "0.1.0"}, []}, + {{dependencies, app5, "0.3.0"}, []}, + {{dependencies, app5, "2.0.0"}, []}, + {{dependencies, app5, "6.0.0"}, []}, + {{versions, app1}, ["0.1.1", + "0.2", + "3.0"]}, + {{versions, app2}, ["0.0.1", + "0.1", + "1.0", + "3.0"]}, + {{versions, app3}, ["0.1.0", + "0.1.3", + "2.0.0", + "3.0.0", + "4.0.0"]}, + {{versions, app4}, ["0.1.0", + "0.3.0", + "5.0.0", + "6.0.0"]}, + {{versions, app5}, ["0.1.0", + "0.3.0", + "2.0.0", + "6.0.0"]}], []), + + Res = sin_dep_resolver:new(sin_test_resolver, + sin_config:create_matcher([], Config), sin_state:new()), + State = new(Res), + + ?assertMatch({failed,{possible_culprit,[app1]}}, + all_deps(State, [app1, app3], [])). + + +conflicting_exclude_test() -> + App1Deps = [app2, + {app3, "2.0.0"}, + {app4, "0.3.0", "5.0.0", between}], + + App2Deps = [{app4, "5.0.0", gte}], + App3Deps = [{app5, "6.0.0"}], + + Config = sin_config:new_from_terms([{{dependencies, app1, "0.1.1"}, App1Deps}, + {{dependencies, app1, "0.2"}, App1Deps}, + {{dependencies, app1, "3.0"}, App1Deps}, + {{dependencies, app2, "0.0.1"}, App2Deps}, + {{dependencies, app2, "0.1"}, App2Deps}, + {{dependencies, app2, "1.0"}, App2Deps}, + {{dependencies, app2, "3.0"}, App2Deps}, + {{dependencies, app3, "0.1.0"}, App3Deps}, + {{dependencies, app3, "0.1.3"}, App3Deps}, + {{dependencies, app3, "2.0.0"}, App3Deps}, + {{dependencies, app3, "3.0.0"}, App3Deps}, + {{dependencies, app3, "4.0.0"}, App3Deps}, + {{dependencies, app4, "0.1.0"}, [{app5, "0.1.0"}]}, + {{dependencies, app4, "0.3.0"}, [{app5, "0.3.0"}]}, + {{dependencies, app4, "5.0.0"}, [{app5, "2.0.0"}]}, + {{dependencies, app4, "6.0.0"}, [{app5, "6.0.0"}]}, + {{dependencies, app5, "0.1.0"}, []}, + {{dependencies, app5, "0.3.0"}, []}, + {{dependencies, app5, "2.0.0"}, []}, + {{dependencies, app5, "6.0.0"}, []}, + {{versions, app1}, ["0.1.1", + "0.2", + "3.0"]}, + {{versions, app2}, ["0.0.1", + "0.1", + "1.0", + "3.0"]}, + {{versions, app3}, ["0.1.0", + "0.1.3", + "2.0.0", + "3.0.0", + "4.0.0"]}, + {{versions, app4}, ["0.1.0", + "0.3.0", + "5.0.0", + "6.0.0"]}, + {{versions, app5}, ["0.1.0", + "0.3.0", + "2.0.0", + "6.0.0"]}], []), + + Res = sin_dep_resolver:new(sin_test_resolver, + sin_config:create_matcher([], Config), sin_state:new()), + State = new(Res), + + ?assertMatch({_, [{excluded,app3}, + {app1,"3.0"}, + {app2,"3.0"}, + {app4,"5.0.0"}, + {app5,"2.0.0"}]}, + all_deps(State, [app1], [{exclude, app3}])). + + +-endif. diff --git a/src/sin_fs_resolver.erl b/src/sin_fs_resolver.erl new file mode 100644 index 0000000..f41c4d5 --- /dev/null +++ b/src/sin_fs_resolver.erl @@ -0,0 +1,233 @@ +%%%------------------------------------------------------------------- +%%% @author Eric Merritt <[email protected]> +%%% @copyright (C) 2011, Erlware, LLC +%%% @doc +%%% A resolver for resolving applications in the file system +%%% @end +%%% Created : 17 Sep 2011 by Eric Merritt <> +%%%------------------------------------------------------------------- +-module(sin_fs_resolver). + +-behaviour(sin_dep_resolver). + +-export([new/2, app_dependencies/3, app_versions/2, resolve/3]). + +-include("internal.hrl"). + +%%============================================================================ +%% Api +%%============================================================================ +-spec new(sin_config:config(), sin_state:state()) -> sin_dep_resolver:impl(). +new(Config, State) -> + BuildDir = sin_state:get_value(build_dir, State), + AppBDir = filename:join([BuildDir, "apps"]), + ErlLib = case os:getenv("ERL_LIBS") of + false -> + []; + Libs -> + Libs + end, + {[AppBDir, code:lib_dir() | + get_erl_lib_path(ErlLib) ++ + Config:match(dep_dirs, [])], State, + sin_state:get_value(project_applist, State)}. + +-spec app_dependencies(sin_dep_resolver:state(), + sin_dep_solver:app(), + sin_dep_solver:version()) -> + {sin_dep_resolver:state(), + [sin_dep_solver:spec()]}. +app_dependencies(RState={PathList, State, ProjectApps}, App, Ver) -> + case lists:member(App, ProjectApps) of + true -> + {Deps, VersionedDeps} = get_app_constraints(State, + sin_state:get_value({apps, App, dotapp}, State)), + {RState, Deps ++ VersionedDeps}; + false -> + case look_for_dependency_path(State, PathList, App, Ver) of + {ok, Path} -> + {RState, get_dependency_information(State, Path, App)}; + not_found -> + ?SIN_RAISE(State, {unable_to_find_dependencies, App, Ver}) + end + end. + +-spec app_versions(sin_dep_resolver:impl(), sin_dep_solver:app()) -> + {sin_dep_resolver:impl(), + [sin_dep_solver:versions()]}. +app_versions(RState={PathList, State, ProjectApps}, App) -> + case lists:member(App, ProjectApps) of + true -> + {RState, [get_app_vsn(State, sin_state:get_value({apps, App, dotapp}, State))]}; + false -> + VsnList = get_available_versions(State, PathList, App), + %% Make sure the elements are unique + {RState, sets:to_list(sets:from_list(VsnList))} + end. + +%% @doc resolve the on disc location of the dependency +-spec resolve(sin_dep_resolver:impl(), sin_dep_solver:app(), sin_dep_solver:version()) -> + {sin_dep_resolver:impl(), string()}. +resolve(RState={PathList, State, ProjectApps}, App, Version) -> + case lists:member(App, ProjectApps) of + true -> + BuildDir = sin_state:get_value(build_dir, State), + Path = + filename:join([BuildDir, "apps", + erlang:atom_to_list(App) ++ "-" ++Version]), + {RState, Path}; + false -> + case look_for_dependency_path(State, PathList, App, Version) of + {ok, Path} -> + {RState, Path}; + not_found -> + ?SIN_RAISE(State, {unable_to_find_dependencies, App, Version}) + end + end. +%%============================================================================ +%% Internal Functions +%%============================================================================ +-spec get_erl_lib_path(string()) -> [string()]. +get_erl_lib_path(Paths) -> + Elements = re:split(Paths, get_path_sep()), + lists:map(fun(El) -> + erlang:binary_to_list(El) + end, Elements). + +-spec get_path_sep() -> char(). +get_path_sep() -> + Test = filename:join("o", "o"), + {Sep, got} = lists:foldl(fun($o, {_, no}) -> + {ignored, yes}; + (Sep, {_, yes}) -> + {Sep, got}; + (_, Acc) -> + Acc + end, {[], no}, Test), + [Sep]. + + +-spec look_for_dependency_path(sin_state:impl(), [string()], + sin_dep_solver:app(), sin_dep_solver:version()) -> + {ok, string()} | not_found. +look_for_dependency_path(State, PathList, App, Ver) -> + case ec_lists:search(fun(Path) -> + Name = erlang:atom_to_list(App) ++ "-" ++ Ver, + FullPath = filename:join(Path, Name), + case sin_utils:file_exists(State, FullPath) of + true -> + {ok, FullPath}; + false -> + not_found + end + end, PathList) of + {ok, FullPath, _} -> + {ok, FullPath}; + _ -> + not_found + end. + +%% @doc We do this by plugging looking in three places. The deps.config, if +%% it exists, then in the versioned_dependencies element in the app +%% metadata, and finally in the simple list of dependencies in +%% applications element of the app metadata. +-spec get_dependency_information(sin_state:state(), [string()], sin_dep_solver:app()) -> + [sin_dep_solver:spec()]. +get_dependency_information(State, FullPath, AppName) -> + {Deps, VersionedDeps} = get_app_constraints(State, FullPath, AppName), + Deps ++ VersionedDeps ++ get_dep_config_deps(State, FullPath). + +-spec get_dep_config_deps(sin_state:state(), string()) -> + [sin_dep_solver:spec()]. +get_dep_config_deps(State, FullPath) -> + DepConfig = filename:join(FullPath, "deps.config"), + + case file:consult(DepConfig) of + {ok, Terms} -> + Terms; + {error, enoent} -> + []; + Error -> + ?SIN_RAISE(State, {error_opening_file, FullPath, Error}) + end. + +-spec get_app_constraints(sin_state:state(), string(), sin_dep_solver:app()) -> + [sin_dep_solver:spec()]. +get_app_constraints(State, FullPath, AppName) -> + AppConfig = filename:join([FullPath, "ebin", + erlang:atom_to_list(AppName) ++ ".app"]), + get_app_constraints(State, AppConfig). + +-spec get_app_constraints(sin_state:state(), string()) -> + [sin_dep_solver:spec()]. +get_app_constraints(State, AppConfig) -> + case file:consult(AppConfig) of + {ok, [{application, _AppName, Terms}]} -> + DirectDeps = + case lists:keyfind(applications, 1, Terms) of + {applications, Deps0} -> + Deps0; + _ -> + [] + end, + Included = + case lists:keyfind(included_applications, 1, Terms) of + {included_applications, Deps1} -> + Deps1; + _ -> + [] + end, + VersionedDeps = + case lists:keyfind(dep_constraints, 1, Terms) of + {dep_constraints, Deps2} -> + Deps2; + _ -> + [] + end, + {DirectDeps ++ Included, VersionedDeps}; + {error, enoent} -> + ?SIN_RAISE(State, {no_app_config, AppConfig}); + {error, SomeOtherError} -> + ?SIN_RAISE(State, {error_opening_file, AppConfig, SomeOtherError}) + end. + +-spec get_available_versions(sin_state:state(), [string()], sin_dep_solver:app()) -> + [sin_dep_solver:version()]. +get_available_versions(State, PathList, App) -> + lists:foldl(fun(Path, Acc0) -> + case file:list_dir(Path) of + {ok, Dirs} -> + lists:foldl(fun(Dir, Acc1) -> + case re:split(Dir, "^" ++ + erlang:atom_to_list(App) ++ + "-(\\S+)$") of + [<<>>, Version, <<>>] -> + [erlang:binary_to_list(Version) | Acc1]; + _ -> + Acc1 + end + end, Acc0, Dirs); + {error, enoent} -> + Acc0; + Error -> + ?SIN_RAISE(State, + {unable_to_access_directory, Error, Path}) + end + end, [], PathList). + +-spec get_app_vsn(sin_state:state(), [string()]) -> + string(). +get_app_vsn(State, AppConfigPath) -> + case file:consult(AppConfigPath) of + {ok, [{application, _AppName, Terms}]} -> + case lists:keyfind(vsn, 1, Terms) of + {vsn, Vsn} -> + Vsn; + _ -> + ?SIN_RAISE(State, {app_config_contains_no_version, AppConfigPath}) + end; + {error, enoent} -> + ?SIN_RAISE(State, {no_app_config, AppConfigPath}); + {error, SomeOtherError} -> + ?SIN_RAISE(State, {error_opening_file, AppConfigPath, SomeOtherError}) + end. diff --git a/src/sin_resolver.erl b/src/sin_resolver.erl deleted file mode 100644 index d3e0017..0000000 --- a/src/sin_resolver.erl +++ /dev/null @@ -1,138 +0,0 @@ -%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- -%%%------------------------------------------------------------------- -%%% @doc -%%% Resolves individual items for the dependency engine. -%%% @copyright 2007-2011 Erlware -%%% @end -%%%------------------------------------------------------------------- --module(sin_resolver). - --include_lib("eunit/include/eunit.hrl"). --include("internal.hrl"). - -%% API --export([package_versions/3, - package_dependencies/4, - find_package_location/3, - format_exception/1]). - -%%==================================================================== -%% API -%%==================================================================== - -%% @doc Get the list of versions available for the specified package. --spec package_versions(sin_config:config(), - string(), atom()) -> [Vsn::string()]. -package_versions(State, LibDir, Package) when is_atom(Package) -> - lists:sort(fun ewr_util:is_version_greater/2, - get_package_versions(State, Package, - LibDir)). - - -%% @doc Get the list of dependencies for the specified package and the specified -%% version. --spec package_dependencies(sin_config:config(), - LibDir::string(), Package::atom(), - Version::string()) -> Deps::term(). -package_dependencies(State, LibDir, Package, Version) -> - NPackage = atom_to_list(Package), - NDeps = get_package_dependencies(State, - NPackage, - Version, - LibDir), - NDeps. - - -%% @doc Get the dependencies for a package and version. --spec find_package_location(atom(), string(), string()) -> string(). -find_package_location(LibDir, Package, Version) when is_atom(Package) -> - find_package_location(LibDir, atom_to_list(Package), Version); -find_package_location(LibDir, Package, Version) -> - filename:join([LibDir, Package ++ "-" ++ Version]). - -%% @doc Format an exception thrown by this module --spec format_exception(sin_exceptions:exception()) -> - string(). -format_exception(Exception) -> - sin_exceptions:format_exception(Exception). - -%%==================================================================== -%%% Internal functions -%%==================================================================== - -%% @doc Get the version from an app-version --spec get_version(sin_config:config(), string()) -> Vsn::string(). -get_version(_State, [$- | Rest]) -> - Rest; -get_version(State, [_ | Rest]) -> - get_version(State, Rest); -get_version(State, []) -> - ?SIN_RAISE(State, unable_to_parse, ["Unable to find package version"]). - -%% @doc Get all the versions for a package, search across all relavent -%% major/minor versions. --spec get_package_versions(sin_config:config(), - atom(), string()) -> [Version::string()]. -get_package_versions(State, Package, LibDir) -> - - AppVersions = lists:filter(fun(X) -> - filelib:is_dir(X) end, - filelib:wildcard(filename:join(LibDir, - Package) ++ - "-*")), - lists:map(fun(X) -> - get_version(State, filename:basename(X)) - end, - AppVersions). - -%% @doc Get the dependencies for a package and version. --spec get_package_dependencies(sin_config:config(), - atom(), string(), string()) -> - {[atom()], [atom()]}. -get_package_dependencies(State, Package, Version, LibDir) -> - DotAppName = lists:flatten([Package, ".app"]), - AppName = lists:flatten([Package, "-", Version]), - Location = filename:join([LibDir,AppName, - "ebin", DotAppName]), - case file:consult(Location) of - {ok, [Term]} -> - handle_parse_output(State, Term); - {error, _} -> - ?SIN_RAISE(State, {invalid_app_file, Location}, - "Invalid application: ~s", [Location]) - end. - -%% @doc get the version the deps and the versioned deps from an *.app term. --spec handle_parse_output(sin_config:config(), AppInfo::term()) -> - {Vsn::string(), VersionedDeps::list(), Deps::list()}. -handle_parse_output(_, {application, _, Ops}) -> - {get_deps(Ops), get_ideps(Ops)}; -handle_parse_output(State, _) -> - ?SIN_RAISE(State, invalid_app_data, "Invalid dependency info"). - -%% @doc Get the list of non-versioned dependencies from the oplist. This is -%% specifed in the applications entry. --spec get_deps([{atom(), term()}]) -> [atom()]. -get_deps([{applications, List} | _T]) -> - List; -get_deps([_ | T]) -> - get_deps(T); -get_deps([]) -> - []. - -%% @doc Get the list of included applications. --spec get_ideps(OpList::[{atom(), term()}]) -> term(). -get_ideps([{included_applications, List} | _T]) -> - List; -get_ideps([_ | T]) -> - get_ideps(T); -get_ideps([]) -> - []. - -%%==================================================================== -%% tests -%%==================================================================== -get_version_test() -> - ?assertMatch("1.0", get_version(sin_config:new(), "sinan-1.0")), - ?assertMatch("1.3.2.2", get_version(sin_config:new(), "bah-1.3.2.2")). - diff --git a/src/sin_test_resolver.erl b/src/sin_test_resolver.erl new file mode 100644 index 0000000..ada55c8 --- /dev/null +++ b/src/sin_test_resolver.erl @@ -0,0 +1,26 @@ +%%%------------------------------------------------------------------- +%%% @author Eric Merritt <[email protected]> +%%% @copyright (C) 2011, Erlware, LLC +%%% @doc +%%% A resolver used only for testing +%%% @end +%%% Created : 17 Sep 2011 by Eric Merritt <> +%%%------------------------------------------------------------------- +-module(sin_test_resolver). + +-behaviour(sin_dep_resolver). + +-export([new/2, app_dependencies/3, app_versions/2, resolve/3]). + +new(Config, _State) -> + Config. + +app_dependencies(Config, App, Ver) -> + {Config,Config:match({dependencies, App, Ver})}. + +app_versions(Config, App) -> + {Config, Config:match({versions, App})}. + +%% Not used for testing +resolve(Config, _App, _Version) -> + {Config, ""}. -- 1.7.6.4 -- You received this message because you are subscribed to the Google Groups "erlware-dev" group. To post to this group, send email to [email protected]. To unsubscribe from this group, send email to [email protected]. For more options, visit this group at http://groups.google.com/group/erlware-dev?hl=en.
