Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 

366 rader
14 KiB

-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.