-module(rebar_packages).
|
|
|
|
-export([get/2
|
|
,get_all_names/1
|
|
,registry_dir/1
|
|
,package_dir/2
|
|
,find_highest_matching/5
|
|
,verify_table/1
|
|
,format_error/1
|
|
,update_package/3
|
|
,resolve_version/6]).
|
|
|
|
-ifdef(TEST).
|
|
-export([new_package_table/0, find_highest_matching_/5, cmp_/4, cmpl_/4, valid_vsn/1]).
|
|
-endif.
|
|
|
|
-export_type([package/0]).
|
|
|
|
-include("rebar.hrl").
|
|
-include_lib("providers/include/providers.hrl").
|
|
|
|
-type pkg_name() :: binary() | atom().
|
|
-type vsn() :: binary().
|
|
-type package() :: pkg_name() | {pkg_name(), vsn()}.
|
|
|
|
format_error({missing_package, Name, Vsn}) ->
|
|
io_lib:format("Package not found in any repo: ~ts ~ts", [rebar_utils:to_binary(Name),
|
|
rebar_utils:to_binary(Vsn)]);
|
|
format_error({missing_package, Pkg}) ->
|
|
io_lib:format("Package not found in any repo: ~p", [Pkg]).
|
|
|
|
-spec get(rebar_hex_repos:repo(), binary()) -> {ok, map()} | {error, term()}.
|
|
get(Config, Name) ->
|
|
try r3_hex_api_package:get(Config, Name) of
|
|
{ok, {200, _Headers, PkgInfo}} ->
|
|
{ok, PkgInfo};
|
|
{ok, {404, _, _}} ->
|
|
{error, not_found};
|
|
Error ->
|
|
?DEBUG("Hex api request failed: ~p", [Error]),
|
|
{error, unknown}
|
|
catch
|
|
error:{badmatch, {error, {failed_connect, _}}} ->
|
|
{error, failed_to_connect};
|
|
_:Exception ->
|
|
?DEBUG("hex_api_package:get failed: ~p", [Exception]),
|
|
{error, unknown}
|
|
end.
|
|
|
|
|
|
-spec get_all_names(rebar_state:t()) -> [binary()].
|
|
get_all_names(State) ->
|
|
verify_table(State),
|
|
lists:usort(ets:select(?PACKAGE_TABLE, [{#package{key={'$1', '_', '_'},
|
|
_='_'},
|
|
[], ['$1']}])).
|
|
|
|
-spec get_package_versions(unicode:unicode_binary(), ec_semver:semver(),
|
|
unicode:unicode_binary(),
|
|
ets:tid(), rebar_state:t()) -> [vsn()].
|
|
get_package_versions(Dep, {_, AlphaInfo}, Repo, Table, State) ->
|
|
?MODULE:verify_table(State),
|
|
AllowPreRelease = rebar_state:get(State, deps_allow_prerelease, false)
|
|
orelse AlphaInfo =/= {[],[]},
|
|
ets:select(Table, [{#package{key={Dep, {'$1', '$2'}, Repo},
|
|
_='_'},
|
|
[{'==', '$2', {{[],[]}}} || not AllowPreRelease], [{{'$1', '$2'}}]}]).
|
|
|
|
-spec get_package(unicode:unicode_binary(), unicode:unicode_binary(),
|
|
binary() | undefined | '_',
|
|
[unicode:unicode_binary()] | ['_'], ets:tab(), rebar_state:t())
|
|
-> {ok, #package{}} | not_found.
|
|
get_package(Dep, Vsn, undefined, Repos, Table, State) ->
|
|
get_package(Dep, Vsn, '_', Repos, Table, State);
|
|
get_package(Dep, Vsn, Hash, Repos, Table, State) ->
|
|
?MODULE:verify_table(State),
|
|
MatchingPackages = ets:select(Table, [{#package{key={Dep, ec_semver:parse(Vsn), Repo},
|
|
_='_'}, [], ['$_']} || Repo <- Repos]),
|
|
PackagesWithProperHash = lists:filter(
|
|
fun(#package{key = {_Dep, _Vsn, Repo}, outer_checksum = PkgChecksum}) ->
|
|
if (PkgChecksum =/= Hash) andalso (Hash =/= '_') ->
|
|
?WARN("Checksum mismatch for package ~ts-~ts from repo ~ts", [Dep, Vsn, Repo]),
|
|
false;
|
|
true ->
|
|
true
|
|
end
|
|
end, MatchingPackages
|
|
),
|
|
case PackagesWithProperHash of
|
|
%% have to allow multiple matches in the list for cases that Repo is `_`
|
|
[Package | _] ->
|
|
{ok, Package};
|
|
[] ->
|
|
not_found
|
|
end.
|
|
|
|
new_package_table() ->
|
|
?PACKAGE_TABLE = ets:new(?PACKAGE_TABLE, [named_table, public, ordered_set, {keypos, 2}]),
|
|
ets:insert(?PACKAGE_TABLE, {?PACKAGE_INDEX_VERSION, package_index_version}).
|
|
|
|
load_and_verify_version(State) ->
|
|
{ok, RegistryDir} = registry_dir(State),
|
|
case ets:file2tab(filename:join(RegistryDir, ?INDEX_FILE)) of
|
|
{ok, _} ->
|
|
case ets:lookup_element(?PACKAGE_TABLE, package_index_version, 1) of
|
|
?PACKAGE_INDEX_VERSION ->
|
|
true;
|
|
V ->
|
|
%% no reason to confuse the user since we just start fresh and they
|
|
%% shouldn't notice, so log as a debug message only
|
|
?DEBUG("Package index version mismatch. Current version ~p, this rebar3 expecting ~p",
|
|
[V, ?PACKAGE_INDEX_VERSION]),
|
|
(catch ets:delete(?PACKAGE_TABLE)),
|
|
new_package_table()
|
|
end;
|
|
_ ->
|
|
new_package_table()
|
|
end.
|
|
|
|
handle_missing_package(PkgKey, Repo, State, Fun) ->
|
|
Name =
|
|
case PkgKey of
|
|
{N, Vsn, _Repo} ->
|
|
?DEBUG("Package ~ts-~ts not found. Fetching registry updates for "
|
|
"package and trying again...", [N, Vsn]),
|
|
N;
|
|
_ ->
|
|
?DEBUG("Package ~p not found. Fetching registry updates for "
|
|
"package and trying again...", [PkgKey]),
|
|
PkgKey
|
|
end,
|
|
|
|
update_package(Name, Repo, State),
|
|
try
|
|
Fun(State)
|
|
catch
|
|
_:_ ->
|
|
%% Even after an update the package is still missing, time to error out
|
|
throw(?PRV_ERROR({missing_package, PkgKey}))
|
|
end.
|
|
|
|
registry_dir(State) ->
|
|
CacheDir = rebar_dir:global_cache_dir(rebar_state:opts(State)),
|
|
RegistryDir = filename:join([CacheDir, "hex"]),
|
|
case filelib:ensure_dir(filename:join(RegistryDir, "placeholder")) of
|
|
ok -> ok;
|
|
{error, Posix} when Posix == eaccess; Posix == enoent ->
|
|
?ABORT("Could not write to ~p. Please ensure the path is writeable.",
|
|
[RegistryDir])
|
|
end,
|
|
{ok, RegistryDir}.
|
|
|
|
-spec package_dir(rebar_hex_repos:repo(), rebar_state:t()) -> {ok, file:filename_all()}.
|
|
package_dir(Repo, State) ->
|
|
{ok, RegistryDir} = registry_dir(State),
|
|
RepoName = maps:get(name, Repo),
|
|
PackageDir = filename:join([RegistryDir, rebar_utils:to_list(RepoName), "packages"]),
|
|
ok = filelib:ensure_dir(filename:join(PackageDir, "placeholder")),
|
|
{ok, PackageDir}.
|
|
|
|
|
|
%% 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, Repo, Table, State) ->
|
|
try find_highest_matching_(Dep, Constraint, Repo, Table, State) of
|
|
none ->
|
|
handle_missing_package(Dep, Repo, State,
|
|
fun(State1) ->
|
|
find_highest_matching_(Dep, Constraint, Repo, Table, State1)
|
|
end);
|
|
Result ->
|
|
Result
|
|
catch
|
|
_:_ ->
|
|
handle_missing_package(Dep, Repo, State,
|
|
fun(State1) ->
|
|
find_highest_matching_(Dep, Constraint, Repo, Table, State1)
|
|
end)
|
|
end.
|
|
|
|
find_highest_matching_(Dep, Constraint, #{name := Repo}, Table, State) ->
|
|
try get_package_versions(Dep, Constraint, Repo, Table, State) of
|
|
[Vsn] ->
|
|
handle_single_vsn(Vsn, Constraint);
|
|
Vsns ->
|
|
case handle_vsns(Constraint, Vsns) of
|
|
none ->
|
|
none;
|
|
FoundVsn ->
|
|
{ok, FoundVsn}
|
|
end
|
|
catch
|
|
error:badarg ->
|
|
none
|
|
end.
|
|
|
|
handle_vsns(Constraint, Vsns) ->
|
|
lists:foldl(fun(Version, Highest) ->
|
|
case ec_semver:pes(Version, Constraint) andalso
|
|
(Highest =:= none orelse ec_semver:gt(Version, Highest)) of
|
|
true ->
|
|
Version;
|
|
false ->
|
|
Highest
|
|
end
|
|
end, none, Vsns).
|
|
|
|
handle_single_vsn(Vsn, Constraint) ->
|
|
case ec_semver:pes(Vsn, Constraint) of
|
|
true ->
|
|
{ok, Vsn};
|
|
false ->
|
|
none
|
|
end.
|
|
|
|
verify_table(State) ->
|
|
ets:info(?PACKAGE_TABLE, named_table) =:= true orelse load_and_verify_version(State).
|
|
|
|
parse_deps(Deps) ->
|
|
[{maps:get(app, D, Name), {pkg, Name, Constraint, undefined, undefined}}
|
|
|| D=#{package := Name,
|
|
requirement := Constraint} <- Deps].
|
|
|
|
parse_checksum(<<Checksum:256/big-unsigned>>) ->
|
|
list_to_binary(
|
|
rebar_string:uppercase(
|
|
lists:flatten(io_lib:format("~64.16.0b", [Checksum]))));
|
|
parse_checksum(Checksum) ->
|
|
Checksum.
|
|
|
|
update_package(Name, RepoConfig=#{name := Repo}, State) ->
|
|
?MODULE:verify_table(State),
|
|
try r3_hex_repo:get_package(get_package_repo_config(RepoConfig), Name) of
|
|
{ok, {200, _Headers, Releases}} ->
|
|
_ = insert_releases(Name, Releases, Repo, ?PACKAGE_TABLE),
|
|
{ok, RegistryDir} = rebar_packages:registry_dir(State),
|
|
PackageIndex = filename:join(RegistryDir, ?INDEX_FILE),
|
|
case ets:tab2file(?PACKAGE_TABLE, PackageIndex) of
|
|
ok -> ok;
|
|
{error, Error} ->
|
|
?WARN("Failed to update package index at ~p: ~p", [PackageIndex, Error])
|
|
end;
|
|
{error, unverified} ->
|
|
?WARN(unverified_repo_message(), [Repo]),
|
|
fail;
|
|
Error ->
|
|
?DEBUG("Hex get_package request failed: ~p", [Error]),
|
|
%% TODO: add better log message. r3_hex_core should export a format_error
|
|
?WARN("Failed to update package ~ts from repo ~ts", [Name, Repo]),
|
|
fail
|
|
catch
|
|
_:Exception ->
|
|
?DEBUG("hex_repo:get_package failed for package ~p: ~p", [Name, Exception]),
|
|
fail
|
|
end.
|
|
|
|
get_package_repo_config(RepoConfig=#{mirror_of := _}) ->
|
|
get_package_repo_config(maps:remove(mirror_of, RepoConfig));
|
|
get_package_repo_config(RepoConfig=#{name := _}) ->
|
|
get_package_repo_config(maps:remove(name, RepoConfig));
|
|
get_package_repo_config(RepoConfig) ->
|
|
RepoConfig.
|
|
|
|
unverified_repo_message() ->
|
|
"The registry repository ~ts uses a record format that has been deprecated for "
|
|
"security reasons. The repository should be updated in order to be safer. "
|
|
"You can disable this check by setting REBAR_NO_VERIFY_REPO_ORIGIN=1".
|
|
|
|
insert_releases(Name, Releases, Repo, Table) ->
|
|
[true = ets:insert(Table,
|
|
#package{key={Name, ec_semver:parse(Version), Repo},
|
|
inner_checksum=parse_checksum(InnerChecksum),
|
|
outer_checksum=parse_checksum(OuterChecksum),
|
|
retired=maps:get(retired, Release, false),
|
|
dependencies=parse_deps(Dependencies)})
|
|
|| Release=#{inner_checksum := InnerChecksum,
|
|
outer_checksum := OuterChecksum,
|
|
version := Version,
|
|
dependencies := Dependencies} <- Releases].
|
|
|
|
-spec resolve_version(unicode:unicode_binary(), unicode:unicode_binary() | undefined,
|
|
binary() | undefined,
|
|
binary() | undefined,
|
|
ets:tab(), rebar_state:t())
|
|
-> {error, {invalid_vsn, unicode:unicode_binary()}} |
|
|
not_found |
|
|
{ok, #package{}, map()}.
|
|
%% if checksum is defined search for any matching repo matching pkg-vsn and checksum
|
|
resolve_version(Dep, DepVsn, _OldHash, Hash, HexRegistry, State) when is_binary(Hash) ->
|
|
Resources = rebar_state:resources(State),
|
|
#{repos := RepoConfigs} = rebar_resource_v2:find_resource_state(pkg, Resources),
|
|
RepoNames = [RepoName || #{name := RepoName} <- RepoConfigs],
|
|
|
|
%% allow retired packages when we have a checksum
|
|
case get_package(Dep, DepVsn, Hash, RepoNames, HexRegistry, State) of
|
|
{ok, Package=#package{key={_, _, RepoName}}} ->
|
|
{ok, RepoConfig} = rebar_hex_repos:get_repo_config(RepoName, RepoConfigs),
|
|
{ok, Package, RepoConfig};
|
|
_ ->
|
|
Fun = fun(Repo) ->
|
|
case resolve_version_(Dep, DepVsn, Repo, HexRegistry, State) of
|
|
none ->
|
|
not_found;
|
|
{ok, Vsn} ->
|
|
get_package(Dep, Vsn, Hash, [Repo], HexRegistry, State)
|
|
end
|
|
end,
|
|
handle_missing_no_exception(Fun, Dep, State)
|
|
end;
|
|
resolve_version(Dep, undefined, _OldHash, Hash, HexRegistry, State) ->
|
|
Fun = fun(Repo) ->
|
|
case highest_matching(Dep, {0,{[],[]}}, Repo, HexRegistry, State) of
|
|
none ->
|
|
not_found;
|
|
{ok, Vsn} ->
|
|
get_package(Dep, Vsn, Hash, [Repo], HexRegistry, State)
|
|
end
|
|
end,
|
|
handle_missing_no_exception(Fun, Dep, State);
|
|
resolve_version(Dep, DepVsn, _OldHash, Hash, HexRegistry, State) ->
|
|
case valid_vsn(DepVsn) of
|
|
false ->
|
|
{error, {invalid_vsn, DepVsn}};
|
|
_ ->
|
|
Fun = fun(Repo) ->
|
|
case resolve_version_(Dep, DepVsn, Repo, HexRegistry, State) of
|
|
none ->
|
|
not_found;
|
|
{ok, Vsn} ->
|
|
get_package(Dep, Vsn, Hash, [Repo], HexRegistry, State)
|
|
end
|
|
end,
|
|
handle_missing_no_exception(Fun, Dep, State)
|
|
end.
|
|
|
|
check_all_repos(Fun, RepoConfigs) ->
|
|
ec_lists:search(fun(#{name := R}) ->
|
|
Fun(R)
|
|
end, RepoConfigs).
|
|
|
|
handle_missing_no_exception(Fun, Dep, State) ->
|
|
Resources = rebar_state:resources(State),
|
|
#{repos := RepoConfigs} = rebar_resource_v2:find_resource_state(pkg, Resources),
|
|
|
|
%% first check all repos in order for a local match
|
|
%% if none is found then we step through checking after updating the repo registry
|
|
case check_all_repos(Fun, RepoConfigs) of
|
|
not_found ->
|
|
ec_lists:search(fun(Config=#{name := R}) ->
|
|
case ?MODULE:update_package(Dep, Config, State) of
|
|
ok ->
|
|
Fun(R);
|
|
_ ->
|
|
not_found
|
|
end
|
|
end, RepoConfigs);
|
|
Result ->
|
|
Result
|
|
end.
|
|
|
|
resolve_version_(Dep, DepVsn, Repo, HexRegistry, State) ->
|
|
case DepVsn of
|
|
<<"~>", Vsn/binary>> ->
|
|
highest_matching(Dep, rm_ws(Vsn), Repo, HexRegistry, State);
|
|
<<">=", Vsn/binary>> ->
|
|
cmp(Dep, rm_ws(Vsn), Repo, HexRegistry, State, fun ec_semver:gte/2);
|
|
<<">", Vsn/binary>> ->
|
|
cmp(Dep, rm_ws(Vsn), Repo, HexRegistry, State, fun ec_semver:gt/2);
|
|
<<"<=", Vsn/binary>> ->
|
|
cmpl(Dep, rm_ws(Vsn), Repo, HexRegistry, State, fun ec_semver:lte/2);
|
|
<<"<", Vsn/binary>> ->
|
|
cmpl(Dep, rm_ws(Vsn), Repo, HexRegistry, State, fun ec_semver:lt/2);
|
|
<<"==", Vsn/binary>> ->
|
|
{ok, Vsn};
|
|
Vsn ->
|
|
{ok, Vsn}
|
|
end.
|
|
|
|
rm_ws(<<" ", R/binary>>) ->
|
|
ec_semver:parse(rm_ws(R));
|
|
rm_ws(R) ->
|
|
ec_semver:parse(R).
|
|
|
|
valid_vsn(Vsn) ->
|
|
%% Regepx from https://github.com/sindresorhus/semver-regex/blob/master/index.js
|
|
SemVerRegExp = "v?(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))?"
|
|
"(-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9a-z-]+(\\.[0-9a-z-]+)*)?",
|
|
SupportedVersions = "^(>=?|<=?|~>|==)?\\s*" ++ SemVerRegExp ++ "$",
|
|
re:run(Vsn, SupportedVersions, [unicode]) =/= nomatch.
|
|
|
|
highest_matching(Dep, Vsn, Repo, HexRegistry, State) ->
|
|
find_highest_matching_(Dep, Vsn, #{name => Repo}, HexRegistry, State).
|
|
|
|
cmp(Dep, Vsn, Repo, HexRegistry, State, CmpFun) ->
|
|
case get_package_versions(Dep, Vsn, Repo, HexRegistry, State) of
|
|
[] ->
|
|
none;
|
|
Vsns ->
|
|
cmp_(undefined, Vsn, Vsns, CmpFun)
|
|
end.
|
|
|
|
cmp_(undefined, MinVsn, [], _CmpFun) ->
|
|
{ok, MinVsn};
|
|
cmp_(HighestDepVsn, _MinVsn, [], _CmpFun) ->
|
|
{ok, HighestDepVsn};
|
|
|
|
cmp_(BestMatch, MinVsn, [Vsn | R], CmpFun) ->
|
|
case CmpFun(Vsn, MinVsn) of
|
|
true ->
|
|
cmp_(Vsn, Vsn, R, CmpFun);
|
|
false ->
|
|
cmp_(BestMatch, MinVsn, R, CmpFun)
|
|
end.
|
|
|
|
%% We need to treat this differently since we want a version that is LOWER but
|
|
%% the higest possible one.
|
|
cmpl(Dep, Vsn, Repo, HexRegistry, State, CmpFun) ->
|
|
case get_package_versions(Dep, Vsn, Repo, HexRegistry, State) of
|
|
[] ->
|
|
none;
|
|
Vsns ->
|
|
cmpl_(undefined, Vsn, Vsns, CmpFun)
|
|
end.
|
|
|
|
cmpl_(undefined, MaxVsn, [], _CmpFun) ->
|
|
{ok, MaxVsn};
|
|
cmpl_(HighestDepVsn, _MaxVsn, [], _CmpFun) ->
|
|
{ok, HighestDepVsn};
|
|
|
|
cmpl_(undefined, MaxVsn, [Vsn | R], CmpFun) ->
|
|
case CmpFun(Vsn, MaxVsn) of
|
|
true ->
|
|
cmpl_(Vsn, MaxVsn, R, CmpFun);
|
|
false ->
|
|
cmpl_(undefined, MaxVsn, R, CmpFun)
|
|
end;
|
|
|
|
cmpl_(BestMatch, MaxVsn, [Vsn | R], CmpFun) ->
|
|
case CmpFun(Vsn, MaxVsn) of
|
|
true ->
|
|
case ec_semver:gte(Vsn, BestMatch) of
|
|
true ->
|
|
cmpl_(Vsn, MaxVsn, R, CmpFun);
|
|
false ->
|
|
cmpl_(BestMatch, MaxVsn, R, CmpFun)
|
|
end;
|
|
false ->
|
|
cmpl_(BestMatch, MaxVsn, R, CmpFun)
|
|
end.
|