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