diff options
-rw-r--r-- | src/rebar_app_discover.erl | 9 | ||||
-rw-r--r-- | src/rebar_config.erl | 47 | ||||
-rw-r--r-- | src/rebar_digraph.erl | 10 | ||||
-rw-r--r-- | src/rebar_packages.erl | 55 | ||||
-rw-r--r-- | src/rebar_prv_install_deps.erl | 60 | ||||
-rw-r--r-- | src/rebar_prv_packages.erl | 33 | ||||
-rw-r--r-- | src/rebar_prv_update.erl | 48 | ||||
-rw-r--r-- | src/rebar_state.erl | 86 | ||||
-rw-r--r-- | src/rebar_utils.erl | 66 | ||||
-rw-r--r-- | test/mock_pkg_resource.erl | 17 | ||||
-rw-r--r-- | test/rebar_compile_SUITE.erl | 30 |
11 files changed, 287 insertions, 174 deletions
diff --git a/src/rebar_app_discover.erl b/src/rebar_app_discover.erl index 0799313..fe930bd 100644 --- a/src/rebar_app_discover.erl +++ b/src/rebar_app_discover.erl @@ -48,9 +48,9 @@ merge_deps(AppInfo, State) -> State1 = lists:foldl(fun(Profile, StateAcc) -> AppProfDeps = rebar_state:get(AppState, {deps, Profile}, []), TopLevelProfDeps = rebar_state:get(StateAcc, {deps, Profile}, []), - ProfDeps2 = dedup(lists:keymerge(1, - lists:keysort(1, TopLevelProfDeps), - lists:keysort(1, AppProfDeps))), + ProfDeps2 = dedup(rebar_utils:tup_umerge( + rebar_utils:tup_sort(TopLevelProfDeps) + ,rebar_utils:tup_sort(AppProfDeps))), rebar_state:set(StateAcc, {deps, Profile}, ProfDeps2) end, State, lists:reverse(CurrentProfiles)), @@ -164,7 +164,8 @@ create_app_info(AppDir, AppFile) -> AppInfo1 = rebar_app_info:applications( rebar_app_info:app_details(AppInfo, AppDetails), IncludedApplications++Applications), - rebar_app_info:dir(AppInfo1, AppDir); + Valid = rebar_app_utils:validate_application_info(AppInfo1), + rebar_app_info:dir(rebar_app_info:valid(AppInfo1, Valid), AppDir); _ -> error end. diff --git a/src/rebar_config.erl b/src/rebar_config.erl index c1c4381..76e03ea 100644 --- a/src/rebar_config.erl +++ b/src/rebar_config.erl @@ -28,10 +28,12 @@ -export([consult/1 ,consult_file/1 + ,format_error/1 ,merge_locks/2]). -include("rebar.hrl"). +-include_lib("providers/include/providers.hrl"). %% =================================================================== %% Public API @@ -77,6 +79,9 @@ merge_locks(Config, [Locks]) -> NewDeps = find_newly_added(ConfigDeps, Locks), [{{locks, default}, Locks}, {{deps, default}, NewDeps++Deps} | Config]. +format_error({bad_dep_name, Dep}) -> + io_lib:format("Dependency name must be an atom, instead found: ~p", [Dep]). + %% =================================================================== %% Internal functions %% =================================================================== @@ -106,21 +111,27 @@ bs(Vars) -> %% Find deps that have been added to the config after the lock was created find_newly_added(ConfigDeps, LockedDeps) -> - [Dep || Dep <- ConfigDeps, - begin - NewDep = ec_cnv:to_binary(element(1, Dep)), - case lists:keyfind(NewDep, 1, LockedDeps) of - false -> - true; - Match -> - case element(3, Match) of - 0 -> - true; - _ -> - ?WARN("Newly added dep ~s is locked at a lower level. " - "If you really want to unlock it, use 'rebar3 upgrade ~s'", - [NewDep, NewDep]), - false - end - end - end]. + rebar_utils:filtermap(fun(Dep) when is_tuple(Dep) -> + check_newly_added(element(1, Dep), LockedDeps); + (Dep) -> + check_newly_added(Dep, LockedDeps) + end, ConfigDeps). + +check_newly_added(Dep, LockedDeps) when is_atom(Dep) -> + NewDep = ec_cnv:to_binary(Dep), + case lists:keyfind(NewDep, 1, LockedDeps) of + false -> + true; + Match -> + case element(3, Match) of + 0 -> + true; + _ -> + ?WARN("Newly added dep ~s is locked at a lower level. " + "If you really want to unlock it, use 'rebar3 upgrade ~s'", + [NewDep, NewDep]), + false + end + end; +check_newly_added(Dep, _) -> + throw(?PRV_ERROR({bad_dep_name, Dep})). diff --git a/src/rebar_digraph.erl b/src/rebar_digraph.erl index 129ea35..7c1fe90 100644 --- a/src/rebar_digraph.erl +++ b/src/rebar_digraph.erl @@ -72,10 +72,12 @@ cull_deps(Graph, Vertices) -> cull_deps(Graph, Vertices, 1, - lists:foldl(fun({Key, _}, Levels) -> dict:store(Key, 0, Levels) end, - dict:new(), Vertices), - lists:foldl(fun({Key, _}=N, Solution) -> dict:store(Key, N, Solution) end, - dict:new(), Vertices), + lists:foldl(fun({Key, _}, Levels) -> + dict:store(Key, 0, Levels) + end, dict:new(), Vertices), + lists:foldl(fun({Key, _}=N, Solution) -> + dict:store(Key, N, Solution) + end, dict:new(), Vertices), []). cull_deps(_Graph, [], _Level, Levels, Solution, Discarded) -> diff --git a/src/rebar_packages.erl b/src/rebar_packages.erl index 5c67600..8982573 100644 --- a/src/rebar_packages.erl +++ b/src/rebar_packages.erl @@ -1,6 +1,8 @@ -module(rebar_packages). --export([get_packages/1]). +-export([get_packages/1 + ,registry/1 + ,find_highest_matching/3]). -export_type([package/0]). @@ -37,3 +39,54 @@ get_packages(State) -> ?ERROR("Bad packages index, try to fix with `rebar3 update`", []), {dict:new(), digraph:new()} end. + +registry(State) -> + Dir = rebar_dir:global_cache_dir(State), + RegistryDir = filename:join(Dir, "packages"), + HexFile = filename:join(RegistryDir, "registry"), + case ets:file2tab(HexFile) of + {ok, T} -> + {ok, T}; + {error, Reason} -> + ?DEBUG("Error loading registry: ~p", [Reason]), + error + end. + + +%% Hex supports use of ~> to specify the version required for a dependency. +%% Since rebar3 requires exact versions to choose from we find the highest +%% available version of the dep that passes the constraint. + +%% `~>` will never include pre-release versions of its upper bound. +%% It can also be used to set an upper bound on only the major +%% version part. See the table below for `~>` requirements and +%% their corresponding translation. +%% `~>` | Translation +%% :------------- | :--------------------- +%% `~> 2.0.0` | `>= 2.0.0 and < 2.1.0` +%% `~> 2.1.2` | `>= 2.1.2 and < 2.2.0` +%% `~> 2.1.3-dev` | `>= 2.1.3-dev and < 2.2.0` +%% `~> 2.0` | `>= 2.0.0 and < 3.0.0` +%% `~> 2.1` | `>= 2.1.0 and < 3.0.0` +find_highest_matching(Dep, Constraint, T) -> + case ets:lookup(T, Dep) of + [{Dep, [[Vsn]]}] -> + case ec_semver:pes(Vsn, Constraint) of + true -> + Vsn; + false -> + ?WARN("Only existing version of ~s is ~s which does not match constraint ~~> ~s. " + "Using anyway, but it is not guarenteed to work.", [Dep, Vsn, Constraint]), + Vsn + end; + [{Dep, [[HeadVsn | VsnTail]]}] -> + lists:foldl(fun(Version, Highest) -> + case ec_semver:pes(Version, Constraint) andalso + ec_semver:gt(Version, Highest) of + true -> + Version; + false -> + Highest + end + end, HeadVsn, VsnTail) + end. diff --git a/src/rebar_prv_install_deps.erl b/src/rebar_prv_install_deps.erl index db2b036..c535b4d 100644 --- a/src/rebar_prv_install_deps.erl +++ b/src/rebar_prv_install_deps.erl @@ -97,6 +97,8 @@ do(State) -> end. -spec format_error(any()) -> iolist(). +format_error({load_registry_fail, Dep}) -> + io_lib:format("Error loading registry to resolve version of ~s. Try fixing by running 'rebar3 update'", [Dep]); format_error({bad_constraint, Name, Constraint}) -> io_lib:format("Unable to parse version for package ~s: ~s", [Name, Constraint]); format_error({parse_dep, Dep}) -> @@ -129,9 +131,11 @@ handle_deps(Profile, State, Deps, Locks) when is_list(Locks) -> -> {ok, [rebar_app_info:t()], rebar_state:t()} | {error, string()}. handle_deps(_Profile, State, [], _, _) -> {ok, [], State}; -handle_deps(Profile, State, Deps, Upgrade, Locks) -> +handle_deps(Profile, State0, Deps, Upgrade, Locks) -> %% Read in package index and dep graph - {Packages, Graph} = rebar_packages:get_packages(State), + {Packages, Graph} = rebar_state:packages(State0), + Registry = rebar_packages:registry(State0), + State = rebar_state:packages(rebar_state:registry(State0, Registry), {Packages, Graph}), %% Split source deps from pkg deps, needed to keep backwards compatibility DepsDir = rebar_dir:deps_dir(State), {SrcDeps, PkgDeps} = parse_deps(DepsDir, Deps, State, Locks, 0), @@ -210,8 +214,8 @@ handle_pkg_dep(Profile, Pkg, Packages, Upgrade, DepsDir, Fetched, Seen, State) - AppInfo = package_to_app(DepsDir, Packages, Pkg), Level = rebar_app_info:dep_level(AppInfo), {NewSeen, NewState} = maybe_lock(Profile, AppInfo, Seen, State, Level), - maybe_fetch(AppInfo, Upgrade, Seen, NewState), - {[AppInfo | Fetched], NewSeen, NewState}. + {_, AppInfo1} = maybe_fetch(AppInfo, Upgrade, Seen, NewState), + {[AppInfo1 | Fetched], NewSeen, NewState}. maybe_lock(Profile, AppInfo, Seen, State, Level) -> case rebar_app_info:is_checkout(AppInfo) of @@ -312,8 +316,8 @@ update_unseen_src_dep(AppInfo, Profile, Level, SrcDeps, PkgDeps, SrcApps, State, handle_upgrade(AppInfo, SrcDeps, PkgDeps, SrcApps, Level, State1, Locks); _ -> - maybe_fetch(AppInfo, false, Seen, State1), - handle_dep(AppInfo, SrcDeps, PkgDeps, SrcApps, + {_, AppInfo1} = maybe_fetch(AppInfo, false, Seen, State1), + handle_dep(AppInfo1, SrcDeps, PkgDeps, SrcApps, Level, State1, Locks) end, {NewSrcDeps, NewPkgDeps, NewSrcApps, State2, NewSeen, NewLocks}. @@ -323,12 +327,12 @@ handle_upgrade(AppInfo, SrcDeps, PkgDeps, SrcApps, Level, State, Locks) -> case lists:keyfind(Name, 1, Locks) of false -> case maybe_fetch(AppInfo, true, sets:new(), State) of - true -> - handle_dep(AppInfo, SrcDeps, PkgDeps, SrcApps, + {true, AppInfo1} -> + handle_dep(AppInfo1, SrcDeps, PkgDeps, SrcApps, Level, State, Locks); - false -> - {[AppInfo|SrcDeps], PkgDeps, SrcApps, State, Locks} + {false, AppInfo1} -> + {[AppInfo1|SrcDeps], PkgDeps, SrcApps, State, Locks} end; _StillLocked -> {[AppInfo|SrcDeps], PkgDeps, SrcApps, State, Locks} @@ -371,31 +375,37 @@ handle_dep(State, DepsDir, AppInfo, Locks, Level) -> {AppInfo2, SrcDeps, PkgDeps, Locks++NewLocks, State1}. -spec maybe_fetch(rebar_app_info:t(), boolean() | {true, binary(), integer()}, - sets:set(binary()), rebar_state:t()) -> boolean(). + sets:set(binary()), rebar_state:t()) -> {boolean(), rebar_app_info:t()}. maybe_fetch(AppInfo, Upgrade, Seen, State) -> AppDir = ec_cnv:to_list(rebar_app_info:dir(AppInfo)), %% Don't fetch dep if it exists in the _checkouts dir case rebar_app_info:is_checkout(AppInfo) of true -> - false; + {false, AppInfo}; false -> case rebar_app_discover:find_app(AppDir, all) of false -> case in_default(AppInfo, State) of false -> - fetch_app(AppInfo, AppDir, State); + {fetch_app(AppInfo, AppDir, State), AppInfo}; {true, FoundApp} -> - ?INFO("Linking ~s to ~s", [rebar_app_info:dir(FoundApp), AppDir]), + %% Preserve the state we created with overrides + AppState = rebar_app_info:state(AppInfo), + FoundApp1 = rebar_app_info:state(FoundApp, AppState), + ?INFO("Linking ~s to ~s", [rebar_app_info:dir(FoundApp1), AppDir]), filelib:ensure_dir(AppDir), - rebar_file_utils:symlink_or_copy(rebar_app_info:dir(FoundApp), AppDir), - true + rebar_file_utils:symlink_or_copy(rebar_app_info:dir(FoundApp1), AppDir), + {true, FoundApp1} end; - {true, _} -> + {true, AppInfo1} -> + %% Preserve the state we created with overrides + AppState = rebar_app_info:state(AppInfo), + AppInfo2 = rebar_app_info:state(AppInfo1, AppState), case sets:is_element(rebar_app_info:name(AppInfo), Seen) of true -> - false; + {false, AppInfo2}; false -> - maybe_upgrade(AppInfo, AppDir, Upgrade, State) + {maybe_upgrade(AppInfo, AppDir, Upgrade, State), AppInfo2} end end end. @@ -439,13 +449,14 @@ parse_dep({Name, Vsn}, {SrcDepsAcc, PkgDepsAcc}, DepsDir, State) when is_list(Vs ,ec_cnv:to_binary(Vsn)) | PkgDepsAcc]} end; parse_dep(Name, {SrcDepsAcc, PkgDepsAcc}, DepsDir, State) when is_atom(Name) -> + {PkgName, PkgVsn} = get_package(ec_cnv:to_binary(Name), State), CheckoutsDir = ec_cnv:to_list(rebar_dir:checkouts_dir(State, Name)), case rebar_app_info:discover(CheckoutsDir) of {ok, _App} -> Dep = new_dep(DepsDir, Name, [], [], State), {[Dep | SrcDepsAcc], PkgDepsAcc}; not_found -> - {SrcDepsAcc, [ec_cnv:to_binary(Name) | PkgDepsAcc]} + {SrcDepsAcc, [{PkgName, PkgVsn} | PkgDepsAcc]} end; parse_dep({Name, Source}, {SrcDepsAcc, PkgDepsAcc}, DepsDir, State) when is_tuple(Source) -> Dep = new_dep(DepsDir, Name, [], Source, State), @@ -561,3 +572,12 @@ warn_skip_pkg({Name, Source}, State) -> not_needs_compile(App) -> not(rebar_app_info:is_checkout(App)) andalso rebar_app_info:valid(App). + +get_package(Dep, State) -> + case rebar_state:registry(State) of + {ok, T} -> + HighestDepVsn = rebar_packages:find_highest_matching(Dep, "0", T), + {Dep, HighestDepVsn}; + error -> + throw(?PRV_ERROR({load_registry_fail, Dep})) + end. diff --git a/src/rebar_prv_packages.erl b/src/rebar_prv_packages.erl index 8ba9e92..c7c0d50 100644 --- a/src/rebar_prv_packages.erl +++ b/src/rebar_prv_packages.erl @@ -7,6 +7,8 @@ format_error/1]). -include("rebar.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). +-include_lib("providers/include/providers.hrl"). -define(PROVIDER, pkgs). -define(DEPS, []). @@ -25,33 +27,26 @@ init(State) -> -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> - {Packages, _Graph} = rebar_packages:get_packages(State), - print_packages(Packages), - {ok, State}. + case rebar_packages:registry(State) of + {ok, Registry} -> + print_packages(Registry), + {ok, State}; + error -> + ?PRV_ERROR(load_registry_fail) + end. -spec format_error(any()) -> iolist(). -format_error(Reason) -> - io_lib:format("~p", [Reason]). +format_error(load_registry_fail) -> + "Failed to load package regsitry. Try running 'rebar3 update' to fix". -print_packages(Packages) -> - Keys = lists:keysort(1, dict:fetch_keys(Packages)), - Pkgs = merge(Keys), +print_packages(Table) -> + MS = ets:fun2ms(fun({Key, [Value]}) when is_binary(Key) -> {Key, Value} end), + Pkgs = ets:select(Table, MS), lists:foreach(fun({Name, Vsns}) -> VsnStr = join(Vsns, <<", ">>), io:format("~s:~n Versions: ~s~n~n", [Name, VsnStr]) end, Pkgs). --spec merge([{binary(), binary()}]) -> [{binary(), [binary()]}]. -merge(List) -> - merge([], List). - -merge(List, []) -> - List; -merge([{Key, Values} | T], [{Key, Value} | Rest]) -> - merge([{Key, [Value | Values]} | T], Rest); -merge(List, [{Key, Value} | Rest]) -> - merge([{Key, [Value]} | List], Rest). - -spec join([binary()], binary()) -> binary(). join([Bin], _Sep) -> <<Bin/binary>>; diff --git a/src/rebar_prv_update.erl b/src/rebar_prv_update.erl index be5cafd..6cdabeb 100644 --- a/src/rebar_prv_update.erl +++ b/src/rebar_prv_update.erl @@ -35,10 +35,14 @@ init(State) -> do(State) -> ?INFO("Updating package index...", []), try - Url = rebar_state:get(State, rebar_packages_cdn, "https://s3.amazonaws.com/s3.hex.pm/registry.ets.gz"), + Dir = rebar_dir:global_cache_dir(State), + RegistryDir = filename:join(Dir, "packages"), + filelib:ensure_dir(filename:join(RegistryDir, "dummy")), + HexFile = filename:join(RegistryDir, "registry"), TmpDir = ec_file:insecure_mkdtemp(), TmpFile = filename:join(TmpDir, "packages.gz"), - HexFile = filename:join(TmpDir, "packages"), + + Url = rebar_state:get(State, rebar_packages_cdn, "https://s3.amazonaws.com/s3.hex.pm/registry.ets.gz"), {ok, _RequestId} = httpc:request(get, {Url, []}, [], [{stream, TmpFile}, {sync, true}]), {ok, Data} = file:read_file(TmpFile), @@ -90,7 +94,7 @@ update_graph(Pkg, PkgVsn, Deps, HexRegistry, Graph) -> lists:foldl(fun([Dep, DepVsn, _, _], DepsListAcc) -> case DepVsn of <<"~> ", Vsn/binary>> -> - HighestDepVsn = find_highest_matching(Dep, Vsn, HexRegistry), + HighestDepVsn = rebar_packages:find_highest_matching(Dep, Vsn, HexRegistry), digraph:add_edge(Graph, {Pkg, PkgVsn}, {Dep, HighestDepVsn}), [{Dep, DepVsn} | DepsListAcc]; Vsn -> @@ -98,41 +102,3 @@ update_graph(Pkg, PkgVsn, Deps, HexRegistry, Graph) -> [{Dep, Vsn} | DepsListAcc] end end, [], Deps). - -%% Hex supports use of ~> to specify the version required for a dependency. -%% Since rebar3 requires exact versions to choose from we find the highest -%% available version of the dep that passes the constraint. - -%% `~>` will never include pre-release versions of its upper bound. -%% It can also be used to set an upper bound on only the major -%% version part. See the table below for `~>` requirements and -%% their corresponding translation. -%% `~>` | Translation -%% :------------- | :--------------------- -%% `~> 2.0.0` | `>= 2.0.0 and < 2.1.0` -%% `~> 2.1.2` | `>= 2.1.2 and < 2.2.0` -%% `~> 2.1.3-dev` | `>= 2.1.3-dev and < 2.2.0` -%% `~> 2.0` | `>= 2.0.0 and < 3.0.0` -%% `~> 2.1` | `>= 2.1.0 and < 3.0.0` -find_highest_matching(Dep, Constraint, T) -> - case ets:lookup(T, Dep) of - [{Dep, [[Vsn]]}] -> - case ec_semver:pes(Vsn, Constraint) of - true -> - Vsn; - false -> - ?WARN("Only existing version of ~s is ~s which does not match constraint ~~> ~s. " - "Using anyway, but it is not guarenteed to work.", [Dep, Vsn, Constraint]), - Vsn - end; - [{Dep, [[HeadVsn | VsnTail]]}] -> - lists:foldl(fun(Version, Highest) -> - case ec_semver:pes(Version, Constraint) andalso - ec_semver:gt(Version, Highest) of - true -> - Version; - false -> - Highest - end - end, HeadVsn, VsnTail) - end. diff --git a/src/rebar_state.erl b/src/rebar_state.erl index 68c71a9..f922977 100644 --- a/src/rebar_state.erl +++ b/src/rebar_state.erl @@ -30,6 +30,9 @@ overrides/1, overrides/2, apply_overrides/2, + packages/1, packages/2, + registry/1, registry/2, + resources/1, resources/2, add_resource/2, providers/1, providers/2, add_provider/2]). @@ -51,6 +54,8 @@ deps_to_build = [] :: [rebar_app_info:t()], all_deps = [] :: [rebar_app_info:t()], + packages = undefined :: {rebar_dict(), rebar_digraph()} | undefined, + registry = undefined :: {ok, ets:tid()} | error | undefined, overrides = [], resources = [], providers = []}). @@ -242,7 +247,8 @@ merge_opts(NewOpts, OldOpts) -> true -> NewValue; false -> - tup_umerge(tup_sort(NewValue), tup_sort(OldValue)) + rebar_utils:tup_umerge(rebar_utils:tup_sort(NewValue) + ,rebar_utils:tup_sort(OldValue)) end; (_Key, NewValue, _OldValue) -> NewValue @@ -298,6 +304,22 @@ namespace(#state_t{namespace=Namespace}) -> namespace(State=#state_t{}, Namespace) -> State#state_t{namespace=Namespace}. +packages(State=#state_t{packages=undefined}) -> + rebar_packages:get_packages(State); +packages(#state_t{packages=Packages}) -> + Packages. + +packages(State, Packages) -> + State#state_t{packages=Packages}. + +registry(State=#state_t{registry=undefined}) -> + rebar_packages:registry(State); +registry(#state_t{registry=Registry}) -> + Registry. + +registry(State, Registry) -> + State#state_t{registry=Registry}. + -spec resources(t()) -> rebar_resource:resource(). resources(#state_t{resources=Resources}) -> Resources. @@ -340,65 +362,3 @@ create_logic_providers(ProviderModules, State0) -> %% =================================================================== %% Internal functions %% =================================================================== - -%% Sort the list in proplist-order, meaning that `{a,b}' and `{a,c}' -%% both compare as usual, and `a' and `b' do the same, but `a' and `{a,b}' will -%% compare based on the first element of the key, and in order. So the following -%% list will sort as: -%% - `[native, {native,o3}, check]' -> `[check, native, {native, o3}]' -%% - `[native, {native,o3}, {native, o2}, check]' -> `[check,native,{native,o3},{native,o2}]' -%% Meaning that: -%% a) no deduplication takes place -%% b) the key of a tuple is what counts in being sorted, but atoms are seen as {atom} -%% as far as comparison is concerned (departing from lists:ukeysort/2) -%% c) order is preserved for similar keys and tuples no matter their size (sort is stable) -%% -%% These properties let us merge proplists fairly easily. -tup_sort(List) -> - lists:sort(fun(A, B) when is_tuple(A), is_tuple(B) -> element(1, A) =< element(1, B) - ; (A, B) when is_tuple(A) -> element(1, A) =< B - ; (A, B) when is_tuple(B) -> A =< element(1, B) - ; (A, B) -> A =< B - end, List). - -%% Custom merge functions. The objective is to behave like lists:umerge/2, -%% except that we also compare the merge elements based on the key if they're a -%% tuple, such that `{key, val1}' is always prioritized over `{key, val0}' if -%% the former is from the 'new' list. -%% -%% This lets us apply proper overrides to list of elements according to profile -%% priority. This function depends on a stable proplist sort. -tup_umerge([], Olds) -> - Olds; -tup_umerge([New|News], Olds) -> - lists:reverse(umerge(News, Olds, [], New)). - -%% This is equivalent to umerge2_2 in the stdlib, except we use the expanded -%% value/key only to compare -umerge(News, [Old|Olds], Merged, Cmp) when element(1, Cmp) == element(1, Old); - element(1, Cmp) == Old; - Cmp == element(1, Old); - Cmp =< Old -> - umerge(News, Olds, [Cmp | Merged], Cmp, Old); -umerge(News, [Old|Olds], Merged, Cmp) -> - umerge(News, Olds, [Old | Merged], Cmp); -umerge(News, [], Merged, Cmp) -> - lists:reverse(News, [Cmp | Merged]). - -%% Similar to stdlib's umerge2_1 in the stdlib, except that when the expanded -%% value/keys compare equal, we check if the element is a full dupe to clear it -%% (like the stdlib function does) or otherwise keep the duplicate around in -%% an order that prioritizes 'New' elements. -umerge([New|News], Olds, Merged, CmpMerged, Cmp) when CmpMerged == Cmp -> - umerge(News, Olds, Merged, New); -umerge([New|News], Olds, Merged, _CmpMerged, Cmp) when element(1,New) == element(1, Cmp); - element(1,New) == Cmp; - New == element(1, Cmp); - New =< Cmp -> - umerge(News, Olds, [New | Merged], New, Cmp); -umerge([New|News], Olds, Merged, _CmpMerged, Cmp) -> % > - umerge(News, Olds, [Cmp | Merged], New); -umerge([], Olds, Merged, CmpMerged, Cmp) when CmpMerged == Cmp -> - lists:reverse(Olds, Merged); -umerge([], Olds, Merged, _CmpMerged, Cmp) -> - lists:reverse(Olds, [Cmp | Merged]). diff --git a/src/rebar_utils.erl b/src/rebar_utils.erl index d92ab80..bc2e4ac 100644 --- a/src/rebar_utils.erl +++ b/src/rebar_utils.erl @@ -52,7 +52,9 @@ args_to_tasks/1, expand_env_variable/3, get_arch/0, - wordsize/0]). + wordsize/0, + tup_umerge/2, + tup_sort/1]). %% for internal use only -export([otp_release/0]). @@ -225,6 +227,68 @@ erl_opts(Config) -> %% was enclosed in quotes and might have commas but should not be split. args_to_tasks(Args) -> new_task(Args, []). +%% Sort the list in proplist-order, meaning that `{a,b}' and `{a,c}' +%% both compare as usual, and `a' and `b' do the same, but `a' and `{a,b}' will +%% compare based on the first element of the key, and in order. So the following +%% list will sort as: +%% - `[native, {native,o3}, check]' -> `[check, native, {native, o3}]' +%% - `[native, {native,o3}, {native, o2}, check]' -> `[check,native,{native,o3},{native,o2}]' +%% Meaning that: +%% a) no deduplication takes place +%% b) the key of a tuple is what counts in being sorted, but atoms are seen as {atom} +%% as far as comparison is concerned (departing from lists:ukeysort/2) +%% c) order is preserved for similar keys and tuples no matter their size (sort is stable) +%% +%% These properties let us merge proplists fairly easily. +tup_sort(List) -> + lists:sort(fun(A, B) when is_tuple(A), is_tuple(B) -> element(1, A) =< element(1, B) + ; (A, B) when is_tuple(A) -> element(1, A) =< B + ; (A, B) when is_tuple(B) -> A =< element(1, B) + ; (A, B) -> A =< B + end, List). + +%% Custom merge functions. The objective is to behave like lists:umerge/2, +%% except that we also compare the merge elements based on the key if they're a +%% tuple, such that `{key, val1}' is always prioritized over `{key, val0}' if +%% the former is from the 'new' list. +%% +%% This lets us apply proper overrides to list of elements according to profile +%% priority. This function depends on a stable proplist sort. +tup_umerge([], Olds) -> + Olds; +tup_umerge([New|News], Olds) -> + lists:reverse(umerge(News, Olds, [], New)). + +%% This is equivalent to umerge2_2 in the stdlib, except we use the expanded +%% value/key only to compare +umerge(News, [Old|Olds], Merged, Cmp) when element(1, Cmp) == element(1, Old); + element(1, Cmp) == Old; + Cmp == element(1, Old); + Cmp =< Old -> + umerge(News, Olds, [Cmp | Merged], Cmp, Old); +umerge(News, [Old|Olds], Merged, Cmp) -> + umerge(News, Olds, [Old | Merged], Cmp); +umerge(News, [], Merged, Cmp) -> + lists:reverse(News, [Cmp | Merged]). + +%% Similar to stdlib's umerge2_1 in the stdlib, except that when the expanded +%% value/keys compare equal, we check if the element is a full dupe to clear it +%% (like the stdlib function does) or otherwise keep the duplicate around in +%% an order that prioritizes 'New' elements. +umerge([New|News], Olds, Merged, CmpMerged, Cmp) when CmpMerged == Cmp -> + umerge(News, Olds, Merged, New); +umerge([New|News], Olds, Merged, _CmpMerged, Cmp) when element(1,New) == element(1, Cmp); + element(1,New) == Cmp; + New == element(1, Cmp); + New =< Cmp -> + umerge(News, Olds, [New | Merged], New, Cmp); +umerge([New|News], Olds, Merged, _CmpMerged, Cmp) -> % > + umerge(News, Olds, [Cmp | Merged], New); +umerge([], Olds, Merged, CmpMerged, Cmp) when CmpMerged == Cmp -> + lists:reverse(Olds, Merged); +umerge([], Olds, Merged, _CmpMerged, Cmp) -> + lists:reverse(Olds, [Cmp | Merged]). + %% ==================================================================== %% Internal functions %% ==================================================================== diff --git a/test/mock_pkg_resource.erl b/test/mock_pkg_resource.erl index afb6fb4..615e8a5 100644 --- a/test/mock_pkg_resource.erl +++ b/test/mock_pkg_resource.erl @@ -110,6 +110,8 @@ mock_pkg_index(Opts) -> GraphParts = to_graph_parts(Dict), Digraph = rebar_digraph:restore_graph(GraphParts), meck:new(rebar_packages, [passthrough, no_link]), + meck:expect(rebar_packages, registry, + fun(_State) -> {ok, to_registry(Deps)} end), meck:expect(rebar_packages, get_packages, fun(_State) -> {Dict, Digraph} end). @@ -117,10 +119,23 @@ mock_pkg_index(Opts) -> %%%%%%%%%%%%%%% %%% Helpers %%% %%%%%%%%%%%%%%% + +to_registry(Deps) -> + Tid = ets:new(registry, []), + lists:foreach(fun({{Name, Vsn}, _}) -> + case ets:lookup(Tid, Name) of + [{_, [Vsns]}] -> + ets:insert(Tid, {Name, [[Vsn | Vsns]]}); + _ -> + ets:insert(Tid, {Name, [[Vsn]]}) + end + end, Deps), + Tid. + all_files(Dir) -> filelib:wildcard(filename:join([Dir, "**"])). -archive_names(Dir, App, Vsn, Files) -> +archive_names(Dir, _App, _Vsn, Files) -> [{(F -- Dir) -- "/", F} || F <- Files]. find_parts(Apps, Skip) -> find_parts(Apps, Skip, dict:new()). diff --git a/test/rebar_compile_SUITE.erl b/test/rebar_compile_SUITE.erl index bc72913..2fdf36d 100644 --- a/test/rebar_compile_SUITE.erl +++ b/test/rebar_compile_SUITE.erl @@ -18,7 +18,8 @@ deps_in_path/1, delete_beam_if_source_deleted/1, checkout_priority/1, - compile_plugins/1]). + compile_plugins/1, + highest_version_of_pkg_dep/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -45,7 +46,7 @@ all() -> build_all_srcdirs, recompile_when_hrl_changes, recompile_when_opts_change, dont_recompile_when_opts_dont_change, dont_recompile_yrl_or_xrl, delete_beam_if_source_deleted, - deps_in_path, checkout_priority, compile_plugins]. + deps_in_path, checkout_priority, compile_plugins, highest_version_of_pkg_dep]. build_basic_app(Config) -> AppDir = ?config(apps, Config), @@ -420,3 +421,28 @@ compile_plugins(Config) -> Config, RConf, ["compile"], {ok, [{app, Name}, {plugin, PluginName}, {dep, DepName}]} ). + +highest_version_of_pkg_dep(Config) -> + AppDir = ?config(apps, Config), + + Name = rebar_test_utils:create_random_name("app1_"), + Vsn = rebar_test_utils:create_random_vsn(), + rebar_test_utils:create_app(AppDir, Name, Vsn, [kernel, stdlib]), + + PkgName = rebar_test_utils:create_random_name("pkg1_"), + mock_git_resource:mock([]), + mock_pkg_resource:mock([ + {pkgdeps, [{{iolist_to_binary(PkgName), <<"0.1.0">>}, []}, + {{iolist_to_binary(PkgName), <<"0.0.1">>}, []}, + {{iolist_to_binary(PkgName), <<"0.1.3">>}, []}, + {{iolist_to_binary(PkgName), <<"0.1.1">>}, []}]} + ]), + + RConfFile = rebar_test_utils:create_config(AppDir, [{deps, [list_to_atom(PkgName)]}]), + {ok, RConf} = file:consult(RConfFile), + + %% Build with deps. + rebar_test_utils:run_and_check( + Config, RConf, ["compile"], + {ok, [{app, Name}, {dep, PkgName, <<"0.1.3">>}]} + ). |