From d75ba02671236634906989f70eadc785658b4959 Mon Sep 17 00:00:00 2001 From: Tristan Sloughter Date: Fri, 10 Apr 2015 22:31:01 -0500 Subject: support single atoms for pkg deps, fetch highest version available --- src/rebar_app_discover.erl | 9 ++--- src/rebar_config.erl | 40 ++++++++++++---------- src/rebar_digraph.erl | 12 ++++--- src/rebar_packages.erl | 55 +++++++++++++++++++++++++++++- src/rebar_prv_install_deps.erl | 55 ++++++++++++++++++++---------- src/rebar_prv_packages.erl | 33 ++++++++---------- src/rebar_prv_update.erl | 48 ++++---------------------- src/rebar_state.erl | 77 ++++++++---------------------------------- src/rebar_utils.erl | 66 +++++++++++++++++++++++++++++++++++- 9 files changed, 225 insertions(+), 170 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..97aea91 100644 --- a/src/rebar_config.erl +++ b/src/rebar_config.erl @@ -106,21 +106,25 @@ 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_dep(element(1, Dep), LockedDeps); + (Dep) -> + check_dep(Dep, LockedDeps) + end, ConfigDeps). + +check_dep(Dep, LockedDeps) -> + 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. diff --git a/src/rebar_digraph.erl b/src/rebar_digraph.erl index 129ea35..35fd7e7 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) -> @@ -99,7 +101,7 @@ cull_deps(Graph, Vertices, Level, Levels, Solution, Discarded) -> DiscardedAcc1} end end, {NewVertices, SolutionAcc, LevelsAcc, DiscardedAcc}, OutNeighbors) - end, {[], Solution, Levels, Discarded}, lists:keysort(1, Vertices)), + end, {[], Solution, Levels, Discarded}, lists:sort(Vertices)), cull_deps(Graph, NV, Level+1, LS, NS, DS). subgraph(Graph, Vertices) -> diff --git a/src/rebar_packages.erl b/src/rebar_packages.erl index 5c67600..3890510 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, "registry2"), + 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..57cd05f 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,10 @@ 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), + State = rebar_state:packages(State0, {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 +213,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 +315,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 +326,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 +374,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} -> + %% Preserve the state we created with overrides + AppState = rebar_app_info:state(AppInfo), + FoundApp = rebar_app_info:state(FoundApp, AppState), ?INFO("Linking ~s to ~s", [rebar_app_info:dir(FoundApp), AppDir]), filelib:ensure_dir(AppDir), rebar_file_utils:symlink_or_copy(rebar_app_info:dir(FoundApp), AppDir), - true + {true, AppInfo} 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 +448,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 +571,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_packages: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) -> <>; 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..af875d7 100644 --- a/src/rebar_state.erl +++ b/src/rebar_state.erl @@ -30,6 +30,8 @@ overrides/1, overrides/2, apply_overrides/2, + packages/1, packages/2, + resources/1, resources/2, add_resource/2, providers/1, providers/2, add_provider/2]). @@ -51,6 +53,8 @@ deps_to_build = [] :: [rebar_app_info:t()], all_deps = [] :: [rebar_app_info:t()], + packages = undefined :: {rebar_dict(), rebar_digraph()} | undefined, + overrides = [], resources = [], providers = []}). @@ -242,7 +246,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 +303,14 @@ 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}. + -spec resources(t()) -> rebar_resource:resource(). resources(#state_t{resources=Resources}) -> Resources. @@ -340,65 +353,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 %% ==================================================================== -- cgit v1.1