-module(rebar_packages).
|
|
|
|
-export([packages/1
|
|
,close_packages/0
|
|
,load_and_verify_version/1
|
|
,deps/3
|
|
,registry_dir/1
|
|
,package_dir/1
|
|
,registry_checksum/2
|
|
,find_highest_matching/6
|
|
,find_highest_matching/4
|
|
,find_all/3
|
|
,verify_table/1
|
|
,format_error/1]).
|
|
|
|
-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()}.
|
|
|
|
-spec packages(rebar_state:t()) -> ets:tid().
|
|
packages(State) ->
|
|
catch ets:delete(?PACKAGE_TABLE),
|
|
case load_and_verify_version(State) of
|
|
true ->
|
|
ok;
|
|
false ->
|
|
?DEBUG("Error loading package index.", []),
|
|
handle_bad_index(State)
|
|
end.
|
|
|
|
handle_bad_index(State) ->
|
|
?ERROR("Bad packages index. Trying to fix by updating the registry.", []),
|
|
{ok, State1} = rebar_prv_update:do(State),
|
|
case load_and_verify_version(State1) of
|
|
true ->
|
|
ok;
|
|
false ->
|
|
%% Still unable to load after an update, create an empty registry
|
|
ets:new(?PACKAGE_TABLE, [named_table, public])
|
|
end.
|
|
|
|
close_packages() ->
|
|
catch ets:delete(?PACKAGE_TABLE).
|
|
|
|
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, 2) of
|
|
?PACKAGE_INDEX_VERSION ->
|
|
true;
|
|
_ ->
|
|
(catch ets:delete(?PACKAGE_TABLE)),
|
|
rebar_prv_update:hex_to_index(State)
|
|
end;
|
|
_ ->
|
|
rebar_prv_update:hex_to_index(State)
|
|
end.
|
|
|
|
deps(Name, Vsn, State) ->
|
|
try
|
|
deps_(Name, Vsn, State)
|
|
catch
|
|
_:_ ->
|
|
handle_missing_package({Name, Vsn}, State, fun(State1) -> deps_(Name, Vsn, State1) end)
|
|
end.
|
|
|
|
deps_(Name, Vsn, State) ->
|
|
?MODULE:verify_table(State),
|
|
ets:lookup_element(?PACKAGE_TABLE, {ec_cnv:to_binary(Name), ec_cnv:to_binary(Vsn)}, 2).
|
|
|
|
handle_missing_package(Dep, State, Fun) ->
|
|
case Dep of
|
|
{Name, Vsn} ->
|
|
?INFO("Package ~s-~s not found. Fetching registry updates and trying again...", [Name, Vsn]);
|
|
_ ->
|
|
?INFO("Package ~p not found. Fetching registry updates and trying again...", [Dep])
|
|
end,
|
|
|
|
{ok, State1} = rebar_prv_update:do(State),
|
|
try
|
|
Fun(State1)
|
|
catch
|
|
_:_ ->
|
|
%% Even after an update the package is still missing, time to error out
|
|
throw(?PRV_ERROR({missing_package, Dep}))
|
|
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"]),
|
|
ok = filelib:ensure_dir(filename:join(RegistryDir, "placeholder")),
|
|
{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(string:tokens(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.
|
|
|
|
registry_checksum({pkg, Name, Vsn, _Hash}, State) ->
|
|
try
|
|
?MODULE:verify_table(State),
|
|
ets:lookup_element(?PACKAGE_TABLE, {Name, Vsn}, 3)
|
|
catch
|
|
_:_ ->
|
|
throw(?PRV_ERROR({missing_package, ec_cnv:to_binary(Name), ec_cnv:to_binary(Vsn)}))
|
|
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 find_all(Dep, Table, State) of
|
|
{ok, [Vsn]} ->
|
|
handle_single_vsn(Pkg, PkgVsn, Dep, Vsn, Constraint);
|
|
{ok, [HeadVsn | VsnTail]} ->
|
|
{ok, handle_vsns(Constraint, HeadVsn, VsnTail)}
|
|
catch
|
|
error:badarg ->
|
|
none
|
|
end.
|
|
|
|
find_all(Dep, Table, State) ->
|
|
?MODULE:verify_table(State),
|
|
try ets:lookup_element(Table, Dep, 2) of
|
|
[Vsns] when is_list(Vsns)->
|
|
{ok, Vsns};
|
|
Vsns ->
|
|
{ok, Vsns}
|
|
catch
|
|
error:badarg ->
|
|
none
|
|
end.
|
|
|
|
handle_vsns(Constraint, 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).
|
|
|
|
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} ->
|
|
?WARN("Only existing version of ~s is ~s which does not match constraint ~~> ~s. "
|
|
"Using anyway, but it is not guaranteed to work.", [Dep, Vsn, Constraint]);
|
|
_ ->
|
|
?WARN("[~s:~s] Only existing version of ~s is ~s which does not match constraint ~~> ~s. "
|
|
"Using anyway, but it is not guaranteed to work.", [Pkg, PkgVsn, Dep, Vsn, Constraint])
|
|
end,
|
|
{ok, Vsn}
|
|
end.
|
|
|
|
format_error({missing_package, {Name, Vsn}}) ->
|
|
io_lib:format("Package not found in registry: ~s-~s.", [ec_cnv:to_binary(Name), ec_cnv:to_binary(Vsn)]);
|
|
format_error({missing_package, Dep}) ->
|
|
io_lib:format("Package not found in registry: ~p.", [Dep]).
|
|
|
|
verify_table(State) ->
|
|
ets:info(?PACKAGE_TABLE, named_table) =:= true orelse load_and_verify_version(State).
|