-module(rebar_packages).
|
|
|
|
-export([get/2
|
|
,get_all_names/1
|
|
,get_package_versions/3
|
|
,get_package_deps/3
|
|
,new_package_table/0
|
|
,load_and_verify_version/1
|
|
,registry_dir/1
|
|
,package_dir/1
|
|
,registry_checksum/3
|
|
,find_highest_matching/6
|
|
,find_highest_matching/4
|
|
,find_highest_matching_/6
|
|
,verify_table/1
|
|
,format_error/1
|
|
,update_package/2
|
|
,resolve_version/4]).
|
|
|
|
-ifdef(TEST).
|
|
-export([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 registry: ~ts-~ts.", [rebar_utils:to_binary(Name),
|
|
rebar_utils:to_binary(Vsn)]);
|
|
format_error({missing_package, Pkg}) ->
|
|
io_lib:format("Package not found in registry: ~p.", [Pkg]).
|
|
|
|
-spec get(hex_core:config(), binary()) -> {ok, map()} | {error, term()}.
|
|
get(Config, Name) ->
|
|
case hex_api_package:get(Config, Name) of
|
|
{ok, {200, _Headers, PkgInfo}} ->
|
|
{ok, PkgInfo};
|
|
_ ->
|
|
{error, blewup}
|
|
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(binary(), ets:tid(), rebar_state:t()) -> [vsn()].
|
|
get_package_versions(Dep, Table, State) ->
|
|
?MODULE:verify_table(State),
|
|
ets:select(Table, [{#package{key={Dep,'$1'},
|
|
_='_'},
|
|
[], ['$1']}]).
|
|
|
|
new_package_table() ->
|
|
ets:new(?PACKAGE_TABLE, [named_table, public, ordered_set, {keypos, 2}]),
|
|
ets:insert(package_index, {?PACKAGE_INDEX_VERSION, package_index_version}).
|
|
|
|
-spec get_package_deps(binary(), vsn(), rebar_state:t()) -> [map()].
|
|
get_package_deps(Name, Vsn, State) ->
|
|
try_lookup(?PACKAGE_TABLE, {Name, Vsn}, #package.dependencies, State).
|
|
|
|
-spec registry_checksum(binary(), vsn(), rebar_state:t()) -> binary().
|
|
registry_checksum(Name, Vsn, State) ->
|
|
try_lookup(?PACKAGE_TABLE, {Name, Vsn}, #package.checksum, State).
|
|
|
|
try_lookup(Table, Key, Element, State) ->
|
|
?MODULE:verify_table(State),
|
|
try
|
|
ets:lookup_element(Table, Key, Element)
|
|
catch
|
|
_:_ ->
|
|
handle_missing_package(Key, State, fun(_) ->
|
|
ets:lookup_element(Table, Key, Element)
|
|
end)
|
|
end.
|
|
|
|
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, State, Fun) ->
|
|
Name =
|
|
case PkgKey of
|
|
{N, Vsn} ->
|
|
?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, 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)),
|
|
case rebar_state:get(State, rebar_packages_cdn, ?DEFAULT_CDN) of
|
|
?DEFAULT_CDN ->
|
|
RegistryDir = filename:join([CacheDir, "hex", "default"]),
|
|
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};
|
|
CDN ->
|
|
case rebar_utils:url_append_path(CDN, ?REMOTE_PACKAGE_DIR) of
|
|
{ok, Parsed} ->
|
|
{ok, {_, _, Host, _, Path, _}} = http_uri:parse(Parsed),
|
|
CDNHostPath = lists:reverse(rebar_string:lexemes(Host, ".")),
|
|
CDNPath = tl(filename:split(Path)),
|
|
RegistryDir = filename:join([CacheDir, "hex"] ++ CDNHostPath ++ CDNPath),
|
|
ok = filelib:ensure_dir(filename:join(RegistryDir, "placeholder")),
|
|
{ok, RegistryDir};
|
|
_ ->
|
|
{uri_parse_error, CDN}
|
|
end
|
|
end.
|
|
|
|
package_dir(State) ->
|
|
case registry_dir(State) of
|
|
{ok, RegistryDir} ->
|
|
PackageDir = filename:join([RegistryDir, "packages"]),
|
|
ok = filelib:ensure_dir(filename:join(PackageDir, "placeholder")),
|
|
{ok, PackageDir};
|
|
Error ->
|
|
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, Table, State) ->
|
|
find_highest_matching(undefined, undefined, Dep, Constraint, Table, State).
|
|
|
|
find_highest_matching(Pkg, PkgVsn, Dep, Constraint, Table, State) ->
|
|
try find_highest_matching_(Pkg, PkgVsn, Dep, Constraint, Table, State) of
|
|
none ->
|
|
handle_missing_package(Dep, State,
|
|
fun(State1) ->
|
|
find_highest_matching_(Pkg, PkgVsn, Dep, Constraint, Table, State1)
|
|
end);
|
|
Result ->
|
|
Result
|
|
catch
|
|
_:_ ->
|
|
handle_missing_package(Dep, State,
|
|
fun(State1) ->
|
|
find_highest_matching_(Pkg, PkgVsn, Dep, Constraint, Table, State1)
|
|
end)
|
|
end.
|
|
|
|
find_highest_matching_(Pkg, PkgVsn, Dep, Constraint, Table, State) ->
|
|
try get_package_versions(Dep, Table, State) of
|
|
[Vsn] ->
|
|
handle_single_vsn(Pkg, PkgVsn, Dep, 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(Pkg, PkgVsn, Dep, Vsn, Constraint) ->
|
|
case ec_semver:pes(Vsn, Constraint) of
|
|
true ->
|
|
{ok, Vsn};
|
|
false ->
|
|
case {Pkg, PkgVsn} of
|
|
{undefined, undefined} ->
|
|
?DEBUG("Only existing version of ~ts is ~ts which does not match constraint ~~> ~ts. "
|
|
"Using anyway, but it is not guaranteed to work.", [Dep, Vsn, Constraint]);
|
|
_ ->
|
|
?DEBUG("[~ts:~ts] Only existing version of ~ts is ~ts which does not match constraint ~~> ~ts. "
|
|
"Using anyway, but it is not guaranteed to work.", [Pkg, PkgVsn, Dep, Vsn, Constraint])
|
|
end,
|
|
{ok, Vsn}
|
|
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}}
|
|
|| 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, State) ->
|
|
Resources = rebar_state:resources(State),
|
|
#{hex_config := HexConfig} = rebar_resource:find_resource_state(pkg, Resources),
|
|
case hex_repo:get_package(HexConfig, Name) of
|
|
{ok, {200, _Headers, #{releases := Releases}}} ->
|
|
_ = insert_releases(Name, Releases, ?PACKAGE_TABLE),
|
|
{ok, RegistryDir} = rebar_packages:registry_dir(State),
|
|
PackageIndex = filename:join(RegistryDir, ?INDEX_FILE),
|
|
ok = ets:tab2file(?PACKAGE_TABLE, PackageIndex);
|
|
_ ->
|
|
fail
|
|
end.
|
|
|
|
insert_releases(Name, Releases, Table) ->
|
|
[true = ets:insert(Table,
|
|
#package{key={Name, Version},
|
|
checksum=parse_checksum(Checksum),
|
|
dependencies=parse_deps(Dependencies)})
|
|
|| #{checksum := Checksum,
|
|
version := Version,
|
|
dependencies := Dependencies} <- Releases].
|
|
|
|
resolve_version(Dep, undefined, HexRegistry, State) ->
|
|
find_highest_matching(Dep, "0", HexRegistry, State);
|
|
resolve_version(Dep, DepVsn, HexRegistry, State) ->
|
|
case {valid_vsn(DepVsn), DepVsn} of
|
|
{false, Vsn} ->
|
|
{error, {invalid_vsn, Vsn}};
|
|
{_, <<"~>", Vsn/binary>>} ->
|
|
highest_matching(Dep, rm_ws(Vsn), HexRegistry, State);
|
|
{_, <<">=", Vsn/binary>>} ->
|
|
cmp(Dep, rm_ws(Vsn), HexRegistry, State, fun ec_semver:gte/2);
|
|
{_, <<">", Vsn/binary>>} ->
|
|
cmp(Dep, rm_ws(Vsn), HexRegistry, State, fun ec_semver:gt/2);
|
|
{_, <<"<=", Vsn/binary>>} ->
|
|
cmpl(Dep, rm_ws(Vsn), HexRegistry, State, fun ec_semver:lte/2);
|
|
{_, <<"<", Vsn/binary>>} ->
|
|
cmpl(Dep, rm_ws(Vsn), HexRegistry, State, fun ec_semver:lt/2);
|
|
{_, <<"==", Vsn/binary>>} ->
|
|
{ok, Vsn};
|
|
{_, Vsn} ->
|
|
{ok, Vsn}
|
|
end.
|
|
|
|
rm_ws(<<" ", R/binary>>) ->
|
|
rm_ws(R);
|
|
rm_ws(R) ->
|
|
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, HexRegistry, State) ->
|
|
case find_highest_matching_(undefined, undefined, Dep, Vsn, HexRegistry, State) of
|
|
{ok, HighestDepVsn} ->
|
|
{ok, HighestDepVsn};
|
|
none ->
|
|
{error, {invalid_vsn, Vsn}}
|
|
end.
|
|
|
|
cmp(Dep, Vsn, HexRegistry, State, CmpFun) ->
|
|
Vsns = get_package_versions(Dep, HexRegistry, State),
|
|
cmp_(undefined, Vsn, Vsns, CmpFun).
|
|
|
|
cmp_(undefined, MinVsn, [], _CmpFun) ->
|
|
MinVsn;
|
|
cmp_(HighestDepVsn, _MinVsn, [], _CmpFun) ->
|
|
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, HexRegistry, State, CmpFun) ->
|
|
Vsns = get_package_versions(Dep, HexRegistry, State),
|
|
cmpl_(undefined, Vsn, Vsns, CmpFun).
|
|
|
|
cmpl_(undefined, MaxVsn, [], _CmpFun) ->
|
|
MaxVsn;
|
|
cmpl_(HighestDepVsn, _MaxVsn, [], _CmpFun) ->
|
|
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.
|