diff options
Diffstat (limited to 'src')
30 files changed, 1520 insertions, 1061 deletions
diff --git a/src/rebar.app.src b/src/rebar.app.src index c96f65c..530a79e 100644 --- a/src/rebar.app.src +++ b/src/rebar.app.src @@ -30,6 +30,7 @@ relx, cf, inets, + hex_core, eunit_formatters]}, {env, [ %% Default log level @@ -67,6 +68,7 @@ rebar_prv_release, rebar_prv_relup, rebar_prv_report, + rebar_prv_repos, rebar_prv_shell, rebar_prv_state, rebar_prv_tar, diff --git a/src/rebar.hrl b/src/rebar.hrl index f461c70..572cbe8 100644 --- a/src/rebar.hrl +++ b/src/rebar.hrl @@ -25,14 +25,35 @@ -define(CONFIG_VERSION, "1.1.0"). -define(DEFAULT_CDN, "https://repo.hex.pm/"). -define(REMOTE_PACKAGE_DIR, "tarballs"). --define(REMOTE_REGISTRY_FILE, "registry.ets.gz"). -define(LOCK_FILE, "rebar.lock"). -define(DEFAULT_COMPILER_SOURCE_FORMAT, relative). +-define(PACKAGE_INDEX_VERSION, 4). +-define(PACKAGE_TABLE, package_index_v4). +-define(INDEX_FILE, "packages-v4.idx"). +-define(HEX_AUTH_FILE, "hex.config"). +-define(PUBLIC_HEX_REPO, <<"hexpm">>). --define(PACKAGE_INDEX_VERSION, 3). --define(PACKAGE_TABLE, package_index). --define(INDEX_FILE, "packages.idx"). --define(REGISTRY_FILE, "registry"). +%% ignore this function in all modules +%% not every module that exports it and relies on it being called implements provider +-ignore_xref([{format_error, 1}]). + +%% the package record is used in a select match spec which upsets dialyzer +%% this is the suggested workaround from Tobias +%% http://erlang.org/pipermail/erlang-questions/2009-February/041445.html +-type ms_field() :: '$1' | '_'. + +%% TODO: change package and requirement keys to be required (:=) after dropping support for OTP-18 +-record(package, {key :: {unicode:unicode_binary() | ms_field(), unicode:unicode_binary() | ms_field(), + unicode:unicode_binary() | ms_field()}, + checksum :: binary() | ms_field(), + retired :: boolean() | ms_field(), + dependencies :: [#{package => unicode:unicode_binary(), + requirement => unicode:unicode_binary()}] | ms_field()}). + +-record(resource, {type :: atom(), + module :: module(), + state :: term(), + implementation :: rebar_resource | rebar_resource_v2}). -ifdef(namespaced_types). -type rebar_dict() :: dict:dict(). diff --git a/src/rebar3.erl b/src/rebar3.erl index ec8e953..e87cb19 100644 --- a/src/rebar3.erl +++ b/src/rebar3.erl @@ -103,7 +103,7 @@ run(RawArgs) -> case erlang:system_info(version) of "6.1" -> ?WARN("Due to a filelib bug in Erlang 17.1 it is recommended" - "you update to a newer release.", []); + "you update to a newer release.", []); _ -> ok end, @@ -139,8 +139,14 @@ run_aux(State, RawArgs) -> rebar_state:set(State1, rebar_packages_cdn, CDN) end, + %% TODO: this means use of REBAR_PROFILE=profile will replace the repos with + %% the repos defined in the profile. But it will not work with `as profile`. + %% Maybe it shouldn't work with either to be consistent? + Resources = application:get_env(rebar, resources, []), + State2_ = rebar_state:create_resources(Resources, State2), + %% bootstrap test profile - State3 = rebar_state:add_to_profile(State2, test, test_state(State1)), + State3 = rebar_state:add_to_profile(State2_, test, test_state(State1)), %% Process each command, resetting any state between each one BaseDir = rebar_state:get(State, base_dir, ?DEFAULT_BASE_DIR), @@ -375,7 +381,11 @@ state_from_global_config(Config, GlobalConfigFile) -> %% We don't want to worry about global plugin install state effecting later %% usage. So we throw away the global profile state used for plugin install. - GlobalConfigThrowAway = rebar_state:current_profiles(GlobalConfig, [global]), + GlobalConfigThrowAway0 = rebar_state:current_profiles(GlobalConfig, [global]), + + Resources = application:get_env(rebar, resources, []), + GlobalConfigThrowAway = rebar_state:create_resources(Resources, GlobalConfigThrowAway0), + GlobalState = case rebar_state:get(GlobalConfigThrowAway, plugins, []) of [] -> GlobalConfigThrowAway; @@ -386,7 +396,8 @@ state_from_global_config(Config, GlobalConfigFile) -> end, GlobalPlugins = rebar_state:providers(GlobalState), GlobalConfig2 = rebar_state:set(GlobalConfig, plugins, []), - GlobalConfig3 = rebar_state:set(GlobalConfig2, {plugins, global}, rebar_state:get(GlobalConfigThrowAway, plugins, [])), + GlobalConfig3 = rebar_state:set(GlobalConfig2, {plugins, global}, + rebar_state:get(GlobalConfigThrowAway, plugins, [])), rebar_state:providers(rebar_state:new(GlobalConfig3, Config), GlobalPlugins). -spec test_state(rebar_state:t()) -> [{'extra_src_dirs',[string()]} | {'erl_opts',[any()]}]. diff --git a/src/rebar_api.erl b/src/rebar_api.erl index 9d9071e..4dabe8a 100644 --- a/src/rebar_api.erl +++ b/src/rebar_api.erl @@ -88,4 +88,4 @@ processing_base_dir(State) -> %% its configuration, including for validation of certs. -spec ssl_opts(string()) -> [term()]. ssl_opts(Url) -> - rebar_pkg_resource:ssl_opts(Url). + rebar_utils:ssl_opts(Url). diff --git a/src/rebar_app_discover.erl b/src/rebar_app_discover.erl index 382b36b..e82403c 100644 --- a/src/rebar_app_discover.erl +++ b/src/rebar_app_discover.erl @@ -9,8 +9,7 @@ find_apps/2, find_apps/3, find_app/2, - find_app/3, - find_app/4]). + find_app/3]). -include("rebar.hrl"). -include_lib("providers/include/providers.hrl"). @@ -95,7 +94,7 @@ format_error({missing_module, Module}) -> merge_deps(AppInfo, State) -> %% These steps make sure that hooks and artifacts are run in the context of %% the application they are defined at. If an umbrella structure is used and - %% they are deifned at the top level they will instead run in the context of + %% they are defined at the top level they will instead run in the context of %% the State and at the top level, not as part of an application. CurrentProfiles = rebar_state:current_profiles(State), Default = reset_hooks(rebar_state:default(State), CurrentProfiles), @@ -205,7 +204,7 @@ reset_hooks(Opts, CurrentProfiles) -> -spec all_app_dirs([file:name()]) -> [{file:name(), [file:name()]}]. all_app_dirs(LibDirs) -> lists:flatmap(fun(LibDir) -> - SrcDirs = find_config_src(LibDir, ["src"]), + {_, SrcDirs} = find_config_src(LibDir, ["src"]), app_dirs(LibDir, SrcDirs) end, LibDirs). @@ -278,8 +277,9 @@ find_apps(LibDirs, SrcDirs, Validate) -> %% app info record. -spec find_app(file:filename_all(), valid | invalid | all) -> {true, rebar_app_info:t()} | false. find_app(AppDir, Validate) -> - SrcDirs = find_config_src(AppDir, ["src"]), - find_app(rebar_app_info:new(), AppDir, SrcDirs, Validate). + {Config, SrcDirs} = find_config_src(AppDir, ["src"]), + AppInfo = rebar_app_info:update_opts(rebar_app_info:new(), dict:new(), Config), + find_app_(AppInfo, AppDir, SrcDirs, Validate). %% @doc check that a given app in a directory is there, and whether it's %% valid or not based on the second argument. Returns the related @@ -291,7 +291,7 @@ find_app(AppInfo, AppDir, Validate) -> %% of src/ AppOpts = rebar_app_info:opts(AppInfo), SrcDirs = rebar_dir:src_dirs(AppOpts, ["src"]), - find_app(AppInfo, AppDir, SrcDirs, Validate). + find_app_(AppInfo, AppDir, SrcDirs, Validate). %% @doc check that a given app in a directory is there, and whether it's %% valid or not based on the second argument. The third argument includes @@ -301,6 +301,14 @@ find_app(AppInfo, AppDir, Validate) -> [file:filename_all()], valid | invalid | all) -> {true, rebar_app_info:t()} | false. find_app(AppInfo, AppDir, SrcDirs, Validate) -> + Config = rebar_config:consult(AppDir), + AppInfo1 = rebar_app_info:update_opts(AppInfo, rebar_app_info:opts(AppInfo), Config), + find_app_(AppInfo1, AppDir, SrcDirs, Validate). + +-spec find_app_(rebar_app_info:t(), file:filename_all(), + [file:filename_all()], valid | invalid | all) -> + {true, rebar_app_info:t()} | false. +find_app_(AppInfo, AppDir, SrcDirs, Validate) -> AppFile = filelib:wildcard(filename:join([AppDir, "ebin", "*.app"])), AppSrcFile = lists:append( [filelib:wildcard(filename:join([AppDir, SrcDir, "*.app.src"])) @@ -331,17 +339,14 @@ create_app_info(AppInfo, AppDir, AppFile) -> AppInfo2 = rebar_app_info:applications( rebar_app_info:app_details(AppInfo1, AppDetails), IncludedApplications++Applications), - C = rebar_config:consult(AppDir), - AppInfo3 = rebar_app_info:update_opts(AppInfo2, - rebar_app_info:opts(AppInfo2), C), - Valid = case rebar_app_utils:validate_application_info(AppInfo3) =:= true - andalso rebar_app_info:has_all_artifacts(AppInfo3) =:= true of + Valid = case rebar_app_utils:validate_application_info(AppInfo2) =:= true + andalso rebar_app_info:has_all_artifacts(AppInfo2) =:= true of true -> true; _ -> false end, - rebar_app_info:dir(rebar_app_info:valid(AppInfo3, Valid), AppDir). + rebar_app_info:dir(rebar_app_info:valid(AppInfo2, Valid), AppDir). %% @doc Read in and parse the .app file if it is availabe. Do the same for %% the .app.src file if it exists. @@ -403,12 +408,20 @@ try_handle_app_file(_AppInfo, Other, _AppDir, _AppSrcFile, _, _Validate) -> AppFile :: file:filename(), AppDir :: file:filename(), AppSrcFile :: file:filename(). -try_handle_app_src_file(_AppInfo, _, _AppDir, [], _Validate) -> - false; +try_handle_app_src_file(AppInfo, _, _AppDir, [], _Validate) -> + %% if .app and .app.src are not found check for a mix config file + %% it is assumed a plugin will build the application, including + %% a .app after this step + case filelib:is_file(filename:join(rebar_app_info:dir(AppInfo), "mix.exs")) of + true -> + {true, AppInfo}; + false -> + false + end; try_handle_app_src_file(_AppInfo, _, _AppDir, _AppSrcFile, valid) -> false; try_handle_app_src_file(AppInfo, _, AppDir, [File], Validate) when Validate =:= invalid - ; Validate =:= all -> + ; Validate =:= all -> AppInfo1 = rebar_app_info:app_file(AppInfo, undefined), AppInfo2 = create_app_info(AppInfo1, AppDir, File), case filename:extension(File) of @@ -437,8 +450,8 @@ to_atom(Bin) -> find_config_src(AppDir, Default) -> case rebar_config:consult(AppDir) of [] -> - Default; + {[], Default}; Terms -> %% TODO: handle profiles I guess, but we don't have that info - proplists:get_value(src_dirs, Terms, Default) + {Terms, proplists:get_value(src_dirs, Terms, Default)} end. diff --git a/src/rebar_app_info.erl b/src/rebar_app_info.erl index 88d6335..56ae4c0 100644 --- a/src/rebar_app_info.erl +++ b/src/rebar_app_info.erl @@ -7,6 +7,7 @@ new/4, new/5, update_opts/3, + update_opts_deps/2, discover/1, name/1, name/2, @@ -43,8 +44,6 @@ get/2, get/3, set/3, - resource_type/1, - resource_type/2, source/1, source/2, is_lock/1, @@ -53,6 +52,8 @@ is_checkout/2, valid/1, valid/2, + is_available/1, + is_available/2, verify_otp_vsn/1, has_all_artifacts/1, @@ -72,7 +73,7 @@ app_file_src :: file:filename_all() | undefined, app_file_src_script:: file:filename_all() | undefined, app_file :: file:filename_all() | undefined, - original_vsn :: binary() | string() | undefined, + original_vsn :: binary() | undefined, parent=root :: binary() | root, app_details=[] :: list(), applications=[] :: list(), @@ -83,11 +84,11 @@ dep_level=0 :: integer(), dir :: file:name(), out_dir :: file:name(), - resource_type :: pkg | src | undefined, source :: string() | tuple() | checkout | undefined, is_lock=false :: boolean(), is_checkout=false :: boolean(), - valid :: boolean() | undefined}). + valid :: boolean() | undefined, + is_available=false :: boolean()}). %%============================================================================ %% types @@ -114,14 +115,14 @@ new(AppName) -> {ok, t()}. new(AppName, Vsn) -> {ok, #app_info_t{name=rebar_utils:to_binary(AppName), - original_vsn=Vsn}}. + original_vsn=rebar_utils:to_binary(Vsn)}}. %% @doc build a complete version of the app info with all fields set. -spec new(atom() | binary() | string(), binary() | string(), file:name()) -> {ok, t()}. new(AppName, Vsn, Dir) -> {ok, #app_info_t{name=rebar_utils:to_binary(AppName), - original_vsn=Vsn, + original_vsn=rebar_utils:to_binary(Vsn), dir=rebar_utils:to_list(Dir), out_dir=rebar_utils:to_list(Dir)}}. @@ -130,7 +131,7 @@ new(AppName, Vsn, Dir) -> {ok, t()}. new(AppName, Vsn, Dir, Deps) -> {ok, #app_info_t{name=rebar_utils:to_binary(AppName), - original_vsn=Vsn, + original_vsn=rebar_utils:to_binary(Vsn), dir=rebar_utils:to_list(Dir), out_dir=rebar_utils:to_list(Dir), deps=Deps}}. @@ -141,7 +142,7 @@ new(AppName, Vsn, Dir, Deps) -> new(Parent, AppName, Vsn, Dir, Deps) -> {ok, #app_info_t{name=rebar_utils:to_binary(AppName), parent=Parent, - original_vsn=Vsn, + original_vsn=rebar_utils:to_binary(Vsn), dir=rebar_utils:to_list(Dir), out_dir=rebar_utils:to_list(Dir), deps=Deps}}. @@ -150,10 +151,12 @@ new(Parent, AppName, Vsn, Dir, Deps) -> %% file for the app -spec update_opts(t(), rebar_dict(), [any()]) -> t(). update_opts(AppInfo, Opts, Config) -> - LockDeps = case resource_type(AppInfo) of - pkg -> - Deps = deps(AppInfo), - [{{locks, default}, Deps}, {{deps, default}, Deps}]; + LockDeps = case source(AppInfo) of + Tuple when is_tuple(Tuple) andalso element(1, Tuple) =:= pkg -> + %% Deps are set separate for packages + %% instead of making it seem we have no deps + %% don't set anything here. + []; _ -> deps_from_config(dir(AppInfo), Config) end, @@ -165,8 +168,18 @@ update_opts(AppInfo, Opts, Config) -> NewOpts = rebar_opts:merge_opts(LocalOpts, Opts), - AppInfo#app_info_t{opts=NewOpts - ,default=NewOpts}. + AppInfo#app_info_t{opts=NewOpts, + default=NewOpts}. + +%% @doc update the opts based on new deps, usually from an app's hex registry metadata +-spec update_opts_deps(t(), [any()]) -> t(). +update_opts_deps(AppInfo=#app_info_t{opts=Opts}, Deps) -> + LocalOpts = dict:from_list([{{locks, default}, Deps}, {{deps, default}, Deps}]), + NewOpts = rebar_opts:merge_opts(LocalOpts, Opts), + AppInfo#app_info_t{opts=NewOpts, + default=NewOpts, + deps=Deps}. + %% @private extract the deps for an app in `Dir' based on its config file data -spec deps_from_config(file:filename(), [any()]) -> [{tuple(), any()}, ...]. @@ -350,15 +363,15 @@ parent(AppInfo=#app_info_t{}, Parent) -> %% @doc returns the original version of the app (unevaluated if %% asking for a semver) --spec original_vsn(t()) -> string(). +-spec original_vsn(t()) -> binary(). original_vsn(#app_info_t{original_vsn=Vsn}) -> Vsn. %% @doc stores the original version of the app (unevaluated if %% asking for a semver) --spec original_vsn(t(), string()) -> t(). +-spec original_vsn(t(), binary() | string()) -> t(). original_vsn(AppInfo=#app_info_t{}, Vsn) -> - AppInfo#app_info_t{original_vsn=Vsn}. + AppInfo#app_info_t{original_vsn=rebar_utils:to_binary(Vsn)}. %% @doc returns the list of applications the app depends on. -spec applications(t()) -> list(). @@ -438,16 +451,6 @@ ebin_dir(#app_info_t{out_dir=OutDir}) -> priv_dir(#app_info_t{out_dir=OutDir}) -> rebar_utils:to_list(filename:join(OutDir, "priv")). -%% @doc returns whether the app is source app or a package app. --spec resource_type(t()) -> pkg | src. -resource_type(#app_info_t{resource_type=ResourceType}) -> - ResourceType. - -%% @doc sets whether the app is source app or a package app. --spec resource_type(t(), pkg | src) -> t(). -resource_type(AppInfo=#app_info_t{}, Type) -> - AppInfo#app_info_t{resource_type=Type}. - %% @doc finds the source specification for the app -spec source(t()) -> string() | tuple(). source(#app_info_t{source=Source}) -> @@ -478,6 +481,17 @@ is_checkout(#app_info_t{is_checkout=IsCheckout}) -> is_checkout(AppInfo=#app_info_t{}, IsCheckout) -> AppInfo#app_info_t{is_checkout=IsCheckout}. +%% @doc returns whether the app source exists in the deps dir +-spec is_available(t()) -> boolean(). +is_available(#app_info_t{is_available=IsAvailable}) -> + IsAvailable. + +%% @doc sets whether the app's source is available +%% only set if the app's source is found in the expected dep directory +-spec is_available(t(), boolean()) -> t(). +is_available(AppInfo=#app_info_t{}, IsAvailable) -> + AppInfo#app_info_t{is_available=IsAvailable}. + %% @doc returns whether the app is valid (built) or not -spec valid(t()) -> boolean(). valid(AppInfo=#app_info_t{valid=undefined}) -> diff --git a/src/rebar_app_utils.erl b/src/rebar_app_utils.erl index 1d7ef5b..35e908c 100644 --- a/src/rebar_app_utils.erl +++ b/src/rebar_app_utils.erl @@ -217,26 +217,23 @@ parse_dep(_, Dep, _, _, _) -> dep_to_app(Parent, DepsDir, Name, Vsn, Source, IsLock, State) -> CheckoutsDir = rebar_utils:to_list(rebar_dir:checkouts_dir(State, Name)), AppInfo = case rebar_app_info:discover(CheckoutsDir) of - {ok, App} -> - rebar_app_info:source(rebar_app_info:is_checkout(App, true), checkout); - not_found -> - Dir = rebar_utils:to_list(filename:join(DepsDir, Name)), - {ok, AppInfo0} = - case rebar_app_info:discover(Dir) of - {ok, App} -> - {ok, rebar_app_info:parent(App, Parent)}; - not_found -> - rebar_app_info:new(Parent, Name, Vsn, Dir, []) - end, - rebar_app_info:source(AppInfo0, Source) - end, - C = rebar_config:consult(rebar_app_info:dir(AppInfo)), - AppInfo1 = rebar_app_info:update_opts(AppInfo, rebar_app_info:opts(AppInfo), C), - Overrides = rebar_state:get(State, overrides, []), - AppInfo2 = rebar_app_info:set(AppInfo1, overrides, rebar_app_info:get(AppInfo, overrides, [])++Overrides), - AppInfo3 = rebar_app_info:apply_overrides(rebar_app_info:get(AppInfo2, overrides, []), AppInfo2), - AppInfo4 = rebar_app_info:apply_profiles(AppInfo3, [default, prod]), - AppInfo5 = rebar_app_info:profiles(AppInfo4, [default]), + {ok, App} -> + rebar_app_info:source(rebar_app_info:is_checkout(App, true), checkout); + not_found -> + Dir = rebar_utils:to_list(filename:join(DepsDir, Name)), + {ok, AppInfo0} = + case rebar_app_info:discover(Dir) of + {ok, App} -> + {ok, rebar_app_info:is_available(rebar_app_info:parent(App, Parent), + true)}; + not_found -> + rebar_app_info:new(Parent, Name, Vsn, Dir, []) + end, + rebar_app_info:source(AppInfo0, Source) + end, + Overrides = rebar_app_info:get(AppInfo, overrides, []) ++ rebar_state:get(State, overrides, []), + AppInfo2 = rebar_app_info:set(AppInfo, overrides, Overrides), + AppInfo5 = rebar_app_info:profiles(AppInfo2, [default]), rebar_app_info:is_lock(AppInfo5, IsLock). %% @doc Takes a given application app_info record along with the project. @@ -250,52 +247,36 @@ expand_deps_sources(Dep, State) -> %% around version if required. -spec update_source(rebar_app_info:t(), Source, rebar_state:t()) -> rebar_app_info:t() when - Source :: tuple() | atom() | binary(). % TODO: meta to source() + Source :: rebar_resource_v2:source(). update_source(AppInfo, {pkg, PkgName, PkgVsn, Hash}, State) -> - {PkgName1, PkgVsn1} = case PkgVsn of - undefined -> - get_package(PkgName, "0", State); - <<"~>", Vsn/binary>> -> - [Vsn1] = [X || X <- binary:split(Vsn, [<<" ">>], [global]), X =/= <<>>], - get_package(PkgName, Vsn1, State); - _ -> - {PkgName, PkgVsn} - end, - %% store the expected hash for the dependency - Hash1 = case Hash of - undefined -> % unknown, define the hash since we know the dep - fetch_checksum(PkgName1, PkgVsn1, Hash, State); - _ -> % keep as is - Hash - end, - AppInfo1 = rebar_app_info:source(AppInfo, {pkg, PkgName1, PkgVsn1, Hash1}), - Deps = rebar_packages:deps(PkgName1 - ,PkgVsn1 - ,State), - AppInfo2 = rebar_app_info:resource_type(rebar_app_info:deps(AppInfo1, Deps), pkg), - rebar_app_info:original_vsn(AppInfo2, PkgVsn1); + case rebar_packages:resolve_version(PkgName, PkgVsn, Hash, + ?PACKAGE_TABLE, State) of + {ok, Package, RepoConfig} -> + #package{key={_, PkgVsn1, _}, + checksum=Hash1, + dependencies=Deps} = Package, + AppInfo1 = rebar_app_info:source(AppInfo, {pkg, PkgName, PkgVsn1, Hash1, RepoConfig}), + AppInfo2 = rebar_app_info:update_opts_deps(AppInfo1, Deps), + rebar_app_info:original_vsn(AppInfo2, PkgVsn1); + not_found -> + throw(?PRV_ERROR({missing_package, PkgName, PkgVsn})); + {error, {invalid_vsn, InvalidVsn}} -> + throw(?PRV_ERROR({invalid_vsn, PkgName, InvalidVsn})) + end; update_source(AppInfo, Source, _State) -> rebar_app_info:source(AppInfo, Source). -%% @doc grab the checksum for a given package --spec fetch_checksum(atom(), string(), iodata() | undefined, rebar_state:t()) -> - iodata() | no_return(). -fetch_checksum(PkgName, PkgVsn, Hash, State) -> - try - rebar_packages:registry_checksum({pkg, PkgName, PkgVsn, Hash}, State) - catch - _:_ -> - ?INFO("Package ~ts-~ts not found. Fetching registry updates and trying again...", [PkgName, PkgVsn]), - {ok, _} = rebar_prv_update:do(State), - rebar_packages:registry_checksum({pkg, PkgName, PkgVsn, Hash}, State) - end. - %% @doc convert a given exception's payload into an io description. -spec format_error(any()) -> iolist(). -format_error({missing_package, Package}) -> - io_lib:format("Package not found in registry: ~ts", [Package]); +format_error({missing_package, Name, undefined}) -> + io_lib:format("Package not found in any repo: ~ts.", [rebar_utils:to_binary(Name)]); +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({parse_dep, Dep}) -> io_lib:format("Failed parsing dep ~p", [Dep]); +format_error({invalid_vsn, Dep, InvalidVsn}) -> + io_lib:format("Dep ~ts has invalid version ~ts", [Dep, InvalidVsn]); format_error(Error) -> io_lib:format("~p", [Error]). @@ -303,18 +284,6 @@ format_error(Error) -> %% Internal functions %% =================================================================== -%% @private find the correct version of a package based on the version -%% and name passed in. --spec get_package(binary(), binary() | string(), rebar_state:t()) -> - term() | no_return(). -get_package(Dep, Vsn, State) -> - case rebar_packages:find_highest_matching(Dep, Vsn, ?PACKAGE_TABLE, State) of - {ok, HighestDepVsn} -> - {Dep, HighestDepVsn}; - none -> - throw(?PRV_ERROR({missing_package, rebar_utils:to_binary(Dep)})) - end. - %% @private checks that all the beam files have been properly %% created. -spec has_all_beams(file:filename_all(), [module()]) -> diff --git a/src/rebar_config.erl b/src/rebar_config.erl index 797dddc..2651ca1 100644 --- a/src/rebar_config.erl +++ b/src/rebar_config.erl @@ -74,7 +74,7 @@ consult_lock_file(File) -> read_attrs(beta, Locks, []); [{Vsn, Locks}|Attrs] when is_list(Locks) -> % versioned lock file %% Because this is the first version of rebar3 to introduce a lock - %% file, all versionned lock files with a different versions have + %% file, all versioned lock files with a different version have %% to be newer. case Vsn of ?CONFIG_VERSION -> diff --git a/src/rebar_fetch.erl b/src/rebar_fetch.erl index d2c7706..9c76e0e 100644 --- a/src/rebar_fetch.erl +++ b/src/rebar_fetch.erl @@ -7,104 +7,74 @@ %% ------------------------------------------------------------------- -module(rebar_fetch). --export([lock_source/3, - download_source/3, - needs_update/3]). +-export([lock_source/2, + download_source/2, + needs_update/2]). -export([format_error/1]). -include("rebar.hrl"). -include_lib("providers/include/providers.hrl"). --spec lock_source(file:filename_all(), rebar_resource:resource(), rebar_state:t()) -> - rebar_resource:resource() | {error, string()}. -lock_source(AppDir, Source, State) -> - Resources = rebar_state:resources(State), - Module = get_resource_type(Source, Resources), - Module:lock(AppDir, Source). +-spec lock_source(rebar_app_info:t(), rebar_state:t()) + -> rebar_resource_v2:source() | {error, string()}. +lock_source(AppInfo, State) -> + rebar_resource_v2:lock(AppInfo, State). --spec download_source(file:filename_all(), rebar_resource:resource(), rebar_state:t()) -> - true | {error, any()}. -download_source(AppDir, Source, State) -> - try download_source_(AppDir, Source, State) of - true -> - true; - Error -> - throw(?PRV_ERROR(Error)) +-spec download_source(rebar_app_info:t(), rebar_state:t()) + -> rebar_app_info:t() | {error, any()}. +download_source(AppInfo, State) -> + AppDir = rebar_app_info:dir(AppInfo), + try download_source_(AppInfo, State) of + ok -> + %% freshly downloaded, update the app info opts to reflect the new config + Config = rebar_config:consult(AppDir), + AppInfo1 = rebar_app_info:update_opts(AppInfo, rebar_app_info:opts(AppInfo), Config), + case rebar_app_discover:find_app(AppInfo1, AppDir, all) of + {true, AppInfo2} -> + rebar_app_info:is_available(AppInfo2, true); + false -> + throw(?PRV_ERROR({dep_app_not_found, rebar_app_info:name(AppInfo1)})) + end; + {error, Reason} -> + throw(?PRV_ERROR(Reason)) catch + throw:{no_resource, Type, Location} -> + throw(?PRV_ERROR({no_resource, Location, Type})); ?WITH_STACKTRACE(C,T,S) ?DEBUG("rebar_fetch exception ~p ~p ~p", [C, T, S]), - throw(?PRV_ERROR({fetch_fail, Source})) + throw(?PRV_ERROR({fetch_fail, rebar_app_info:source(AppInfo)})) end. -download_source_(AppDir, Source, State) -> - Resources = rebar_state:resources(State), - Module = get_resource_type(Source, Resources), +download_source_(AppInfo, State) -> + AppDir = rebar_app_info:dir(AppInfo), TmpDir = ec_file:insecure_mkdtemp(), AppDir1 = rebar_utils:to_list(AppDir), - case Module:download(TmpDir, Source, State) of - {ok, _} -> + case rebar_resource_v2:download(TmpDir, AppInfo, State) of + ok -> ec_file:mkdir_p(AppDir1), code:del_path(filename:absname(filename:join(AppDir1, "ebin"))), ok = rebar_file_utils:rm_rf(filename:absname(AppDir1)), ?DEBUG("Moving checkout ~p to ~p", [TmpDir, filename:absname(AppDir1)]), - ok = rebar_file_utils:mv(TmpDir, filename:absname(AppDir1)), - true; + rebar_file_utils:mv(TmpDir, filename:absname(AppDir1)); Error -> Error end. --spec needs_update(file:filename_all(), rebar_resource:resource(), rebar_state:t()) -> boolean() | {error, string()}. -needs_update(AppDir, Source, State) -> - Resources = rebar_state:resources(State), - Module = get_resource_type(Source, Resources), +-spec needs_update(rebar_app_info:t(), rebar_state:t()) + -> boolean() | {error, string()}. +needs_update(AppInfo, State) -> try - Module:needs_update(AppDir, Source) + rebar_resource_v2:needs_update(AppInfo, State) catch _:_ -> true end. -format_error({bad_download, CachePath}) -> - io_lib:format("Download of package does not match md5sum from server: ~ts", [CachePath]); -format_error({unexpected_hash, CachePath, Expected, Found}) -> - io_lib:format("The checksum for package at ~ts (~ts) does not match the " - "checksum previously locked (~ts). Either unlock or " - "upgrade the package, or make sure you fetched it from " - "the same index from which it was initially fetched.", - [CachePath, Found, Expected]); -format_error({failed_extract, CachePath}) -> - io_lib:format("Failed to extract package: ~ts", [CachePath]); -format_error({bad_etag, Source}) -> - io_lib:format("MD5 Checksum comparison failed for: ~ts", [Source]); format_error({fetch_fail, Name, Vsn}) -> io_lib:format("Failed to fetch and copy dep: ~ts-~ts", [Name, Vsn]); format_error({fetch_fail, Source}) -> io_lib:format("Failed to fetch and copy dep: ~p", [Source]); -format_error({bad_checksum, File}) -> - io_lib:format("Checksum mismatch against tarball in ~ts", [File]); -format_error({bad_registry_checksum, File}) -> - io_lib:format("Checksum mismatch against registry in ~ts", [File]). - -get_resource_type({Type, Location}, Resources) -> - find_resource_module(Type, Location, Resources); -get_resource_type({Type, Location, _}, Resources) -> - find_resource_module(Type, Location, Resources); -get_resource_type({Type, _, _, Location}, Resources) -> - find_resource_module(Type, Location, Resources); -get_resource_type(_, _) -> - rebar_pkg_resource. - -find_resource_module(Type, Location, Resources) -> - case lists:keyfind(Type, 1, Resources) of - false -> - case code:which(Type) of - non_existing -> - {error, io_lib:format("Cannot handle dependency ~ts.~n" - " No module for resource type ~p", [Location, Type])}; - _ -> - Type - end; - {Type, Module} -> - Module - end. +format_error({dep_app_not_found, AppName}) -> + io_lib:format("Dependency failure: source for ~ts does not contain a " + "recognizable project and can not be built", [AppName]). diff --git a/src/rebar_git_resource.erl b/src/rebar_git_resource.erl index 3aa875f..29c9ad7 100644 --- a/src/rebar_git_resource.erl +++ b/src/rebar_git_resource.erl @@ -2,21 +2,30 @@ %% ex: ts=4 sw=4 et -module(rebar_git_resource). --behaviour(rebar_resource). +-behaviour(rebar_resource_v2). --export([lock/2 - ,download/3 - ,needs_update/2 - ,make_vsn/1]). +-export([init/2, + lock/2, + download/4, + needs_update/2, + make_vsn/2]). -include("rebar.hrl"). %% Regex used for parsing scp style remote url -define(SCP_PATTERN, "\\A(?<username>[^@]+)@(?<host>[^:]+):(?<path>.+)\\z"). -lock(AppDir, {git, Url, _}) -> - lock(AppDir, {git, Url}); -lock(AppDir, {git, Url}) -> +-spec init(atom(), rebar_state:t()) -> {ok, rebar_resource_v2:resource()}. +init(Type, _State) -> + Resource = rebar_resource_v2:new(Type, ?MODULE, #{}), + {ok, Resource}. + +lock(AppInfo, _) -> + lock_(rebar_app_info:dir(AppInfo), rebar_app_info:source(AppInfo)). + +lock_(AppDir, {git, Url, _}) -> + lock_(AppDir, {git, Url}); +lock_(AppDir, {git, Url}) -> AbortMsg = lists:flatten(io_lib:format("Locking of git dependency failed in ~ts", [AppDir])), Dir = rebar_utils:escape_double_quotes(AppDir), {ok, VsnString} = @@ -33,14 +42,17 @@ lock(AppDir, {git, Url}) -> %% Return true if either the git url or tag/branch/ref is not the same as the currently %% checked out git repo for the dep -needs_update(Dir, {git, Url, {tag, Tag}}) -> +needs_update(AppInfo, _) -> + needs_update_(rebar_app_info:dir(AppInfo), rebar_app_info:source(AppInfo)). + +needs_update_(Dir, {git, Url, {tag, Tag}}) -> {ok, Current} = rebar_utils:sh(?FMT("git describe --tags --exact-match", []), [{cd, Dir}]), Current1 = rebar_string:trim(rebar_string:trim(Current, both, "\n"), both, "\r"), ?DEBUG("Comparing git tag ~ts with ~ts", [Tag, Current1]), not ((Current1 =:= Tag) andalso compare_url(Dir, Url)); -needs_update(Dir, {git, Url, {branch, Branch}}) -> +needs_update_(Dir, {git, Url, {branch, Branch}}) -> %% Fetch remote so we can check if the branch has changed SafeBranch = rebar_utils:escape_chars(Branch), {ok, _} = rebar_utils:sh(?FMT("git fetch origin ~ts", [SafeBranch]), @@ -50,9 +62,9 @@ needs_update(Dir, {git, Url, {branch, Branch}}) -> [{cd, Dir}]), ?DEBUG("Checking git branch ~ts for updates", [Branch]), not ((Current =:= []) andalso compare_url(Dir, Url)); -needs_update(Dir, {git, Url, "master"}) -> - needs_update(Dir, {git, Url, {branch, "master"}}); -needs_update(Dir, {git, _, Ref}) -> +needs_update_(Dir, {git, Url, "master"}) -> + needs_update_(Dir, {git, Url, {branch, "master"}}); +needs_update_(Dir, {git, _, Ref}) -> {ok, Current} = rebar_utils:sh(?FMT("git rev-parse --short=7 -q HEAD", []), [{cd, Dir}]), Current1 = rebar_string:trim(rebar_string:trim(Current, both, "\n"), @@ -98,25 +110,35 @@ parse_git_url(not_scp, Url) -> {error, Reason} end. -download(Dir, {git, Url}, State) -> +download(TmpDir, AppInfo, State, _) -> + case download_(TmpDir, rebar_app_info:source(AppInfo), State) of + {ok, _} -> + ok; + {error, Reason} -> + {error, Reason}; + Error -> + {error, Error} + end. + +download_(Dir, {git, Url}, State) -> ?WARN("WARNING: It is recommended to use {branch, Name}, {tag, Tag} or {ref, Ref}, otherwise updating the dep may not work as expected.", []), - download(Dir, {git, Url, {branch, "master"}}, State); -download(Dir, {git, Url, ""}, State) -> + download_(Dir, {git, Url, {branch, "master"}}, State); +download_(Dir, {git, Url, ""}, State) -> ?WARN("WARNING: It is recommended to use {branch, Name}, {tag, Tag} or {ref, Ref}, otherwise updating the dep may not work as expected.", []), - download(Dir, {git, Url, {branch, "master"}}, State); -download(Dir, {git, Url, {branch, Branch}}, _State) -> + download_(Dir, {git, Url, {branch, "master"}}, State); +download_(Dir, {git, Url, {branch, Branch}}, _State) -> ok = filelib:ensure_dir(Dir), maybe_warn_local_url(Url), git_clone(branch, git_vsn(), Url, Dir, Branch); -download(Dir, {git, Url, {tag, Tag}}, _State) -> +download_(Dir, {git, Url, {tag, Tag}}, _State) -> ok = filelib:ensure_dir(Dir), maybe_warn_local_url(Url), git_clone(tag, git_vsn(), Url, Dir, Tag); -download(Dir, {git, Url, {ref, Ref}}, _State) -> +download_(Dir, {git, Url, {ref, Ref}}, _State) -> ok = filelib:ensure_dir(Dir), maybe_warn_local_url(Url), git_clone(ref, git_vsn(), Url, Dir, Ref); -download(Dir, {git, Url, Rev}, _State) -> +download_(Dir, {git, Url, Rev}, _State) -> ?WARN("WARNING: It is recommended to use {branch, Name}, {tag, Tag} or {ref, Ref}, otherwise updating the dep may not work as expected.", []), ok = filelib:ensure_dir(Dir), maybe_warn_local_url(Url), @@ -195,7 +217,10 @@ git_vsn_fetch() -> undefined end. -make_vsn(Dir) -> +make_vsn(AppInfo, _) -> + make_vsn_(rebar_app_info:dir(AppInfo)). + +make_vsn_(Dir) -> case collect_default_refcount(Dir) of Vsn={plain, _} -> Vsn; diff --git a/src/rebar_hex_repos.erl b/src/rebar_hex_repos.erl new file mode 100644 index 0000000..ebee191 --- /dev/null +++ b/src/rebar_hex_repos.erl @@ -0,0 +1,142 @@ +-module(rebar_hex_repos). + +-export([from_state/2, + get_repo_config/2, + auth_config/1, + update_auth_config/2, + format_error/1]). + +-ifdef(TEST). +%% exported for test purposes +-export([repos/1, merge_repos/1]). +-endif. + +-include("rebar.hrl"). +-include_lib("providers/include/providers.hrl"). + +-export_type([repo/0]). + +-type repo() :: #{name => unicode:unicode_binary(), + api_url => binary(), + api_key => binary(), + repo_url => binary(), + repo_public_key => binary(), + repo_verify => binary()}. + +from_state(BaseConfig, State) -> + HexConfig = rebar_state:get(State, hex, []), + Repos = repos(HexConfig), + %% auth is stored in a separate config file since the plugin generates and modifies it + Auth = ?MODULE:auth_config(State), + %% add base config entries that are specific to use by rebar3 and not overridable + Repos1 = merge_with_base_and_auth(Repos, BaseConfig, Auth), + %% merge organizations parent repo options into each oraganization repo + update_organizations(Repos1). + +-spec get_repo_config(unicode:unicode_binary(), rebar_state:t() | [repo()]) + -> {ok, repo()} | error. +get_repo_config(RepoName, Repos) when is_list(Repos) -> + case ec_lists:find(fun(#{name := N}) -> N =:= RepoName end, Repos) of + error -> + throw(?PRV_ERROR({repo_not_found, RepoName})); + {ok, RepoConfig} -> + {ok, RepoConfig} + end; +get_repo_config(RepoName, State) -> + Resources = rebar_state:resources(State), + #{repos := Repos} = rebar_resource_v2:find_resource_state(pkg, Resources), + get_repo_config(RepoName, Repos). + +merge_with_base_and_auth(Repos, BaseConfig, Auth) -> + [maps:merge(maps:get(maps:get(name, Repo), Auth, #{}), + maps:merge(Repo, BaseConfig)) || Repo <- Repos]. + +%% A user's list of repos are merged by name while keeping the order +%% intact. The order is based on the first use of a repo by name in the +%% list. The default repo is appended to the user's list. +repos(HexConfig) -> + HexDefaultConfig = default_repo(), + case [R || R <- HexConfig, element(1, R) =:= repos] of + [] -> + [HexDefaultConfig]; + %% we only care if the first element is a replace entry + [{repos, replace, Repos} | _]-> + merge_repos(Repos); + Repos -> + RepoList = repo_list(Repos), + merge_repos(RepoList ++ [HexDefaultConfig]) + end. + +-spec merge_repos([repo()]) -> [repo()]. +merge_repos(Repos) -> + lists:foldl(fun(R=#{name := Name}, ReposAcc) -> + %% private organizations include the parent repo before a : + case rebar_string:split(Name, <<":">>) of + [Repo, Org] -> + update_repo_list(R#{name => Name, + organization => Org, + parent => Repo}, ReposAcc); + _ -> + update_repo_list(R, ReposAcc) + end + end, [], Repos). + +update_organizations(Repos) -> + lists:map(fun(Repo=#{organization := Organization, + parent := ParentName}) -> + {ok, Parent} = get_repo_config(ParentName, Repos), + ParentRepoUrl = rebar_utils:to_list(maps:get(repo_url, Parent)), + {ok, RepoUrl} = + rebar_utils:url_append_path(ParentRepoUrl, + filename:join("repos", rebar_utils:to_list(Organization))), + %% still let the organization config override this constructed repo url + maps:merge(Parent#{repo_url => rebar_utils:to_binary(RepoUrl)}, Repo); + (Repo) -> + Repo + end, Repos). + +update_repo_list(R=#{name := N}, [H=#{name := HN} | Rest]) when N =:= HN -> + [maps:merge(R, H) | Rest]; +update_repo_list(R, [H | Rest]) -> + [H | update_repo_list(R, Rest)]; +update_repo_list(R, []) -> + [R]. + +default_repo() -> + HexDefaultConfig = hex_core:default_config(), + HexDefaultConfig#{name => ?PUBLIC_HEX_REPO}. + +repo_list([]) -> + []; +repo_list([{repos, Repos} | T]) -> + Repos ++ repo_list(T); +repo_list([{repos, replace, Repos} | T]) -> + Repos ++ repo_list(T). + +format_error({repo_not_found, RepoName}) -> + io_lib:format("The repo ~ts was not found in the configuration.", [RepoName]). + +%% auth functions + +%% authentication is in a separate config file because the hex plugin updates it + +-spec auth_config_file(rebar_state:t()) -> file:filename_all(). +auth_config_file(State) -> + filename:join(rebar_dir:global_config_dir(State), ?HEX_AUTH_FILE). + +-spec auth_config(rebar_state:t()) -> map(). +auth_config(State) -> + case file:consult(auth_config_file(State)) of + {ok, [Config]} -> + Config; + _ -> + #{} + end. + +-spec update_auth_config(map(), rebar_state:t()) -> ok. +update_auth_config(Updates, State) -> + Config = auth_config(State), + AuthConfigFile = auth_config_file(State), + ok = filelib:ensure_dir(AuthConfigFile), + NewConfig = iolist_to_binary([io_lib:print(maps:merge(Config, Updates)) | ".\n"]), + ok = file:write_file(AuthConfigFile, NewConfig). diff --git a/src/rebar_hg_resource.erl b/src/rebar_hg_resource.erl index abcca88..21d4b9d 100644 --- a/src/rebar_hg_resource.erl +++ b/src/rebar_hg_resource.erl @@ -2,39 +2,51 @@ %% ex: ts=4 sw=4 et -module(rebar_hg_resource). --behaviour(rebar_resource). +-behaviour(rebar_resource_v2). --export([lock/2 - ,download/3 - ,needs_update/2 - ,make_vsn/1]). +-export([init/2, + lock/2, + download/4, + needs_update/2, + make_vsn/2]). -include("rebar.hrl"). -lock(AppDir, {hg, Url, _}) -> - lock(AppDir, {hg, Url}); -lock(AppDir, {hg, Url}) -> +-spec init(atom(), rebar_state:t()) -> {ok, rebar_resource_v2:resource()}. +init(Type, _State) -> + Resource = rebar_resource_v2:new(Type, ?MODULE, #{}), + {ok, Resource}. + +lock(AppInfo, _) -> + lock_(rebar_app_info:dir(AppInfo), rebar_app_info:source(AppInfo)). + +lock_(AppDir, {hg, Url, _}) -> + lock_(AppDir, {hg, Url}); +lock_(AppDir, {hg, Url}) -> Ref = get_ref(AppDir), {hg, Url, {ref, Ref}}. %% Return `true' if either the hg url or tag/branch/ref is not the same as %% the currently checked out repo for the dep -needs_update(Dir, {hg, Url, {tag, Tag}}) -> +needs_update(AppInfo, _) -> + needs_update_(rebar_app_info:dir(AppInfo), rebar_app_info:source(AppInfo)). + +needs_update_(Dir, {hg, Url, {tag, Tag}}) -> Ref = get_ref(Dir), {ClosestTag, Distance} = get_tag_distance(Dir, Ref), ?DEBUG("Comparing hg tag ~ts with ref ~ts (closest tag is ~ts at distance ~ts)", [Tag, Ref, ClosestTag, Distance]), not ((Distance =:= "0") andalso (Tag =:= ClosestTag) andalso compare_url(Dir, Url)); -needs_update(Dir, {hg, Url, {branch, Branch}}) -> +needs_update_(Dir, {hg, Url, {branch, Branch}}) -> Ref = get_ref(Dir), BRef = get_branch_ref(Dir, Branch), not ((Ref =:= BRef) andalso compare_url(Dir, Url)); -needs_update(Dir, {hg, Url, "default"}) -> +needs_update_(Dir, {hg, Url, "default"}) -> Ref = get_ref(Dir), BRef = get_branch_ref(Dir, "default"), not ((Ref =:= BRef) andalso compare_url(Dir, Url)); -needs_update(Dir, {hg, Url, Ref}) -> +needs_update_(Dir, {hg, Url, Ref}) -> LocalRef = get_ref(Dir), TargetRef = case Ref of {ref, Ref1} -> @@ -48,13 +60,23 @@ needs_update(Dir, {hg, Url, Ref}) -> ?DEBUG("Comparing hg ref ~ts with ~ts", [Ref1, LocalRef]), not ((LocalRef =:= TargetRef) andalso compare_url(Dir, Url)). -download(Dir, {hg, Url}, State) -> +download(TmpDir, AppInfo, State, _) -> + case download_(TmpDir, rebar_app_info:source(AppInfo), State) of + {ok, _} -> + ok; + {error, Reason} -> + {error, Reason}; + Error -> + {error, Error} + end. + +download_(Dir, {hg, Url}, State) -> ?WARN("WARNING: It is recommended to use {branch, Name}, {tag, Tag} or {ref, Ref}, otherwise updating the dep may not work as expected.", []), - download(Dir, {hg, Url, {branch, "default"}}, State); -download(Dir, {hg, Url, ""}, State) -> + download_(Dir, {hg, Url, {branch, "default"}}, State); +download_(Dir, {hg, Url, ""}, State) -> ?WARN("WARNING: It is recommended to use {branch, Name}, {tag, Tag} or {ref, Ref}, otherwise updating the dep may not work as expected.", []), - download(Dir, {hg, Url, {branch, "default"}}, State); -download(Dir, {hg, Url, {branch, Branch}}, _State) -> + download_(Dir, {hg, Url, {branch, "default"}}, State); +download_(Dir, {hg, Url, {branch, Branch}}, _State) -> ok = filelib:ensure_dir(Dir), maybe_warn_local_url(Url), rebar_utils:sh(?FMT("hg clone -q -b ~ts ~ts ~ts", @@ -62,7 +84,7 @@ download(Dir, {hg, Url, {branch, Branch}}, _State) -> rebar_utils:escape_chars(Url), rebar_utils:escape_chars(filename:basename(Dir))]), [{cd, filename:dirname(Dir)}]); -download(Dir, {hg, Url, {tag, Tag}}, _State) -> +download_(Dir, {hg, Url, {tag, Tag}}, _State) -> ok = filelib:ensure_dir(Dir), maybe_warn_local_url(Url), rebar_utils:sh(?FMT("hg clone -q -u ~ts ~ts ~ts", @@ -70,7 +92,7 @@ download(Dir, {hg, Url, {tag, Tag}}, _State) -> rebar_utils:escape_chars(Url), rebar_utils:escape_chars(filename:basename(Dir))]), [{cd, filename:dirname(Dir)}]); -download(Dir, {hg, Url, {ref, Ref}}, _State) -> +download_(Dir, {hg, Url, {ref, Ref}}, _State) -> ok = filelib:ensure_dir(Dir), maybe_warn_local_url(Url), rebar_utils:sh(?FMT("hg clone -q -r ~ts ~ts ~ts", @@ -78,7 +100,7 @@ download(Dir, {hg, Url, {ref, Ref}}, _State) -> rebar_utils:escape_chars(Url), rebar_utils:escape_chars(filename:basename(Dir))]), [{cd, filename:dirname(Dir)}]); -download(Dir, {hg, Url, Rev}, _State) -> +download_(Dir, {hg, Url, Rev}, _State) -> ok = filelib:ensure_dir(Dir), maybe_warn_local_url(Url), rebar_utils:sh(?FMT("hg clone -q -r ~ts ~ts ~ts", @@ -87,7 +109,10 @@ download(Dir, {hg, Url, Rev}, _State) -> rebar_utils:escape_chars(filename:basename(Dir))]), [{cd, filename:dirname(Dir)}]). -make_vsn(Dir) -> +make_vsn(AppInfo, _) -> + make_vsn_(rebar_app_info:dir(AppInfo)). + +make_vsn_(Dir) -> BaseHg = "hg -R \"" ++ rebar_utils:escape_double_quotes(Dir) ++ "\" ", Ref = get_ref(Dir), Cmd = BaseHg ++ "log --template \"{latesttag}+build.{latesttagdistance}.rev.{node|short}\"" diff --git a/src/rebar_otp_app.erl b/src/rebar_otp_app.erl index f5bb9cf..1d854da 100644 --- a/src/rebar_otp_app.erl +++ b/src/rebar_otp_app.erl @@ -59,8 +59,8 @@ compile(State, App) -> format_error({missing_app_file, Filename}) -> io_lib:format("App file is missing: ~ts", [Filename]); -format_error({file_read, File, Reason}) -> - io_lib:format("Failed to read required file ~ts for processing: ~ts", [File, file:format_error(Reason)]); +format_error({file_read, AppName, File, Reason}) -> + io_lib:format("Failed to read required ~ts file for processing the application '~ts': ~ts", [File, AppName, file:format_error(Reason)]); format_error({invalid_name, File, AppName}) -> io_lib:format("Invalid ~ts: name of application (~p) must match filename.", [File, AppName]). @@ -79,7 +79,7 @@ validate_app(State, App) -> Error end; {error, Reason} -> - ?PRV_ERROR({file_read, AppFile, Reason}) + ?PRV_ERROR({file_read, rebar_app_info:name(App), ".app", Reason}) end. validate_app_modules(State, App, AppData) -> @@ -110,7 +110,7 @@ preprocess(State, AppInfo, AppSrcFile) -> A1 = apply_app_vars(AppVars, AppData), %% AppSrcFile may contain instructions for generating a vsn number - Vsn = app_vsn(AppData, AppSrcFile, State), + Vsn = app_vsn(AppInfo, AppData, AppSrcFile, State), A2 = lists:keystore(vsn, 1, A1, {vsn, Vsn}), %% systools:make_relup/4 fails with {missing_param, registered} @@ -131,7 +131,7 @@ preprocess(State, AppInfo, AppSrcFile) -> AppFile; {error, Reason} -> - throw(?PRV_ERROR({file_read, AppSrcFile, Reason})) + throw(?PRV_ERROR({file_read, rebar_app_info:name(AppInfo), ".app.src", Reason})) end. load_app_vars(State) -> @@ -226,10 +226,8 @@ consult_app_file(Filename) -> end end. -app_vsn(AppData, AppFile, State) -> - AppDir = filename:dirname(filename:dirname(AppFile)), - Resources = rebar_state:resources(State), - rebar_utils:vcs_vsn(get_value(vsn, AppData, AppFile), AppDir, Resources). +app_vsn(AppInfo, AppData, AppFile, State) -> + rebar_utils:vcs_vsn(AppInfo, get_value(vsn, AppData, AppFile), State). get_value(Key, AppInfo, AppFile) -> case proplists:get_value(Key, AppInfo) of diff --git a/src/rebar_packages.erl b/src/rebar_packages.erl index 8cebeca..8a3ffea 100644 --- a/src/rebar_packages.erl +++ b/src/rebar_packages.erl @@ -1,18 +1,18 @@ -module(rebar_packages). --export([packages/1 - ,close_packages/0 - ,load_and_verify_version/1 - ,deps/3 +-export([get/2 + ,get_all_names/1 ,registry_dir/1 - ,package_dir/1 - ,registry_checksum/2 - ,find_highest_matching/6 - ,find_highest_matching/4 - ,find_highest_matching_/6 - ,find_all/3 + ,package_dir/2 + ,find_highest_matching/5 ,verify_table/1 - ,format_error/1]). + ,format_error/1 + ,update_package/3 + ,resolve_version/5]). + +-ifdef(TEST). +-export([new_package_table/0, find_highest_matching_/5, cmp_/4, cmpl_/4, valid_vsn/1]). +-endif. -export_type([package/0]). @@ -23,119 +23,133 @@ -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) +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 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. -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]) + +-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(), unicode:unicode_binary(), ets:tid(), rebar_state:t()) + -> [vsn()]. +get_package_versions(Dep, Repo, Table, State) -> + ?MODULE:verify_table(State), + ets:select(Table, [{#package{key={Dep,'$1', Repo}, + _='_'}, + [], ['$1']}]). + + +get_package(Dep, Vsn, Hash, Repo, Table, State) -> + get_package(Dep, Vsn, Hash, false, [Repo], Table, State). + +-spec get_package(unicode:unicode_binary(), unicode:unicode_binary(), + binary() | undefined | '_', boolean() | '_', + unicode:unicode_binary() | '_' | list(), ets:tab(), rebar_state:t()) + -> {ok, #package{}} | not_found. +get_package(Dep, Vsn, undefined, Retired, Repo, Table, State) -> + get_package(Dep, Vsn, '_', Retired, Repo, Table, State); +get_package(Dep, Vsn, Hash, Retired, Repos, Table, State) -> + ?MODULE:verify_table(State), + case ets:select(Table, [{#package{key={Dep, Vsn, Repo}, + checksum=Hash, + retired=Retired, + _='_'}, [], ['$_']} || Repo <- Repos]) of + %% have to allow multiple matches in the list for cases that Repo is `_` + [Package | _] -> + {ok, Package}; + _ -> + not_found end. -close_packages() -> - catch ets:delete(?PACKAGE_TABLE). +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, 2) of + 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)), - rebar_prv_update:hex_to_index(State) + new_package_table() end; - _ -> - rebar_prv_update:hex_to_index(State) + _ -> + new_package_table() 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, {rebar_utils:to_binary(Name), rebar_utils:to_binary(Vsn)}, 2). - -handle_missing_package(Dep, State, Fun) -> - case Dep of - {Name, Vsn} -> - ?INFO("Package ~ts-~ts not found. Fetching registry updates and trying again...", [Name, Vsn]); - _ -> - ?INFO("Package ~p not found. Fetching registry updates and trying again...", [Dep]) - 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, - {ok, State1} = rebar_prv_update:do(State), - try - Fun(State1) + 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, Dep})) + 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. + 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}. -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. +-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}. -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, rebar_utils:to_binary(Name), rebar_utils: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 @@ -152,31 +166,28 @@ registry_checksum({pkg, Name, Vsn, _Hash}, State) -> %% `~> 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 +find_highest_matching(Dep, Constraint, Repo, Table, State) -> + try find_highest_matching_(Dep, Constraint, Repo, Table, State) of none -> - handle_missing_package(Dep, State, + handle_missing_package(Dep, Repo, State, fun(State1) -> - find_highest_matching_(Pkg, PkgVsn, Dep, Constraint, Table, State1) + find_highest_matching_(Dep, Constraint, Repo, Table, State1) end); Result -> Result catch _:_ -> - handle_missing_package(Dep, State, + handle_missing_package(Dep, Repo, State, fun(State1) -> - find_highest_matching_(Pkg, PkgVsn, Dep, Constraint, Table, State1) + find_highest_matching_(Dep, Constraint, Repo, 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, Vsns} -> +find_highest_matching_(Dep, Constraint, #{name := Repo}, Table, State) -> + try get_package_versions(Dep, Repo, Table, State) of + [Vsn] -> + handle_single_vsn(Vsn, Constraint); + Vsns -> case handle_vsns(Constraint, Vsns) of none -> none; @@ -188,18 +199,6 @@ find_highest_matching_(Pkg, PkgVsn, Dep, Constraint, Table, State) -> 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, Vsns) -> lists:foldl(fun(Version, Highest) -> case ec_semver:pes(Version, Constraint) andalso @@ -211,26 +210,227 @@ handle_vsns(Constraint, Vsns) -> end end, none, Vsns). -handle_single_vsn(Pkg, PkgVsn, Dep, Vsn, Constraint) -> +handle_single_vsn(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} + none end. -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, 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). + +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, RepoConfig=#{name := Repo}, State) -> + ?MODULE:verify_table(State), + try hex_repo:get_package(RepoConfig#{repo_key => maps:get(read_key, RepoConfig, <<>>)}, Name) of + {ok, {200, _Headers, #{releases := Releases}}} -> + _ = insert_releases(Name, Releases, Repo, ?PACKAGE_TABLE), + {ok, RegistryDir} = rebar_packages:registry_dir(State), + PackageIndex = filename:join(RegistryDir, ?INDEX_FILE), + ok = ets:tab2file(?PACKAGE_TABLE, PackageIndex); + {ok, {403, _Headers, <<>>}} -> + not_found; + {ok, {404, _Headers, _}} -> + not_found; + Error -> + ?DEBUG("Hex get_package request failed: ~p", [Error]), + %% TODO: add better log message. hex_core should export a format_error + ?WARN("Failed to update package from repo ~ts", [Repo]), + fail + catch + _:Exception -> + ?DEBUG("hex_repo:get_package failed for package ~p: ~p", [Name, Exception]), + fail + end. + +insert_releases(Name, Releases, Repo, Table) -> + [true = ets:insert(Table, + #package{key={Name, Version, Repo}, + checksum=parse_checksum(Checksum), + retired=maps:get(retired, Release, false), + dependencies=parse_deps(Dependencies)}) + || Release=#{checksum := Checksum, + version := Version, + dependencies := Dependencies} <- Releases]. + +-spec resolve_version(unicode:unicode_binary(), unicode:unicode_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, 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, 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, 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>>) -> + 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, Repo, HexRegistry, State) -> + find_highest_matching_(Dep, Vsn, #{name => Repo}, HexRegistry, State). + +cmp(Dep, Vsn, Repo, HexRegistry, State, CmpFun) -> + case get_package_versions(Dep, 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, 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. diff --git a/src/rebar_pkg_resource.erl b/src/rebar_pkg_resource.erl index 2cf167e..97eabb6 100644 --- a/src/rebar_pkg_resource.erl +++ b/src/rebar_pkg_resource.erl @@ -2,42 +2,51 @@ %% ex: ts=4 sw=4 et -module(rebar_pkg_resource). --behaviour(rebar_resource). +-behaviour(rebar_resource_v2). --export([lock/2 - ,download/3 - ,download/4 - ,needs_update/2 - ,make_vsn/1]). +-export([init/2, + lock/2, + download/4, + download/5, + needs_update/2, + make_vsn/2, + format_error/1]). --export([request/2 - ,etag/1 - ,ssl_opts/1]). - -%% Exported for ct +-ifdef(TEST). +%% exported for test purposes -export([store_etag_in_cache/2]). +-endif. -include("rebar.hrl"). --include_lib("public_key/include/OTP-PUB-KEY.hrl"). - --type cached_result() :: {'bad_checksum',string()} | - {'bad_registry_checksum',string()} | - {'failed_extract',string()} | - {'ok','true'} | - {'unexpected_hash',string(),_,binary()}. +-include_lib("providers/include/providers.hrl"). --type download_result() :: {bad_download, binary() | string()} | - {fetch_fail, _, _} | cached_result(). +-type package() :: {pkg, binary(), binary(), binary(), rebar_hex_repos:repo()}. %%============================================================================== %% Public API %%============================================================================== --spec lock(AppDir, Source) -> Res when - AppDir :: file:name(), - Source :: tuple(), - Res :: {atom(), string(), any()}. -lock(_AppDir, Source) -> - Source. + +-spec init(atom(), rebar_state:t()) -> {ok, rebar_resource_v2:resource()}. +init(Type, State) -> + {ok, Vsn} = application:get_key(rebar, vsn), + BaseConfig = #{http_adapter => hex_http_httpc, + http_user_agent_fragment => + <<"(rebar3/", (list_to_binary(Vsn))/binary, ") (httpc)">>, + http_adapter_config => #{profile => rebar}}, + Repos = rebar_hex_repos:from_state(BaseConfig, State), + Resource = rebar_resource_v2:new(Type, ?MODULE, #{repos => Repos, + base_config => BaseConfig}), + {ok, Resource}. + + + +-spec lock(AppInfo, ResourceState) -> Res when + AppInfo :: rebar_app_info:t(), + ResourceState :: rebar_resource_v2:resource_state(), + Res :: {atom(), string(), any(), binary()}. +lock(AppInfo, _) -> + {pkg, Name, Vsn, Hash, _RepoConfig} = rebar_app_info:source(AppInfo), + {pkg, Name, Vsn, Hash}. %%------------------------------------------------------------------------------ %% @doc @@ -45,13 +54,13 @@ lock(_AppDir, Source) -> %% version. %% @end %%------------------------------------------------------------------------------ --spec needs_update(Dir, Pkg) -> Res when - Dir :: file:name(), - Pkg :: {pkg, Name :: binary(), Vsn :: binary(), Hash :: binary()}, +-spec needs_update(AppInfo, ResourceState) -> Res when + AppInfo :: rebar_app_info:t(), + ResourceState :: rebar_resource_v2:resource_state(), Res :: boolean(). -needs_update(Dir, {pkg, _Name, Vsn, _Hash}) -> - [AppInfo] = rebar_app_discover:find_apps([Dir], all), - case rebar_app_info:original_vsn(AppInfo) =:= rebar_utils:to_list(Vsn) of +needs_update(AppInfo, _) -> + {pkg, _Name, Vsn, _Hash, _} = rebar_app_info:source(AppInfo), + case rebar_app_info:original_vsn(AppInfo) =:= rebar_utils:to_binary(Vsn) of true -> false; false -> @@ -63,13 +72,19 @@ needs_update(Dir, {pkg, _Name, Vsn, _Hash}) -> %% Download the given pkg. %% @end %%------------------------------------------------------------------------------ --spec download(TmpDir, Pkg, State) -> Res when +-spec download(TmpDir, AppInfo, State, ResourceState) -> Res when TmpDir :: file:name(), - Pkg :: {pkg, Name :: binary(), Vsn :: binary(), Hash :: binary()}, + AppInfo :: rebar_app_info:t(), + ResourceState :: rebar_resource_v2:resource_state(), State :: rebar_state:t(), - Res :: {'error',_} | {'ok',_} | {'tarball',binary() | string()}. -download(TmpDir, Pkg, State) -> - download(TmpDir, Pkg, State, true). + Res :: ok | {error,_}. +download(TmpDir, AppInfo, State, ResourceState) -> + case download(TmpDir, rebar_app_info:source(AppInfo), State, ResourceState, true) of + ok -> + ok; + Error -> + {error, Error} + end. %%------------------------------------------------------------------------------ %% @doc @@ -78,26 +93,28 @@ download(TmpDir, Pkg, State) -> %% is different. %% @end %%------------------------------------------------------------------------------ --spec download(TmpDir, Pkg, State, UpdateETag) -> Res when +-spec download(TmpDir, Pkg, State, ResourceState, UpdateETag) -> Res when TmpDir :: file:name(), - Pkg :: {pkg, Name :: binary(), Vsn :: binary(), Hash :: binary()}, + Pkg :: package(), State :: rebar_state:t(), + ResourceState:: rebar_resource_v2:resource_state(), UpdateETag :: boolean(), - Res :: download_result(). -download(TmpDir, Pkg={pkg, Name, Vsn, _Hash}, State, UpdateETag) -> - CDN = rebar_state:get(State, rebar_packages_cdn, ?DEFAULT_CDN), - {ok, PackageDir} = rebar_packages:package_dir(State), + Res :: ok | {error,_} | {unexpected_hash, string(), integer(), integer()} | + {fetch_fail, binary(), binary()}. +download(TmpDir, Pkg={pkg, Name, Vsn, _Hash, Repo}, State, _ResourceState, UpdateETag) -> + {ok, PackageDir} = rebar_packages:package_dir(Repo, State), Package = binary_to_list(<<Name/binary, "-", Vsn/binary, ".tar">>), ETagFile = binary_to_list(<<Name/binary, "-", Vsn/binary, ".etag">>), CachePath = filename:join(PackageDir, Package), ETagPath = filename:join(PackageDir, ETagFile), - case rebar_utils:url_append_path(CDN, filename:join(?REMOTE_PACKAGE_DIR, - Package)) of - {ok, Url} -> - cached_download(TmpDir, CachePath, Pkg, Url, etag(ETagPath), State, - ETagPath, UpdateETag); - _ -> - {fetch_fail, Name, Vsn} + case cached_download(TmpDir, CachePath, Pkg, etag(CachePath, ETagPath), ETagPath, UpdateETag) of + {bad_registry_checksum, Expected, Found} -> + %% checksum comparison failed. in case this is from a modified cached package + %% overwrite the etag if it exists so it is not relied on again + store_etag_in_cache(ETagPath, <<>>), + ?PRV_ERROR({bad_registry_checksum, Name, Vsn, Expected, Found}); + Result -> + Result end. %%------------------------------------------------------------------------------ @@ -106,12 +123,19 @@ download(TmpDir, Pkg={pkg, Name, Vsn, _Hash}, State, UpdateETag) -> %% Returns {error, string()} as this operation is not supported for pkg sources. %% @end %%------------------------------------------------------------------------------ --spec make_vsn(Vsn) -> Res when - Vsn :: any(), - Res :: {'error',[1..255,...]}. -make_vsn(_) -> +-spec make_vsn(AppInfo, ResourceState) -> Res when + AppInfo :: rebar_app_info:t(), + ResourceState :: rebar_resource_v2:resource_state(), + Res :: {'error', string()}. +make_vsn(_, _) -> {error, "Replacing version of type pkg not supported."}. +format_error({bad_registry_checksum, Name, Vsn, Expected, Found}) -> + io_lib:format("The checksum for package at ~ts-~ts (~ts) does not match the " + "checksum expected from the registry (~ts). " + "Run `rebar3 do unlock ~ts, update` and then try again.", + [Name, Vsn, Found, Expected, Name]). + %%------------------------------------------------------------------------------ %% @doc %% Download the pkg belonging to the given address. If the etag of the pkg @@ -120,29 +144,24 @@ make_vsn(_) -> %% {ok, Contents, NewEtag}, otherwise if some error occured return error. %% @end %%------------------------------------------------------------------------------ --spec request(Url, ETag) -> Res when - Url :: string(), - ETag :: false | string(), - Res :: 'error' | {ok, cached} | {ok, any(), string()}. -request(Url, ETag) -> - HttpOptions = [{ssl, ssl_opts(Url)}, - {relaxed, true} | rebar_utils:get_proxy_auth()], - case httpc:request(get, {Url, [{"if-none-match", "\"" ++ ETag ++ "\""} - || ETag =/= false] ++ - [{"User-Agent", rebar_utils:user_agent()}]}, - HttpOptions, [{body_format, binary}], rebar) of - {ok, {{_Version, 200, _Reason}, Headers, Body}} -> - ?DEBUG("Successfully downloaded ~ts", [Url]), - {"etag", ETag1} = lists:keyfind("etag", 1, Headers), - {ok, Body, rebar_string:trim(ETag1, both, [$"])}; - {ok, {{_Version, 304, _Reason}, _Headers, _Body}} -> - ?DEBUG("Cached copy of ~ts still valid", [Url]), +-spec request(rebar_hex_repos:repo(), binary(), binary(), false | binary()) + -> {ok, cached} | {ok, binary(), binary()} | error. +request(Config, Name, Version, ETag) -> + Config1 = Config#{http_etag => ETag}, + try hex_repo:get_tarball(Config1, Name, Version) of + {ok, {200, #{<<"etag">> := ETag1}, Tarball}} -> + {ok, Tarball, ETag1}; + {ok, {304, _Headers, _}} -> {ok, cached}; - {ok, {{_Version, Code, _Reason}, _Headers, _Body}} -> - ?DEBUG("Request to ~p failed: status code ~p", [Url, Code]), + {ok, {Code, _Headers, _Body}} -> + ?DEBUG("Request for package ~s-~s failed: status code ~p", [Name, Version, Code]), error; {error, Reason} -> - ?DEBUG("Request to ~p failed: ~p", [Url, Reason]), + ?DEBUG("Request for package ~s-~s failed: ~p", [Name, Version, Reason]), + error + catch + _:Exception -> + ?DEBUG("hex_repo:get_tarball failed: ~p", [Exception]), error end. @@ -153,32 +172,23 @@ request(Url, ETag) -> %% returned from the hexpm server. The name is package-vsn.etag. %% @end %%------------------------------------------------------------------------------ --spec etag(Path) -> Res when - Path :: file:name(), - Res :: false | string(). -etag(Path) -> - case file:read_file(Path) of +-spec etag(PackagePath, ETagPath) -> Res when + PackagePath :: file:name(), + ETagPath :: file:name(), + Res :: binary(). +etag(PackagePath, ETagPath) -> + case file:read_file(ETagPath) of {ok, Bin} -> - binary_to_list(Bin); + %% just in case a user deleted a cached package but not its etag + %% verify the package is also there, and if not, ignore the etag + case filelib:is_file(PackagePath) of + true -> + Bin; + false -> + <<>> + end; {error, _} -> - false - end. - -%%------------------------------------------------------------------------------ -%% @doc -%% Return the SSL options adequate for the project based on -%% its configuration, including for validation of certs. -%% @end -%%------------------------------------------------------------------------------ --spec ssl_opts(Url) -> Res when - Url :: string(), - Res :: proplists:proplist(). -ssl_opts(Url) -> - case get_ssl_config() of - ssl_verify_enabled -> - ssl_opts(ssl_verify_enabled, Url); - ssl_verify_disabled -> - [{verify, verify_none}] + <<>> end. %%------------------------------------------------------------------------------ @@ -188,7 +198,7 @@ ssl_opts(Url) -> %%------------------------------------------------------------------------------ -spec store_etag_in_cache(File, ETag) -> Res when File :: file:name(), - ETag :: string(), + ETag :: binary(), Res :: ok. store_etag_in_cache(Path, ETag) -> _ = file:write_file(Path, ETag). @@ -196,223 +206,74 @@ store_etag_in_cache(Path, ETag) -> %%%============================================================================= %%% Private functions %%%============================================================================= --spec cached_download(TmpDir, CachePath, Pkg, Url, ETag, State, ETagPath, - UpdateETag) -> Res when +-spec cached_download(TmpDir, CachePath, Pkg, ETag, ETagPath, UpdateETag) -> Res when TmpDir :: file:name(), CachePath :: file:name(), - Pkg :: {pkg, Name :: binary(), Vsn :: binary(), Hash :: binary()}, - Url :: string(), - ETag :: false | string(), - State :: rebar_state:t(), + Pkg :: package(), + ETag :: binary(), ETagPath :: file:name(), UpdateETag :: boolean(), - Res :: download_result(). -cached_download(TmpDir, CachePath, Pkg={pkg, Name, Vsn, _Hash}, Url, ETag, - State, ETagPath, UpdateETag) -> - case request(Url, ETag) of + Res :: ok | {unexpected_hash, integer(), integer()} | {fetch_fail, binary(), binary()}. +cached_download(TmpDir, CachePath, Pkg={pkg, Name, Vsn, _Hash, RepoConfig}, ETag, + ETagPath, UpdateETag) -> + case request(RepoConfig, Name, Vsn, ETag) of {ok, cached} -> ?INFO("Version cached at ~ts is up to date, reusing it", [CachePath]), - serve_from_cache(TmpDir, CachePath, Pkg, State); + serve_from_cache(TmpDir, CachePath, Pkg); {ok, Body, NewETag} -> ?INFO("Downloaded package, caching at ~ts", [CachePath]), maybe_store_etag_in_cache(UpdateETag, ETagPath, NewETag), - serve_from_download(TmpDir, CachePath, Pkg, NewETag, Body, State, - ETagPath); - error when ETag =/= false -> + serve_from_download(TmpDir, CachePath, Pkg, Body); + error when ETag =/= <<>> -> store_etag_in_cache(ETagPath, ETag), ?INFO("Download error, using cached file at ~ts", [CachePath]), - serve_from_cache(TmpDir, CachePath, Pkg, State); + serve_from_cache(TmpDir, CachePath, Pkg); error -> {fetch_fail, Name, Vsn} end. --spec serve_from_cache(TmpDir, CachePath, Pkg, State) -> Res when +-spec serve_from_cache(TmpDir, CachePath, Pkg) -> Res when TmpDir :: file:name(), CachePath :: file:name(), - Pkg :: {pkg, Name :: binary(), Vsn :: binary(), Hash :: binary()}, - State :: rebar_state:t(), - Res :: cached_result(). -serve_from_cache(TmpDir, CachePath, Pkg, State) -> - {Files, Contents, Version, Meta} = extract(TmpDir, CachePath), - case checksums(Pkg, Files, Contents, Version, Meta, State) of - {Chk, Chk, Chk, Chk} -> - ok = erl_tar:extract({binary, Contents}, [{cwd, TmpDir}, compressed]), - {ok, true}; - {_Hash, Chk, Chk, Chk} -> - ?DEBUG("Expected hash ~p does not match checksums ~p", [_Hash, Chk]), - {unexpected_hash, CachePath, _Hash, Chk}; - {Chk, _Bin, Chk, Chk} -> - ?DEBUG("Checksums: registry: ~p, pkg: ~p", [Chk, _Bin]), - {failed_extract, CachePath}; - {Chk, Chk, _Reg, Chk} -> - ?DEBUG("Checksums: registry: ~p, pkg: ~p", [_Reg, Chk]), - {bad_registry_checksum, CachePath}; - {_Hash, _Bin, _Reg, _Tar} -> - ?DEBUG("Checksums: expected: ~p, registry: ~p, pkg: ~p, meta: ~p", - [_Hash, _Reg, _Bin, _Tar]), - {bad_checksum, CachePath} + Pkg :: package(), + Res :: ok | {error,_} | {bad_registry_checksum, integer(), integer()}. +serve_from_cache(TmpDir, CachePath, Pkg) -> + {ok, Binary} = file:read_file(CachePath), + serve_from_memory(TmpDir, Binary, Pkg). + +-spec serve_from_memory(TmpDir, Tarball, Package) -> Res when + TmpDir :: file:name(), + Tarball :: binary(), + Package :: package(), + Res :: ok | {error,_} | {bad_registry_checksum, integer(), integer()}. +serve_from_memory(TmpDir, Binary, {pkg, _Name, _Vsn, Hash, _RepoConfig}) -> + RegistryChecksum = list_to_integer(binary_to_list(Hash), 16), + case hex_tarball:unpack(Binary, TmpDir) of + {ok, #{checksum := <<Checksum:256/big-unsigned>>}} when RegistryChecksum =/= Checksum -> + ?DEBUG("Expected hash ~64.16.0B does not match checksum of fetched package ~64.16.0B", + [RegistryChecksum, Checksum]), + {bad_registry_checksum, RegistryChecksum, Checksum}; + {ok, #{checksum := <<RegistryChecksum:256/big-unsigned>>}} -> + ok; + {error, Reason} -> + {error, {hex_tarball, Reason}} end. --spec serve_from_download(TmpDir, CachePath, Package, ETag, Binary, State, - ETagPath) -> Res when +-spec serve_from_download(TmpDir, CachePath, Package, Binary) -> Res when TmpDir :: file:name(), CachePath :: file:name(), - Package :: {pkg, Name :: binary(), Vsn :: binary(), Hash :: binary()}, - ETag :: string(), + Package :: package(), Binary :: binary(), - State :: rebar_state:t(), - ETagPath :: file:name(), - Res :: download_result(). -serve_from_download(TmpDir, CachePath, Package, ETag, Binary, State, ETagPath) -> + Res :: ok | {error,_}. +serve_from_download(TmpDir, CachePath, Package, Binary) -> ?DEBUG("Writing ~p to cache at ~ts", [Package, CachePath]), file:write_file(CachePath, Binary), - case etag(ETagPath) of - ETag -> - serve_from_cache(TmpDir, CachePath, Package, State); - FileETag -> - ?DEBUG("Downloaded file ~ts ETag ~ts doesn't match returned ETag ~ts", - [CachePath, ETag, FileETag]), - {bad_download, CachePath} - end. - --spec extract(TmpDir, CachePath) -> Res when - TmpDir :: file:name(), - CachePath :: file:name(), - Res :: {Files, Contents, Version, Meta}, - Files :: list({file:name(), binary()}), - Contents :: binary(), - Version :: binary(), - Meta :: binary(). -extract(TmpDir, CachePath) -> - ec_file:mkdir_p(TmpDir), - {ok, Files} = erl_tar:extract(CachePath, [memory]), - {"contents.tar.gz", Contents} = lists:keyfind("contents.tar.gz", 1, Files), - {"VERSION", Version} = lists:keyfind("VERSION", 1, Files), - {"metadata.config", Meta} = lists:keyfind("metadata.config", 1, Files), - {Files, Contents, Version, Meta}. - --spec checksums(Pkg, Files, Contents, Version, Meta, State) -> Res when - Pkg :: {pkg, Name :: binary(), Vsn :: binary(), Hash :: binary()}, - Files :: list({file:name(), binary()}), - Contents :: binary(), - Version :: binary(), - Meta :: binary(), - State :: rebar_state:t(), - Res :: {Hash, BinChecksum, RegistryChecksum, TarChecksum}, - Hash :: binary(), - BinChecksum :: binary(), - RegistryChecksum :: any(), - TarChecksum :: binary(). -checksums(Pkg={pkg, _Name, _Vsn, Hash}, Files, Contents, Version, Meta, State) -> - Blob = <<Version/binary, Meta/binary, Contents/binary>>, - <<X:256/big-unsigned>> = crypto:hash(sha256, Blob), - BinChecksum = list_to_binary( - rebar_string:uppercase( - lists:flatten(io_lib:format("~64.16.0b", [X])))), - RegistryChecksum = rebar_packages:registry_checksum(Pkg, State), - {"CHECKSUM", TarChecksum} = lists:keyfind("CHECKSUM", 1, Files), - {Hash, BinChecksum, RegistryChecksum, TarChecksum}. - -%%------------------------------------------------------------------------------ -%% @doc -%% Return the SSL options adequate for the project based on -%% its configuration, including for validation of certs. -%% @end -%%------------------------------------------------------------------------------ --spec ssl_opts(Enabled, Url) -> Res when - Enabled :: atom(), - Url :: string(), - Res :: proplists:proplist(). -ssl_opts(ssl_verify_enabled, Url) -> - case check_ssl_version() of - true -> - {ok, {_, _, Hostname, _, _, _}} = - http_uri:parse(rebar_utils:to_list(Url)), - VerifyFun = {fun ssl_verify_hostname:verify_fun/3, - [{check_hostname, Hostname}]}, - CACerts = certifi:cacerts(), - [{verify, verify_peer}, {depth, 2}, {cacerts, CACerts}, - {partial_chain, fun partial_chain/1}, {verify_fun, VerifyFun}]; - false -> - ?WARN("Insecure HTTPS request (peer verification disabled), " - "please update to OTP 17.4 or later", []), - [{verify, verify_none}] - end. - --spec partial_chain(Certs) -> Res when - Certs :: list(any()), - Res :: unknown_ca | {trusted_ca, any()}. -partial_chain(Certs) -> - Certs1 = [{Cert, public_key:pkix_decode_cert(Cert, otp)} || Cert <- Certs], - CACerts = certifi:cacerts(), - CACerts1 = [public_key:pkix_decode_cert(Cert, otp) || Cert <- CACerts], - case ec_lists:find(fun({_, Cert}) -> - check_cert(CACerts1, Cert) - end, Certs1) of - {ok, Trusted} -> - {trusted_ca, element(1, Trusted)}; - _ -> - unknown_ca - end. - --spec extract_public_key_info(Cert) -> Res when - Cert :: #'OTPCertificate'{tbsCertificate::#'OTPTBSCertificate'{}}, - Res :: any(). -extract_public_key_info(Cert) -> - ((Cert#'OTPCertificate'.tbsCertificate)#'OTPTBSCertificate'.subjectPublicKeyInfo). - --spec check_cert(CACerts, Cert) -> Res when - CACerts :: list(any()), - Cert :: any(), - Res :: boolean(). -check_cert(CACerts, Cert) -> - lists:any(fun(CACert) -> - extract_public_key_info(CACert) == extract_public_key_info(Cert) - end, CACerts). - --spec check_ssl_version() -> - boolean(). -check_ssl_version() -> - case application:get_key(ssl, vsn) of - {ok, Vsn} -> - parse_vsn(Vsn) >= {5, 3, 6}; - _ -> - false - end. - --spec get_ssl_config() -> - ssl_verify_disabled | ssl_verify_enabled. -get_ssl_config() -> - GlobalConfigFile = rebar_dir:global_config(), - Config = rebar_config:consult_file(GlobalConfigFile), - case proplists:get_value(ssl_verify, Config, []) of - false -> - ssl_verify_disabled; - _ -> - ssl_verify_enabled - end. - --spec parse_vsn(Vsn) -> Res when - Vsn :: string(), - Res :: {integer(), integer(), integer()}. -parse_vsn(Vsn) -> - version_pad(rebar_string:lexemes(Vsn, ".-")). - --spec version_pad(list(nonempty_string())) -> Res when - Res :: {integer(), integer(), integer()}. -version_pad([Major]) -> - {list_to_integer(Major), 0, 0}; -version_pad([Major, Minor]) -> - {list_to_integer(Major), list_to_integer(Minor), 0}; -version_pad([Major, Minor, Patch]) -> - {list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)}; -version_pad([Major, Minor, Patch | _]) -> - {list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)}. + serve_from_memory(TmpDir, Binary, Package). -spec maybe_store_etag_in_cache(UpdateETag, Path, ETag) -> Res when UpdateETag :: boolean(), Path :: file:name(), - ETag :: string(), + ETag :: binary(), Res :: ok. maybe_store_etag_in_cache(false = _UpdateETag, _Path, _ETag) -> ok; diff --git a/src/rebar_prv_deps.erl b/src/rebar_prv_deps.erl index a88b014..577a859 100644 --- a/src/rebar_prv_deps.erl +++ b/src/rebar_prv_deps.erl @@ -97,10 +97,11 @@ display_dep(_State, {Name, _Vsn, Source}) when is_tuple(Source) -> display_dep(_State, {Name, _Vsn, Source, _Opts}) when is_tuple(Source) -> ?CONSOLE("~ts* (~ts source)", [rebar_utils:to_binary(Name), type(Source)]); %% Locked -display_dep(State, {Name, Source={pkg, _, Vsn}, Level}) when is_integer(Level) -> +display_dep(State, {Name, _Source={pkg, _, Vsn}, Level}) when is_integer(Level) -> DepsDir = rebar_dir:deps_dir(State), AppDir = filename:join([DepsDir, rebar_utils:to_binary(Name)]), - NeedsUpdate = case rebar_fetch:needs_update(AppDir, Source, State) of + {ok, AppInfo} = rebar_app_info:discover(AppDir), + NeedsUpdate = case rebar_fetch:needs_update(AppInfo, State) of true -> "*"; false -> "" end, @@ -108,7 +109,8 @@ display_dep(State, {Name, Source={pkg, _, Vsn}, Level}) when is_integer(Level) - display_dep(State, {Name, Source, Level}) when is_tuple(Source), is_integer(Level) -> DepsDir = rebar_dir:deps_dir(State), AppDir = filename:join([DepsDir, rebar_utils:to_binary(Name)]), - NeedsUpdate = case rebar_fetch:needs_update(AppDir, Source, State) of + {ok, AppInfo} = rebar_app_info:discover(AppDir), + NeedsUpdate = case rebar_fetch:needs_update(AppInfo, State) of true -> "*"; false -> "" end, diff --git a/src/rebar_prv_deps_tree.erl b/src/rebar_prv_deps_tree.erl index 07c7972..d7b49c5 100644 --- a/src/rebar_prv_deps_tree.erl +++ b/src/rebar_prv_deps_tree.erl @@ -39,18 +39,16 @@ format_error(Reason) -> %% Internal functions print_deps_tree(SrcDeps, Verbose, State) -> - Resources = rebar_state:resources(State), D = lists:foldl(fun(App, Dict) -> Name = rebar_app_info:name(App), Vsn = rebar_app_info:original_vsn(App), - AppDir = rebar_app_info:dir(App), - Vsn1 = rebar_utils:vcs_vsn(Vsn, AppDir, Resources), + Vsn1 = rebar_utils:vcs_vsn(App, Vsn, State), Source = rebar_app_info:source(App), Parent = rebar_app_info:parent(App), dict:append_list(Parent, [{Name, Vsn1, Source}], Dict) end, dict:new(), SrcDeps), ProjectAppNames = [{rebar_app_info:name(App) - ,rebar_utils:vcs_vsn(rebar_app_info:original_vsn(App), rebar_app_info:dir(App), Resources) + ,rebar_utils:vcs_vsn(App, rebar_app_info:original_vsn(App), State) ,project} || App <- rebar_state:project_apps(State)], case dict:find(root, D) of {ok, Children} -> diff --git a/src/rebar_prv_install_deps.erl b/src/rebar_prv_install_deps.erl index b735ed0..bad5af4 100644 --- a/src/rebar_prv_install_deps.erl +++ b/src/rebar_prv_install_deps.erl @@ -277,10 +277,8 @@ update_unseen_dep(AppInfo, Profile, Level, Deps, Apps, State, Upgrade, Seen, Loc -spec handle_dep(rebar_state:t(), atom(), file:filename_all(), rebar_app_info:t(), list(), integer()) -> {rebar_app_info:t(), [rebar_app_info:t()], rebar_state:t()}. handle_dep(State, Profile, DepsDir, AppInfo, Locks, Level) -> Name = rebar_app_info:name(AppInfo), - C = rebar_config:consult(rebar_app_info:dir(AppInfo)), - AppInfo0 = rebar_app_info:update_opts(AppInfo, rebar_app_info:opts(AppInfo), C), - AppInfo1 = rebar_app_info:apply_overrides(rebar_app_info:get(AppInfo, overrides, []), AppInfo0), + AppInfo1 = rebar_app_info:apply_overrides(rebar_app_info:get(AppInfo, overrides, []), AppInfo), AppInfo2 = rebar_app_info:apply_profiles(AppInfo1, [default, prod]), Plugins = rebar_app_info:get(AppInfo2, plugins, []), @@ -297,34 +295,33 @@ handle_dep(State, Profile, DepsDir, AppInfo, Locks, Level) -> AppInfo4 = rebar_app_info:deps(AppInfo3, rebar_state:deps_names(Deps)), %% Keep all overrides from the global config and this dep when parsing its deps - Overrides = rebar_app_info:get(AppInfo0, overrides, []), + Overrides = rebar_app_info:get(AppInfo, overrides, []), Deps1 = rebar_app_utils:parse_deps(Name, DepsDir, Deps, rebar_state:set(State, overrides, Overrides) ,Locks, Level+1), {AppInfo4, Deps1, State1}. -spec maybe_fetch(rebar_app_info:t(), atom(), boolean(), - sets:set(binary()), rebar_state:t()) -> {boolean(), rebar_app_info:t()}. + sets:set(binary()), rebar_state:t()) -> {ok, rebar_app_info:t()}. maybe_fetch(AppInfo, Profile, Upgrade, Seen, State) -> AppDir = rebar_utils:to_list(rebar_app_info:dir(AppInfo)), %% Don't fetch dep if it exists in the _checkouts dir case rebar_app_info:is_checkout(AppInfo) of true -> - {false, AppInfo}; + {ok, AppInfo}; false -> - case rebar_app_discover:find_app(AppInfo, AppDir, all) of + case rebar_app_info:is_available(AppInfo) of false -> - true = fetch_app(AppInfo, AppDir, State), - maybe_symlink_default(State, Profile, AppDir, AppInfo), - {true, rebar_app_info:valid(update_app_info(AppDir, AppInfo), false)}; - {true, AppInfo1} -> - case sets:is_element(rebar_app_info:name(AppInfo1), Seen) of + AppInfo1 = fetch_app(AppInfo, State), + maybe_symlink_default(State, Profile, AppDir, AppInfo1), + {ok, rebar_app_info:is_available(rebar_app_info:valid(AppInfo1, false), true)}; + true -> + case sets:is_element(rebar_app_info:name(AppInfo), Seen) of true -> - {false, AppInfo1}; + {ok, AppInfo}; false -> - maybe_symlink_default(State, Profile, AppDir, AppInfo1), - MaybeUpgrade = maybe_upgrade(AppInfo, AppDir, Upgrade, State), - AppInfo2 = update_app_info(AppDir, AppInfo1), - {MaybeUpgrade, AppInfo2} + maybe_symlink_default(State, Profile, AppDir, AppInfo), + AppInfo1 = maybe_upgrade(AppInfo, AppDir, Upgrade, State), + {ok, AppInfo1} end end end. @@ -372,52 +369,37 @@ make_relative_to_root(State, Path) when is_list(Path) -> Root = rebar_dir:root_dir(State), rebar_dir:make_relative_path(Path, Root). -fetch_app(AppInfo, AppDir, State) -> +fetch_app(AppInfo, State) -> ?INFO("Fetching ~ts (~p)", [rebar_app_info:name(AppInfo), - format_source(rebar_app_info:source(AppInfo))]), - Source = rebar_app_info:source(AppInfo), - true = rebar_fetch:download_source(AppDir, Source, State). - -format_source({pkg, Name, Vsn, _Hash}) -> {pkg, Name, Vsn}; -format_source(Source) -> Source. - -%% This is called after the dep has been downloaded and unpacked, if it hadn't been already. -%% So this is the first time for newly downloaded apps that its .app/.app.src data can -%% be read in an parsed. -update_app_info(AppDir, AppInfo) -> - case rebar_app_discover:find_app(AppInfo, AppDir, all) of - {true, AppInfo1} -> - AppInfo1; - false -> - throw(?PRV_ERROR({dep_app_not_found, AppDir, rebar_app_info:name(AppInfo)})) - end. + rebar_resource_v2:format_source(rebar_app_info:source(AppInfo))]), + rebar_fetch:download_source(AppInfo, State). -maybe_upgrade(AppInfo, AppDir, Upgrade, State) -> - Source = rebar_app_info:source(AppInfo), +maybe_upgrade(AppInfo, _AppDir, Upgrade, State) -> case Upgrade orelse rebar_app_info:is_lock(AppInfo) of true -> - case rebar_fetch:needs_update(AppDir, Source, State) of + case rebar_fetch:needs_update(AppInfo, State) of true -> - ?INFO("Upgrading ~ts (~p)", [rebar_app_info:name(AppInfo), rebar_app_info:source(AppInfo)]), - true = rebar_fetch:download_source(AppDir, Source, State); + ?INFO("Upgrading ~ts (~p)", [rebar_app_info:name(AppInfo), + rebar_resource_v2:format_source(rebar_app_info:source(AppInfo))]), + rebar_fetch:download_source(AppInfo, State); false -> case Upgrade of true -> ?INFO("No upgrade needed for ~ts", [rebar_app_info:name(AppInfo)]), - false; + AppInfo; false -> - false + AppInfo end end; false -> - false + AppInfo end. warn_skip_deps(AppInfo, State) -> Msg = "Skipping ~ts (from ~p) as an app of the same name " "has already been fetched", Args = [rebar_app_info:name(AppInfo), - rebar_app_info:source(AppInfo)], + rebar_resource_v2:format_source(rebar_app_info:source(AppInfo))], case rebar_state:get(State, deps_error_on_conflict, false) of false -> case rebar_state:get(State, deps_warning_on_conflict, true) of diff --git a/src/rebar_prv_local_upgrade.erl b/src/rebar_prv_local_upgrade.erl index 3b3c9cb..1931d65 100644 --- a/src/rebar_prv_local_upgrade.erl +++ b/src/rebar_prv_local_upgrade.erl @@ -77,7 +77,7 @@ get_md5(Rebar3Path) -> maybe_fetch_rebar3(Rebar3Md5) -> TmpDir = ec_file:insecure_mkdtemp(), TmpFile = filename:join(TmpDir, "rebar3"), - case rebar_pkg_resource:request("https://s3.amazonaws.com/rebar3/rebar3", Rebar3Md5) of + case request("https://s3.amazonaws.com/rebar3/rebar3", Rebar3Md5) of {ok, Binary, ETag} -> file:write_file(TmpFile, Binary), case etag(TmpFile) of @@ -101,3 +101,29 @@ etag(Path) -> {error, _} -> false end. + +-spec request(Url, ETag) -> Res when + Url :: string(), + ETag :: false | string(), + Res :: 'error' | {ok, cached} | {ok, any(), string()}. +request(Url, ETag) -> + HttpOptions = [{ssl, rebar_utils:ssl_opts(Url)}, + {relaxed, true} | rebar_utils:get_proxy_auth()], + case httpc:request(get, {Url, [{"if-none-match", "\"" ++ ETag ++ "\""} + || ETag =/= false] ++ + [{"User-Agent", rebar_utils:user_agent()}]}, + HttpOptions, [{body_format, binary}], rebar) of + {ok, {{_Version, 200, _Reason}, Headers, Body}} -> + ?DEBUG("Successfully downloaded ~ts", [Url]), + {"etag", ETag1} = lists:keyfind("etag", 1, Headers), + {ok, Body, rebar_string:trim(ETag1, both, [$"])}; + {ok, {{_Version, 304, _Reason}, _Headers, _Body}} -> + ?DEBUG("Cached copy of ~ts still valid", [Url]), + {ok, cached}; + {ok, {{_Version, Code, _Reason}, _Headers, _Body}} -> + ?DEBUG("Request to ~p failed: status code ~p", [Url, Code]), + error; + {error, Reason} -> + ?DEBUG("Request to ~p failed: ~p", [Url, Reason]), + error + end. diff --git a/src/rebar_prv_lock.erl b/src/rebar_prv_lock.erl index cbe8dfe..570c03f 100644 --- a/src/rebar_prv_lock.erl +++ b/src/rebar_prv_lock.erl @@ -54,12 +54,9 @@ format_error(Reason) -> build_locks(State) -> AllDeps = rebar_state:lock(State), [begin - Dir = rebar_app_info:dir(Dep), - Source = rebar_app_info:source(Dep), - %% If source is tuple it is a source dep %% e.g. {git, "git://github.com/ninenines/cowboy.git", "master"} - {rebar_app_info:name(Dep) - ,rebar_fetch:lock_source(Dir, Source, State) - ,rebar_app_info:dep_level(Dep)} + {rebar_app_info:name(Dep), + rebar_fetch:lock_source(Dep, State), + rebar_app_info:dep_level(Dep)} end || Dep <- AllDeps, not(rebar_app_info:is_checkout(Dep))]. diff --git a/src/rebar_prv_packages.erl b/src/rebar_prv_packages.erl index 6e8e683..3e54cdc 100644 --- a/src/rebar_prv_packages.erl +++ b/src/rebar_prv_packages.erl @@ -15,53 +15,75 @@ -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> - State1 = rebar_state:add_provider(State, providers:create([{name, ?PROVIDER}, - {module, ?MODULE}, - {bare, true}, - {deps, ?DEPS}, - {example, "rebar3 pkgs"}, - {short_desc, "List available packages."}, - {desc, info("List available packages")}, - {opts, []}])), + State1 = rebar_state:add_provider(State, + providers:create([{name, ?PROVIDER}, + {module, ?MODULE}, + {bare, true}, + {deps, ?DEPS}, + {example, "rebar3 pkgs elli"}, + {short_desc, "List information for a package."}, + {desc, info("List information for a package")}, + {opts, [{package, undefined, undefined, string, + "Package to fetch information for."}]}])), {ok, State1}. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> - rebar_packages:packages(State), - case rebar_state:command_args(State) of - [Name] -> - print_packages(get_packages(rebar_utils:to_binary(Name))); - _ -> - print_packages(sort_packages()) - end, - {ok, State}. + {Args, _} = rebar_state:command_parsed_args(State), + case proplists:get_value(package, Args, undefined) of + undefined -> + ?PRV_ERROR(no_package_arg); + Name -> + Resources = rebar_state:resources(State), + #{repos := Repos} = rebar_resource_v2:find_resource_state(pkg, Resources), + Results = get_package(rebar_utils:to_binary(Name), Repos), + case lists:all(fun({_, {error, not_found}}) -> true; (_) -> false end, Results) of + true -> + ?PRV_ERROR({not_found, Name}); + false -> + [print_packages(Result) || Result <- Results], + {ok, State} + end + end. --spec format_error(any()) -> iolist(). -format_error(load_registry_fail) -> - "Failed to load package regsitry. Try running 'rebar3 update' to fix". +-spec get_package(binary(), [map()]) -> [{binary(), {ok, map()} | {error, term()}}]. +get_package(Name, Repos) -> + lists:foldl(fun(RepoConfig, Acc) -> + [{maps:get(name, RepoConfig), rebar_packages:get(RepoConfig, Name)} | Acc] + end, [], Repos). -print_packages(Pkgs) -> - orddict:map(fun(Name, Vsns) -> - SortedVsns = lists:sort(fun(A, B) -> - ec_semver:lte(ec_semver:parse(A) - ,ec_semver:parse(B)) - end, Vsns), - VsnStr = join(SortedVsns, <<", ">>), - ?CONSOLE("~ts:~n Versions: ~ts~n", [Name, VsnStr]) - end, Pkgs). -sort_packages() -> - ets:foldl(fun({package_index_version, _}, Acc) -> - Acc; - ({Pkg, Vsns}, Acc) -> - orddict:store(Pkg, Vsns, Acc); - (_, Acc) -> - Acc - end, orddict:new(), ?PACKAGE_TABLE). +-spec format_error(any()) -> iolist(). +format_error(no_package_arg) -> + "Missing package argument to `rebar3 pkgs` command."; +format_error({not_found, Name}) -> + io_lib:format("Package ~ts not found in any repo.", [Name]); +format_error(unknown) -> + "Something went wrong with fetching package metadata.". -get_packages(Name) -> - ets:lookup(?PACKAGE_TABLE, Name). +print_packages({RepoName, {error, not_found}}) -> + ?CONSOLE("~ts: Package not found in this repo.~n", [RepoName]); +print_packages({RepoName, {error, _}}) -> + ?CONSOLE("~ts: Error fetching from this repo.~n", [RepoName]); +print_packages({RepoName, {ok, #{<<"name">> := Name, + <<"meta">> := Meta, + <<"releases">> := Releases}}}) -> + Description = maps:get(<<"description">>, Meta, ""), + Licenses = join(maps:get(<<"licenses">>, Meta, []), <<", ">>), + Links = join_map(maps:get(<<"links">>, Meta, []), <<"\n ">>), + Maintainers = join(maps:get(<<"maintainers">>, Meta, []), <<", ">>), + Versions = [V || #{<<"version">> := V} <- Releases], + VsnStr = join(Versions, <<", ">>), + ?CONSOLE("~ts:~n" + " Name: ~ts~n" + " Description: ~ts~n" + " Licenses: ~ts~n" + " Maintainers: ~ts~n" + " Links:~n ~ts~n" + " Versions: ~ts~n", [RepoName, Name, Description, Licenses, Maintainers, Links, VsnStr]); +print_packages(_) -> + ok. -spec join([binary()], binary()) -> binary(). join([Bin], _Sep) -> @@ -69,6 +91,14 @@ join([Bin], _Sep) -> join([Bin | T], Sep) -> <<Bin/binary, Sep/binary, (join(T, Sep))/binary>>. +-spec join_map(map(), binary()) -> binary(). +join_map(Map, Sep) -> + join_tuple_list(maps:to_list(Map), Sep). + +join_tuple_list([{K, V}], _Sep) -> + <<K/binary, ": ", V/binary>>; +join_tuple_list([{K, V} | T], Sep) -> + <<K/binary, ": ", V/binary, Sep/binary, (join_tuple_list(T, Sep))/binary>>. info(Description) -> io_lib:format("~ts.~n", [Description]). diff --git a/src/rebar_prv_repos.erl b/src/rebar_prv_repos.erl new file mode 100644 index 0000000..0515910 --- /dev/null +++ b/src/rebar_prv_repos.erl @@ -0,0 +1,47 @@ +%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- +%% ex: ts=4 sw=4 et + +-module(rebar_prv_repos). + +-behaviour(provider). + +-export([init/1, + do/1, + format_error/1]). + +-include("rebar.hrl"). + +-define(PROVIDER, repos). +-define(DEPS, []). + +%% =================================================================== +%% Public API +%% =================================================================== + +-spec init(rebar_state:t()) -> {ok, rebar_state:t()}. +init(State) -> + Provider = providers:create( + [{name, ?PROVIDER}, + {module, ?MODULE}, + {bare, false}, + {deps, ?DEPS}, + {example, "rebar3 repos"}, + {short_desc, "Print current package repository configuration"}, + {desc, "Display repository configuration for debugging purpose"}, + {opts, []}]), + State1 = rebar_state:add_provider(State, Provider), + {ok, State1}. + +-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. +do(State) -> + Resources = rebar_state:resources(State), + #{repos := Repos} = rebar_resource_v2:find_resource_state(pkg, Resources), + + ?CONSOLE("Repos:", []), + %%TODO: do some formatting + ?CONSOLE("~p", [Repos]), + {ok, State}. + +-spec format_error(any()) -> iolist(). +format_error(Reason) -> + io_lib:format("~p", [Reason]). diff --git a/src/rebar_prv_shell.erl b/src/rebar_prv_shell.erl index 5b8d789..760f0d8 100644 --- a/src/rebar_prv_shell.erl +++ b/src/rebar_prv_shell.erl @@ -40,6 +40,8 @@ -define(PROVIDER, shell). -define(DEPS, [compile]). +-dialyzer({nowarn_function, rewrite_leaders/2}). + %% =================================================================== %% Public API %% =================================================================== diff --git a/src/rebar_prv_update.erl b/src/rebar_prv_update.erl index 1744631..4c820c5 100644 --- a/src/rebar_prv_update.erl +++ b/src/rebar_prv_update.erl @@ -9,12 +9,6 @@ do/1, format_error/1]). --export([hex_to_index/1]). - --ifdef(TEST). --export([cmp_/6, cmpl_/6, valid_vsn/1]). --endif. - -include("rebar.hrl"). -include_lib("providers/include/providers.hrl"). @@ -39,44 +33,13 @@ init(State) -> -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> - try - case rebar_packages:registry_dir(State) of - {ok, RegistryDir} -> - filelib:ensure_dir(filename:join(RegistryDir, "dummy")), - HexFile = filename:join(RegistryDir, "registry"), - ?INFO("Updating package registry...", []), - TmpDir = ec_file:insecure_mkdtemp(), - TmpFile = filename:join(TmpDir, "packages.gz"), - - CDN = rebar_state:get(State, rebar_packages_cdn, ?DEFAULT_CDN), - case rebar_utils:url_append_path(CDN, ?REMOTE_REGISTRY_FILE) of - {ok, Url} -> - HttpOptions = [{relaxed, true} | rebar_utils:get_proxy_auth()], - ?DEBUG("Fetching registry from ~p", [Url]), - case httpc:request(get, {Url, [{"User-Agent", rebar_utils:user_agent()}]}, - HttpOptions, [{stream, TmpFile}, {sync, true}], - rebar) of - {ok, saved_to_file} -> - {ok, Data} = file:read_file(TmpFile), - Unzipped = zlib:gunzip(Data), - ok = file:write_file(HexFile, Unzipped), - ?INFO("Writing registry to ~ts", [HexFile]), - hex_to_index(State), - {ok, State}; - _ -> - ?PRV_ERROR(package_index_download) - end; - _ -> - ?PRV_ERROR({package_parse_cdn, CDN}) - end; - {uri_parse_error, CDN} -> - ?PRV_ERROR({package_parse_cdn, CDN}) - end - catch - ?WITH_STACKTRACE(_E, C, S) - ?DEBUG("Error creating package index: ~p ~p", [C, S]), - throw(?PRV_ERROR(package_index_write)) - end. + Names = rebar_packages:get_all_names(State), + Resources = rebar_state:resources(State), + #{repos := RepoConfigs} = rebar_resource_v2:find_resource_state(pkg, Resources), + [[update_package(Name, RepoConfig, State) + || Name <- Names] + || RepoConfig <- RepoConfigs], + {ok, State}. -spec format_error(any()) -> iolist(). format_error({package_parse_cdn, Uri}) -> @@ -86,186 +49,11 @@ format_error(package_index_download) -> format_error(package_index_write) -> "Failed to write package index.". -is_supported(<<"make">>) -> true; -is_supported(<<"rebar">>) -> true; -is_supported(<<"rebar3">>) -> true; -is_supported(_) -> false. - -hex_to_index(State) -> - {ok, RegistryDir} = rebar_packages:registry_dir(State), - HexFile = filename:join(RegistryDir, "registry"), - try ets:file2tab(HexFile) of - {ok, Registry} -> - try - PackageIndex = filename:join(RegistryDir, "packages.idx"), - ?INFO("Generating package index...", []), - (catch ets:delete(?PACKAGE_TABLE)), - ets:new(?PACKAGE_TABLE, [named_table, public]), - ets:foldl(fun({{Pkg, PkgVsn}, [Deps, Checksum, BuildTools | _]}, _) when is_list(BuildTools) -> - case lists:any(fun is_supported/1, BuildTools) of - true -> - DepsList = update_deps_list(Pkg, PkgVsn, Deps, Registry, State), - HashedDeps = update_deps_hashes(DepsList), - ets:insert(?PACKAGE_TABLE, {{Pkg, PkgVsn}, HashedDeps, Checksum}); - false -> - true - end; - (_, _) -> - true - end, true, Registry), - - ets:foldl(fun({Pkg, [[]]}, _) when is_binary(Pkg) -> - true; - ({Pkg, [Vsns=[_Vsn | _Rest]]}, _) when is_binary(Pkg) -> - %% Verify the package is of the right build tool by checking if the first - %% version exists in the table from the foldl above - case [V || V <- Vsns, ets:member(?PACKAGE_TABLE, {Pkg, V})] of - [] -> - true; - Vsns1 -> - ets:insert(?PACKAGE_TABLE, {Pkg, Vsns1}) - end; - (_, _) -> - true - end, true, Registry), - ets:insert(?PACKAGE_TABLE, {package_index_version, ?PACKAGE_INDEX_VERSION}), - ?INFO("Writing index to ~ts", [PackageIndex]), - ets:tab2file(?PACKAGE_TABLE, PackageIndex), - true - after - catch ets:delete(Registry) - end; - {error, Reason} -> - ?DEBUG("Error loading package registry: ~p", [Reason]), - false - catch - _:_ -> - fail - end. - -update_deps_list(Pkg, PkgVsn, Deps, HexRegistry, State) -> - lists:foldl(fun([Dep, DepVsn, false, AppName | _], DepsListAcc) -> - Dep1 = {Pkg, PkgVsn, Dep, AppName}, - case {valid_vsn(DepVsn), DepVsn} of - %% Those are all not perfectly implemented! - %% and doubled since spaces seem not to be - %% enforced - {false, Vsn} -> - ?DEBUG("[~ts:~ts], Bad dependency version for ~ts: ~ts.", - [Pkg, PkgVsn, Dep, Vsn]), - DepsListAcc; - {_, <<"~>", Vsn/binary>>} -> - highest_matching(Dep1, rm_ws(Vsn), HexRegistry, - State, DepsListAcc); - {_, <<">=", Vsn/binary>>} -> - cmp(Dep1, rm_ws(Vsn), HexRegistry, State, - DepsListAcc, fun ec_semver:gte/2); - {_, <<">", Vsn/binary>>} -> - cmp(Dep1, rm_ws(Vsn), HexRegistry, State, - DepsListAcc, fun ec_semver:gt/2); - {_, <<"<=", Vsn/binary>>} -> - cmpl(Dep1, rm_ws(Vsn), HexRegistry, State, - DepsListAcc, fun ec_semver:lte/2); - {_, <<"<", Vsn/binary>>} -> - cmpl(Dep1, rm_ws(Vsn), HexRegistry, State, - DepsListAcc, fun ec_semver:lt/2); - {_, <<"==", Vsn/binary>>} -> - [{AppName, {pkg, Dep, Vsn, undefined}} | DepsListAcc]; - {_, Vsn} -> - [{AppName, {pkg, Dep, Vsn, undefined}} | DepsListAcc] - end; - ([_Dep, _DepVsn, true, _AppName | _], DepsListAcc) -> - DepsListAcc - end, [], Deps). - -update_deps_hashes(List) -> - [{Name, {pkg, PkgName, Vsn, lookup_hash(PkgName, Vsn, Hash)}} - || {Name, {pkg, PkgName, Vsn, Hash}} <- List]. - -lookup_hash(Name, Vsn, undefined) -> - try - ets:lookup_element(?PACKAGE_TABLE, {Name, Vsn}, 3) - catch - _:_ -> - undefined - end; -lookup_hash(_, _, Hash) -> - Hash. - - -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({Pkg, PkgVsn, Dep, App}, Vsn, HexRegistry, State, DepsListAcc) -> - case rebar_packages:find_highest_matching_(Pkg, PkgVsn, Dep, Vsn, HexRegistry, State) of - {ok, HighestDepVsn} -> - [{App, {pkg, Dep, HighestDepVsn, undefined}} | DepsListAcc]; - none -> - ?DEBUG("[~ts:~ts] Missing registry entry for package ~ts. Try to fix with `rebar3 update`", - [Pkg, PkgVsn, Dep]), - DepsListAcc - end. - -cmp({_Pkg, _PkgVsn, Dep, _App} = Dep1, Vsn, HexRegistry, State, DepsListAcc, CmpFun) -> - {ok, Vsns} = rebar_packages:find_all(Dep, HexRegistry, State), - cmp_(undefined, Vsn, Vsns, DepsListAcc, Dep1, CmpFun). - - -cmp_(undefined, _MinVsn, [], DepsListAcc, {Pkg, PkgVsn, Dep, _App}, _CmpFun) -> - ?DEBUG("[~ts:~ts] Missing registry entry for package ~ts. Try to fix with `rebar3 update`", - [Pkg, PkgVsn, Dep]), - DepsListAcc; -cmp_(HighestDepVsn, _MinVsn, [], DepsListAcc, {_Pkg, _PkgVsn, Dep, App}, _CmpFun) -> - [{App, {pkg, Dep, HighestDepVsn, undefined}} | DepsListAcc]; - -cmp_(BestMatch, MinVsn, [Vsn | R], DepsListAcc, Dep, CmpFun) -> - case CmpFun(Vsn, MinVsn) of - true -> - cmp_(Vsn, Vsn, R, DepsListAcc, Dep, CmpFun); - false -> - cmp_(BestMatch, MinVsn, R, DepsListAcc, Dep, CmpFun) - end. - -%% We need to treat this differently since we want a version that is LOWER but -%% the higest possible one. -cmpl({_Pkg, _PkgVsn, Dep, _App} = Dep1, Vsn, HexRegistry, State, DepsListAcc, CmpFun) -> - {ok, Vsns} = rebar_packages:find_all(Dep, HexRegistry, State), - cmpl_(undefined, Vsn, Vsns, DepsListAcc, Dep1, CmpFun). - -cmpl_(undefined, _MaxVsn, [], DepsListAcc, {Pkg, PkgVsn, Dep, _App}, _CmpFun) -> - ?DEBUG("[~ts:~ts] Missing registry entry for package ~ts. Try to fix with `rebar3 update`", - [Pkg, PkgVsn, Dep]), - DepsListAcc; - -cmpl_(HighestDepVsn, _MaxVsn, [], DepsListAcc, {_Pkg, _PkgVsn, Dep, App}, _CmpFun) -> - [{App, {pkg, Dep, HighestDepVsn, undefined}} | DepsListAcc]; - -cmpl_(undefined, MaxVsn, [Vsn | R], DepsListAcc, Dep, CmpFun) -> - case CmpFun(Vsn, MaxVsn) of - true -> - cmpl_(Vsn, MaxVsn, R, DepsListAcc, Dep, CmpFun); - false -> - cmpl_(undefined, MaxVsn, R, DepsListAcc, Dep, CmpFun) - end; -cmpl_(BestMatch, MaxVsn, [Vsn | R], DepsListAcc, Dep, CmpFun) -> - case CmpFun(Vsn, MaxVsn) of - true -> - case ec_semver:gte(Vsn, BestMatch) of - true -> - cmpl_(Vsn, MaxVsn, R, DepsListAcc, Dep, CmpFun); - false -> - cmpl_(BestMatch, MaxVsn, R, DepsListAcc, Dep, CmpFun) - end; - false -> - cmpl_(BestMatch, MaxVsn, R, DepsListAcc, Dep, CmpFun) +update_package(Name, RepoConfig, State) -> + case rebar_packages:update_package(Name, RepoConfig, State) of + fail -> + ?WARN("Failed to fetch updates for package ~ts from repo ~ts", [Name, maps:get(name, RepoConfig)]); + _ -> + ok end. diff --git a/src/rebar_prv_upgrade.erl b/src/rebar_prv_upgrade.erl index e4469cf..b1b1b16 100644 --- a/src/rebar_prv_upgrade.erl +++ b/src/rebar_prv_upgrade.erl @@ -82,17 +82,22 @@ do_(State) -> Deps = [Dep || Dep <- TopDeps ++ ProfileDeps, % TopDeps > ProfileDeps is_atom(Dep) orelse is_atom(element(1, Dep))], Names = parse_names(rebar_utils:to_binary(proplists:get_value(package, Args, <<"">>)), Locks), + DepsDict = deps_dict(rebar_state:all_deps(State)), AltDeps = find_non_default_deps(Deps, State), FilteredNames = cull_default_names_if_profiles(Names, Deps, State), case prepare_locks(FilteredNames, Deps, Locks, [], DepsDict, AltDeps) of {error, Reason} -> {error, Reason}; - {Locks0, _Unlocks0} -> + {Locks0, Unlocks0} -> Deps0 = top_level_deps(Deps, Locks), State1 = rebar_state:set(State, {deps, default}, Deps0), DepsDir = rebar_prv_install_deps:profile_dep_dir(State, default), D = rebar_app_utils:parse_deps(root, DepsDir, Deps0, State1, Locks0, 0), + + %% first update the package index for the packages to be upgraded + update_pkg_deps(Unlocks0, D, State1), + State2 = rebar_state:set(State1, {parsed_deps, default}, D), State3 = rebar_state:set(State2, {locks, default}, Locks0), State4 = rebar_state:set(State3, upgrade, true), @@ -121,6 +126,34 @@ format_error({transitive_dependency, Name}) -> format_error(Reason) -> io_lib:format("~p", [Reason]). +%% fetch updates for package deps that have been unlocked for upgrade +update_pkg_deps([], _, _) -> + ok; +update_pkg_deps([{Name, _, _} | Rest], AppInfos, State) -> + case rebar_app_utils:find(Name, AppInfos) of + {ok, AppInfo} -> + case element(1, rebar_app_info:source(AppInfo)) of + pkg -> + Resources = rebar_state:resources(State), + #{repos := RepoConfigs} = rebar_resource_v2:find_resource_state(pkg, Resources), + [update_package(Name, RepoConfig, State) || RepoConfig <- RepoConfigs]; + _ -> + skip + end; + _ -> + %% this should be impossible... + skip + end, + update_pkg_deps(Rest, AppInfos, State). + +update_package(Name, RepoConfig, State) -> + case rebar_packages:update_package(Name, RepoConfig, State) of + fail -> + ?WARN("Failed to fetch updates for package ~ts from repo ~ts", [Name, maps:get(name, RepoConfig)]); + _ -> + ok + end. + parse_names(Bin, Locks) -> case lists:usort(re:split(Bin, <<" *, *">>, [trim, unicode])) of %% Nothing submitted, use *all* apps diff --git a/src/rebar_resource.erl b/src/rebar_resource.erl index cdce7a8..a3a8edb 100644 --- a/src/rebar_resource.erl +++ b/src/rebar_resource.erl @@ -2,23 +2,53 @@ %% ex: ts=4 sw=4 et -module(rebar_resource). --export([]). +-export([new/3, + lock/2, + download/4, + needs_update/2, + make_vsn/2]). --export_type([resource/0 - ,type/0 - ,location/0 - ,ref/0]). +-export_type([source/0, + type/0, + location/0, + ref/0]). --type resource() :: {type(), location(), ref()}. +-include("rebar.hrl"). + +-type source() :: {type(), location(), ref()} | {type(), location(), ref(), binary()}. -type type() :: atom(). -type location() :: string(). -type ref() :: any(). -callback lock(file:filename_all(), tuple()) -> - rebar_resource:resource(). + source(). -callback download(file:filename_all(), tuple(), rebar_state:t()) -> {tarball, file:filename_all()} | {ok, any()} | {error, any()}. -callback needs_update(file:filename_all(), tuple()) -> boolean(). -callback make_vsn(file:filename_all()) -> {plain, string()} | {error, string()}. + +-spec new(type(), module(), term()) -> rebar_resource_v2:resource(). +new(Type, Module, State) -> + #resource{type=Type, + module=Module, + state=State, + implementation=?MODULE}. + +lock(Module, AppInfo) -> + Module:lock(rebar_app_info:dir(AppInfo), rebar_app_info:source(AppInfo)). + +download(Module, TmpDir, AppInfo, State) -> + case Module:download(TmpDir, rebar_app_info:source(AppInfo), State) of + {ok, _} -> + ok; + Error -> + Error + end. + +needs_update(Module, AppInfo) -> + Module:needs_update(rebar_app_info:dir(AppInfo), rebar_app_info:source(AppInfo)). + +make_vsn(Module, AppInfo) -> + Module:make_vsn(rebar_app_info:dir(AppInfo)). diff --git a/src/rebar_resource_v2.erl b/src/rebar_resource_v2.erl new file mode 100644 index 0000000..f032f6e --- /dev/null +++ b/src/rebar_resource_v2.erl @@ -0,0 +1,147 @@ +%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- +%% ex: ts=4 sw=4 et +-module(rebar_resource_v2). + +-export([new/3, + find_resource_state/2, + format_source/1, + lock/2, + download/3, + needs_update/2, + make_vsn/3, + format_error/1]). + +-export_type([resource/0, + source/0, + type/0, + location/0, + ref/0, + resource_state/0]). + +-include("rebar.hrl"). +-include_lib("providers/include/providers.hrl"). + +-type resource() :: #resource{}. +-type source() :: {type(), location(), ref()} | {type(), location(), ref(), binary()}. +-type type() :: atom(). +-type location() :: string(). +-type ref() :: any(). +-type resource_state() :: term(). + +-callback init(type(), rebar_state:t()) -> {ok, resource()}. +-callback lock(rebar_app_info:t(), resource_state()) -> source(). +-callback download(file:filename_all(), rebar_app_info:t(), resource_state(), rebar_state:t()) -> + ok | {error, any()}. +-callback needs_update(rebar_app_info:t(), resource_state()) -> boolean(). +-callback make_vsn(rebar_app_info:t(), resource_state()) -> + {plain, string()} | {error, string()}. + +-spec new(type(), module(), term()) -> resource(). +new(Type, Module, State) -> + #resource{type=Type, + module=Module, + state=State, + implementation=?MODULE}. + +-spec find_resource(type(), [resource()]) -> {ok, resource()} | {error, not_found}. +find_resource(Type, Resources) -> + case ec_lists:find(fun(#resource{type=T}) -> T =:= Type end, Resources) of + error when is_atom(Type) -> + case code:which(Type) of + non_existing -> + {error, not_found}; + _ -> + {ok, rebar_resource:new(Type, Type, #{})} + end; + error -> + {error, not_found}; + {ok, Resource} -> + {ok, Resource} + end. + +find_resource_state(Type, Resources) -> + case lists:keyfind(Type, #resource.type, Resources) of + false -> + {error, not_found}; + #resource{state=State} -> + State + end. + +format_source({pkg, Name, Vsn, _Hash, _}) -> {pkg, Name, Vsn}; +format_source(Source) -> Source. + +lock(AppInfo, State) -> + resource_run(lock, rebar_app_info:source(AppInfo), [AppInfo], State). + +resource_run(Function, Source, Args, State) -> + Resources = rebar_state:resources(State), + case get_resource_type(Source, Resources) of + {ok, #resource{type=_, + module=Module, + state=ResourceState, + implementation=?MODULE}} -> + erlang:apply(Module, Function, Args++[ResourceState]); + {ok, #resource{type=_, + module=Module, + state=_, + implementation=rebar_resource}} -> + erlang:apply(rebar_resource, Function, [Module | Args]) + end. + +download(TmpDir, AppInfo, State) -> + resource_run(download, rebar_app_info:source(AppInfo), [TmpDir, AppInfo, State], State). + +needs_update(AppInfo, State) -> + resource_run(needs_update, rebar_app_info:source(AppInfo), [AppInfo], State). + +%% this is a special case since it is used for project apps as well, not just deps +make_vsn(AppInfo, VcsType, State) -> + Resources = rebar_state:resources(State), + case is_resource_type(VcsType, Resources) of + true -> + case find_resource(VcsType, Resources) of + {ok, #resource{type=_, + module=Module, + state=ResourceState, + implementation=?MODULE}} -> + Module:make_vsn(AppInfo, ResourceState); + {ok, #resource{type=_, + module=Module, + state=_, + implementation=rebar_resource}} -> + rebar_resource:make_vsn(Module, AppInfo) + end; + false -> + unknown + end. + +format_error({no_resource, Location, Type}) -> + io_lib:format("Cannot handle dependency ~ts.~n" + " No module found for resource type ~p.", [Location, Type]); +format_error({no_resource, Source}) -> + io_lib:format("Cannot handle dependency ~ts.~n" + " No module found for unknown resource type.", [Source]). + +is_resource_type(Type, Resources) -> + lists:any(fun(#resource{type=T}) -> T =:= Type end, Resources). + +-spec get_resource_type(term(), [resource()]) -> {ok, resource()}. +get_resource_type({Type, Location}, Resources) -> + get_resource(Type, Location, Resources); +get_resource_type({Type, Location, _}, Resources) -> + get_resource(Type, Location, Resources); +get_resource_type({Type, _, _, Location}, Resources) -> + get_resource(Type, Location, Resources); +get_resource_type(Location={Type, _, _, _, _}, Resources) -> + get_resource(Type, Location, Resources); +get_resource_type(Source, _) -> + throw(?PRV_ERROR({no_resource, Source})). + +-spec get_resource(type(), term(), [resource()]) -> {ok, resource()}. +get_resource(Type, Location, Resources) -> + case find_resource(Type, Resources) of + {error, not_found} -> + throw(?PRV_ERROR({no_resource, Location, Type})); + {ok, Resource} -> + {ok, Resource} + end. diff --git a/src/rebar_state.erl b/src/rebar_state.erl index 3586dd6..ec74eea 100644 --- a/src/rebar_state.erl +++ b/src/rebar_state.erl @@ -38,6 +38,7 @@ to_list/1, + create_resources/2, resources/1, resources/2, add_resource/2, providers/1, providers/2, add_provider/2, allow_provider_overrides/1, allow_provider_overrides/2 @@ -75,26 +76,24 @@ -spec new() -> t(). new() -> - BaseState = base_state(), + BaseState = base_state(dict:new()), BaseState#state_t{dir = rebar_dir:get_cwd()}. -spec new(list()) -> t(). new(Config) when is_list(Config) -> - BaseState = base_state(), Opts = base_opts(Config), - BaseState#state_t { dir = rebar_dir:get_cwd(), - default = Opts, - opts = Opts }. + BaseState = base_state(Opts), + BaseState#state_t{dir=rebar_dir:get_cwd(), + default=Opts}. -spec new(t() | atom(), list()) -> t(). new(Profile, Config) when is_atom(Profile) , is_list(Config) -> - BaseState = base_state(), Opts = base_opts(Config), - BaseState#state_t { dir = rebar_dir:get_cwd(), - current_profiles = [Profile], - default = Opts, - opts = Opts }; + BaseState = base_state(Opts), + BaseState#state_t{dir = rebar_dir:get_cwd(), + current_profiles = [Profile], + default = Opts}; new(ParentState=#state_t{}, Config) -> %% Load terms from rebar.config, if it exists Dir = rebar_dir:get_cwd(), @@ -129,20 +128,15 @@ deps_from_config(Dir, Config) -> [{{locks, default}, D}, {{deps, default}, Deps}] end. -base_state() -> - case application:get_env(rebar, resources) of - undefined -> - Resources = []; - {ok, Resources} -> - Resources - end, - #state_t{resources=Resources}. +base_state(Opts) -> + #state_t{opts=Opts}. base_opts(Config) -> Deps = proplists:get_value(deps, Config, []), Plugins = proplists:get_value(plugins, Config, []), ProjectPlugins = proplists:get_value(project_plugins, Config, []), - Terms = [{{deps, default}, Deps}, {{plugins, default}, Plugins}, {{project_plugins, default}, ProjectPlugins} | Config], + Terms = [{{deps, default}, Deps}, {{plugins, default}, Plugins}, + {{project_plugins, default}, ProjectPlugins} | Config], true = rebar_config:verify_config_format(Terms), dict:from_list(Terms). @@ -359,18 +353,50 @@ namespace(#state_t{namespace=Namespace}) -> namespace(State=#state_t{}, Namespace) -> State#state_t{namespace=Namespace}. --spec resources(t()) -> [{rebar_resource:type(), module()}]. +-spec resources(t()) -> [{rebar_resource_v2:type(), module()}]. resources(#state_t{resources=Resources}) -> Resources. --spec resources(t(), [{rebar_resource:type(), module()}]) -> t(). +-spec resources(t(), [{rebar_resource_v2:type(), module()}]) -> t(). resources(State, NewResources) -> - State#state_t{resources=NewResources}. - --spec add_resource(t(), {rebar_resource:type(), module()}) -> t(). -add_resource(State=#state_t{resources=Resources}, Resource) -> + lists:foldl(fun(Resource, StateAcc) -> + add_resource(StateAcc, Resource) + end, State, NewResources). + +-spec add_resource(t(), {rebar_resource_v2:type(), module()}) -> t(). +add_resource(State=#state_t{resources=Resources}, {ResourceType, ResourceModule}) -> + _ = code:ensure_loaded(ResourceModule), + Resource = case erlang:function_exported(ResourceModule, init, 2) of + true -> + case ResourceModule:init(ResourceType, State) of + {ok, R=#resource{}} -> + R; + _ -> + %% init didn't return a resource + %% must be an old resource + warn_old_resource(ResourceModule), + rebar_resource:new(ResourceType, + ResourceModule, + #{}) + end; + false -> + %% no init, must be initial implementation + warn_old_resource(ResourceModule), + rebar_resource:new(ResourceType, + ResourceModule, + #{}) + end, State#state_t{resources=[Resource | Resources]}. +warn_old_resource(ResourceModule) -> + ?WARN("Using custom resource ~s that implements a deprecated api. " + "It should be upgraded to rebar_resource_v2.", [ResourceModule]). + +create_resources(Resources, State) -> + lists:foldl(fun(R, StateAcc) -> + add_resource(StateAcc, R) + end, State, Resources). + providers(#state_t{providers=Providers}) -> Providers. diff --git a/src/rebar_string.erl b/src/rebar_string.erl index 47cb15c..d03b14e 100644 --- a/src/rebar_string.erl +++ b/src/rebar_string.erl @@ -1,7 +1,7 @@ %%% @doc Compatibility module for string functionality %%% for pre- and post-unicode support. -module(rebar_string). --export([join/2, lexemes/2, trim/3, uppercase/1, lowercase/1, chr/2]). +-export([join/2, split/2, lexemes/2, trim/3, uppercase/1, lowercase/1, chr/2]). -ifdef(unicode_str). @@ -15,6 +15,7 @@ join([], Sep) when is_list(Sep) -> join([H|T], Sep) -> H ++ lists:append([Sep ++ X || X <- T]). +split(Str, SearchPattern) -> string:split(Str, SearchPattern). lexemes(Str, SepList) -> string:lexemes(Str, SepList). trim(Str, Direction, Cluster=[_]) -> string:trim(Str, Direction, Cluster). uppercase(Str) -> string:uppercase(Str). @@ -27,6 +28,8 @@ chr([], _C, _I) -> 0. -else. join(Strings, Separator) -> string:join(Strings, Separator). +split(Str, SearchPattern) when is_list(Str) -> string:split(Str, SearchPattern); +split(Str, SearchPattern) when is_binary(Str) -> binary:split(Str, SearchPattern). lexemes(Str, SepList) -> string:tokens(Str, SepList). trim(Str, Direction, [Char]) -> Dir = case Direction of diff --git a/src/rebar_utils.erl b/src/rebar_utils.erl index 7e57d01..995d212 100644 --- a/src/rebar_utils.erl +++ b/src/rebar_utils.erl @@ -74,13 +74,15 @@ user_agent/0, reread_config/1, reread_config/2, get_proxy_auth/0, - is_list_of_strings/1]). + is_list_of_strings/1, + ssl_opts/1]). %% for internal use only -export([otp_release/0]). -include("rebar.hrl"). +-include_lib("public_key/include/OTP-PUB-KEY.hrl"). -define(ONE_LEVEL_INDENT, " "). -define(APP_NAME_INDEX, 2). @@ -710,12 +712,12 @@ escript_foldl(Fun, Acc, File) -> Error end. -vcs_vsn(Vcs, Dir, Resources) -> - case vcs_vsn_cmd(Vcs, Dir, Resources) of +vcs_vsn(AppInfo, Vcs, State) -> + case vcs_vsn_cmd(AppInfo, Vcs, State) of {plain, VsnString} -> VsnString; {cmd, CmdString} -> - vcs_vsn_invoke(CmdString, Dir); + vcs_vsn_invoke(CmdString, rebar_app_info:dir(AppInfo)); unknown -> ?ABORT("vcs_vsn: Unknown vsn format: ~p", [Vcs]); {error, Reason} -> @@ -723,23 +725,18 @@ vcs_vsn(Vcs, Dir, Resources) -> end. %% Temp work around for repos like relx that use "semver" -vcs_vsn_cmd(Vsn, _, _) when is_binary(Vsn) -> +vcs_vsn_cmd(_AppInfo, Vsn, _) when is_binary(Vsn) -> {plain, Vsn}; -vcs_vsn_cmd(VCS, Dir, Resources) when VCS =:= semver ; VCS =:= "semver" -> - vcs_vsn_cmd(git, Dir, Resources); -vcs_vsn_cmd({cmd, _Cmd}=Custom, _, _) -> +vcs_vsn_cmd(AppInfo, VCS, State) when VCS =:= semver ; VCS =:= "semver" -> + vcs_vsn_cmd(AppInfo, git, State); +vcs_vsn_cmd(_AppInfo, {cmd, _Cmd}=Custom, _) -> Custom; -vcs_vsn_cmd(VCS, Dir, Resources) when is_atom(VCS) -> - case find_resource_module(VCS, Resources) of - {ok, Module} -> - Module:make_vsn(Dir); - {error, _} -> - unknown - end; -vcs_vsn_cmd(VCS, Dir, Resources) when is_list(VCS) -> +vcs_vsn_cmd(AppInfo, VCS, State) when is_atom(VCS) -> + rebar_resource_v2:make_vsn(AppInfo, VCS, State); +vcs_vsn_cmd(AppInfo, VCS, State) when is_list(VCS) -> try list_to_existing_atom(VCS) of AVCS -> - case vcs_vsn_cmd(AVCS, Dir, Resources) of + case vcs_vsn_cmd(AppInfo, AVCS, State) of unknown -> {plain, VCS}; Other -> Other end @@ -754,19 +751,6 @@ vcs_vsn_invoke(Cmd, Dir) -> {ok, VsnString} = rebar_utils:sh(Cmd, [{cd, Dir}, {use_stdout, false}]), rebar_string:trim(VsnString, trailing, "\n"). -find_resource_module(Type, Resources) -> - case lists:keyfind(Type, 1, Resources) of - false -> - case code:which(Type) of - non_existing -> - {error, unknown}; - _ -> - {ok, Type} - end; - {Type, Module} -> - {ok, Module} - end. - %% @doc ident to the level specified -spec indent(non_neg_integer()) -> iolist(). indent(Amount) when erlang:is_integer(Amount) -> @@ -977,3 +961,116 @@ is_list_of_strings(List) when is_list(hd(List)) -> true; is_list_of_strings(List) when is_list(List) -> true. + +%%------------------------------------------------------------------------------ +%% @doc +%% Return the SSL options adequate for the project based on +%% its configuration, including for validation of certs. +%% @end +%%------------------------------------------------------------------------------ +-spec ssl_opts(Url) -> Res when + Url :: string(), + Res :: proplists:proplist(). +ssl_opts(Url) -> + case get_ssl_config() of + ssl_verify_enabled -> + ssl_opts(ssl_verify_enabled, Url); + ssl_verify_disabled -> + [{verify, verify_none}] + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% Return the SSL options adequate for the project based on +%% its configuration, including for validation of certs. +%% @end +%%------------------------------------------------------------------------------ +-spec ssl_opts(Enabled, Url) -> Res when + Enabled :: atom(), + Url :: string(), + Res :: proplists:proplist(). +ssl_opts(ssl_verify_enabled, Url) -> + case check_ssl_version() of + true -> + {ok, {_, _, Hostname, _, _, _}} = + http_uri:parse(rebar_utils:to_list(Url)), + VerifyFun = {fun ssl_verify_hostname:verify_fun/3, + [{check_hostname, Hostname}]}, + CACerts = certifi:cacerts(), + [{verify, verify_peer}, {depth, 2}, {cacerts, CACerts}, + {partial_chain, fun partial_chain/1}, {verify_fun, VerifyFun}]; + false -> + ?WARN("Insecure HTTPS request (peer verification disabled), " + "please update to OTP 17.4 or later", []), + [{verify, verify_none}] + end. + +-spec partial_chain(Certs) -> Res when + Certs :: list(any()), + Res :: unknown_ca | {trusted_ca, any()}. +partial_chain(Certs) -> + Certs1 = [{Cert, public_key:pkix_decode_cert(Cert, otp)} || Cert <- Certs], + CACerts = certifi:cacerts(), + CACerts1 = [public_key:pkix_decode_cert(Cert, otp) || Cert <- CACerts], + case ec_lists:find(fun({_, Cert}) -> + check_cert(CACerts1, Cert) + end, Certs1) of + {ok, Trusted} -> + {trusted_ca, element(1, Trusted)}; + _ -> + unknown_ca + end. + +-spec extract_public_key_info(Cert) -> Res when + Cert :: #'OTPCertificate'{tbsCertificate::#'OTPTBSCertificate'{}}, + Res :: any(). +extract_public_key_info(Cert) -> + ((Cert#'OTPCertificate'.tbsCertificate)#'OTPTBSCertificate'.subjectPublicKeyInfo). + +-spec check_cert(CACerts, Cert) -> Res when + CACerts :: list(any()), + Cert :: any(), + Res :: boolean(). +check_cert(CACerts, Cert) -> + lists:any(fun(CACert) -> + extract_public_key_info(CACert) == extract_public_key_info(Cert) + end, CACerts). + +-spec check_ssl_version() -> + boolean(). +check_ssl_version() -> + case application:get_key(ssl, vsn) of + {ok, Vsn} -> + parse_vsn(Vsn) >= {5, 3, 6}; + _ -> + false + end. + +-spec get_ssl_config() -> + ssl_verify_disabled | ssl_verify_enabled. +get_ssl_config() -> + GlobalConfigFile = rebar_dir:global_config(), + Config = rebar_config:consult_file(GlobalConfigFile), + case proplists:get_value(ssl_verify, Config, []) of + false -> + ssl_verify_disabled; + _ -> + ssl_verify_enabled + end. + +-spec parse_vsn(Vsn) -> Res when + Vsn :: string(), + Res :: {integer(), integer(), integer()}. +parse_vsn(Vsn) -> + version_pad(rebar_string:lexemes(Vsn, ".-")). + +-spec version_pad(list(nonempty_string())) -> Res when + Res :: {integer(), integer(), integer()}. +version_pad([Major]) -> + {list_to_integer(Major), 0, 0}; +version_pad([Major, Minor]) -> + {list_to_integer(Major), list_to_integer(Minor), 0}; +version_pad([Major, Minor, Patch]) -> + {list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)}; +version_pad([Major, Minor, Patch | _]) -> + {list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)}. |