diff options
Diffstat (limited to 'src')
51 files changed, 3108 insertions, 1234 deletions
diff --git a/src/rebar.app.src b/src/rebar.app.src index c96f65c..6058efc 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 @@ -39,6 +40,9 @@ {pkg, rebar_pkg_resource}, {hg, rebar_hg_resource}]}, + {compilers, [rebar_compiler_xrl, rebar_compiler_yrl, + rebar_compiler_mib, rebar_compiler_erl]}, + {providers, [rebar_prv_app_discovery, rebar_prv_as, rebar_prv_bare_compile, @@ -67,6 +71,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..f11302d 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, 3). +-define(PACKAGE_INDEX_VERSION, 5). -define(PACKAGE_TABLE, package_index). -define(INDEX_FILE, "packages.idx"). --define(REGISTRY_FILE, "registry"). +-define(HEX_AUTH_FILE, "hex.config"). +-define(PUBLIC_HEX_REPO, <<"hexpm">>). + +%% 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..059d530 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,17 @@ run_aux(State, RawArgs) -> rebar_state:set(State1, rebar_packages_cdn, CDN) end, + Compilers = application:get_env(rebar, compilers, []), + State0 = rebar_state:compilers(State2, Compilers), + + %% 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, State0), + %% 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), @@ -166,7 +175,20 @@ run_aux(State, RawArgs) -> State10 = rebar_state:code_paths(State9, default, code:get_path()), - rebar_core:init_command(rebar_state:command_args(State10, Args), Task). + case rebar_core:init_command(rebar_state:command_args(State10, Args), Task) of + {ok, State11} -> + case rebar_state:get(State11, caller, command_line) of + api -> + rebar_paths:unset_paths([deps, plugins], State11), + {ok, State11}; + _ -> + {ok, State11} + end; + Other -> + Other + end. + + %% @doc set up base configuration having to do with verbosity, where %% to find config files, and so on, and return an internal rebar3 state term. @@ -375,7 +397,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 +412,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..00eb054 100644 --- a/src/rebar_api.erl +++ b/src/rebar_api.erl @@ -9,6 +9,8 @@ expand_env_variable/3, get_arch/0, wordsize/0, + set_paths/2, + unset_paths/2, add_deps_to_path/1, restore_code_path/1, processing_base_dir/1, @@ -67,6 +69,21 @@ get_arch() -> wordsize() -> rebar_utils:wordsize(). +%% @doc Set code paths. Takes arguments of the form +%% `[plugins, deps]' or `[deps, plugins]' and ensures the +%% project's app and dependencies are set in the right order +%% for the next bit of execution +-spec set_paths(rebar_paths:targets(), rebar_state:t()) -> ok. +set_paths(List, State) -> + rebar_paths:set_paths(List, State). + +%% @doc Unsets code paths. Takes arguments of the form +%% `[plugins, deps]' or `[deps, plugins]' and ensures the +%% paths are no longer active. +-spec unset_paths(rebar_paths:targets(), rebar_state:t()) -> ok. +unset_paths(List, State) -> + rebar_paths:unset_paths(List, State). + %% @doc Add deps to the code path -spec add_deps_to_path(rebar_state:t()) -> ok. add_deps_to_path(State) -> @@ -88,4 +105,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..74681c7 100644 --- a/src/rebar_app_discover.erl +++ b/src/rebar_app_discover.erl @@ -7,10 +7,9 @@ find_unbuilt_apps/1, find_apps/1, find_apps/2, - find_apps/3, + find_apps/4, find_app/2, - find_app/3, - find_app/4]). + find_app/3]). -include("rebar.hrl"). -include_lib("providers/include/providers.hrl"). @@ -24,7 +23,7 @@ do(State, LibDirs) -> Dirs = [filename:join(BaseDir, LibDir) || LibDir <- LibDirs], RebarOpts = rebar_state:opts(State), SrcDirs = rebar_dir:src_dirs(RebarOpts, ["src"]), - Apps = find_apps(Dirs, SrcDirs, all), + Apps = find_apps(Dirs, SrcDirs, all, State), ProjectDeps = rebar_state:deps_names(State), DepsDir = rebar_dir:deps_dir(State), CurrentProfiles = rebar_state:current_profiles(State), @@ -53,7 +52,7 @@ do(State, LibDirs) -> Name = rebar_app_info:name(AppInfo), case enable(State, AppInfo) of true -> - {AppInfo1, StateAcc1} = merge_deps(AppInfo, StateAcc), + {AppInfo1, StateAcc1} = merge_opts(AppInfo, StateAcc), OutDir = filename:join(DepsDir, Name), AppInfo2 = rebar_app_info:out_dir(AppInfo1, OutDir), ProjectDeps1 = lists:delete(Name, ProjectDeps), @@ -88,34 +87,34 @@ format_error({module_list, File}) -> format_error({missing_module, Module}) -> io_lib:format("Module defined in app file missing: ~p~n", [Module]). -%% @doc handles the merging and application of profiles and overrides -%% for a given application, within its own context. --spec merge_deps(rebar_app_info:t(), rebar_state:t()) -> +%% @doc merges configuration of a project app and the top level state +%% some configuration like erl_opts must be merged into a subapp's opts +%% while plugins and hooks need to be kept defined to only either the +%% top level state or an individual application. +-spec merge_opts(rebar_app_info:t(), rebar_state:t()) -> {rebar_app_info:t(), rebar_state:t()}. -merge_deps(AppInfo, State) -> +merge_opts(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), - {C, State1} = project_app_config(AppInfo, State), - AppInfo0 = rebar_app_info:update_opts(AppInfo, Default, C), + {AppInfo1, State1} = maybe_reset_hooks_plugins(AppInfo, State), - Name = rebar_app_info:name(AppInfo0), + Name = rebar_app_info:name(AppInfo1), %% We reset the opts here to default so no profiles are applied multiple times - AppInfo1 = rebar_app_info:apply_overrides(rebar_state:get(State1, overrides, []), AppInfo0), - AppInfo2 = rebar_app_info:apply_profiles(AppInfo1, CurrentProfiles), + AppInfo2 = rebar_app_info:apply_overrides(rebar_state:get(State1, overrides, []), AppInfo1), + AppInfo3 = rebar_app_info:apply_profiles(AppInfo2, CurrentProfiles), %% Will throw an exception if checks fail - rebar_app_info:verify_otp_vsn(AppInfo2), + rebar_app_info:verify_otp_vsn(AppInfo3), State2 = lists:foldl(fun(Profile, StateAcc) -> - handle_profile(Profile, Name, AppInfo2, StateAcc) + handle_profile(Profile, Name, AppInfo3, StateAcc) end, State1, lists:reverse(CurrentProfiles)), - {AppInfo2, State2}. + {AppInfo3, State2}. %% @doc Applies a given profile for an app, ensuring the deps %% match the context it will require. @@ -153,30 +152,32 @@ parse_profile_deps(Profile, Name, Deps, Opts, State) -> ,Locks ,1). -%% @doc Find the app-level config and return the state updated -%% with the relevant app-level data. --spec project_app_config(rebar_app_info:t(), rebar_state:t()) -> - {Config, rebar_state:t()} when - Config :: [any()]. -project_app_config(AppInfo, State) -> - C = rebar_config:consult(rebar_app_info:dir(AppInfo)), +%% reset the State hooks if there is a top level application +-spec maybe_reset_hooks_plugins(AppInfo, State) -> {AppInfo, State} when + AppInfo :: rebar_app_info:t(), + State :: rebar_state:t(). +maybe_reset_hooks_plugins(AppInfo, State) -> Dir = rebar_app_info:dir(AppInfo), - Opts = maybe_reset_hooks(Dir, rebar_state:opts(State), State), - {C, rebar_state:opts(State, Opts)}. - -%% @private Check if the app is at the root of the project. -%% If it is, then drop the hooks from the config so they aren't run twice --spec maybe_reset_hooks(file:filename(), Opts, rebar_state:t()) -> Opts when - Opts :: rebar_dict(). -maybe_reset_hooks(Dir, Opts, State) -> + CurrentProfiles = rebar_state:current_profiles(State), case ec_file:real_dir_path(rebar_dir:root_dir(State)) of Dir -> - CurrentProfiles = rebar_state:current_profiles(State), - reset_hooks(Opts, CurrentProfiles); + Opts = reset_hooks(rebar_state:opts(State), CurrentProfiles), + State1 = rebar_state:opts(State, Opts), + + %% set plugins to empty since this is an app at the top level + %% and top level plugins are installed in run_aux + AppInfo1 = rebar_app_info:set(rebar_app_info:set(AppInfo, {plugins,default}, []), plugins, []), + + {AppInfo1, State1}; _ -> - Opts + %% if not in the top root directory then we need to merge in the + %% default state opts to this subapp's opts + Default = reset_hooks(rebar_state:default(State), CurrentProfiles), + AppInfo1 = rebar_app_info:update_opts(AppInfo, Default), + {AppInfo1, State} end. + %% @doc make the hooks empty for a given set of options -spec reset_hooks(Opts, Profiles) -> Opts when @@ -205,8 +206,8 @@ 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"]), - app_dirs(LibDir, SrcDirs) + {_, SrcDirs} = find_config_src(LibDir, ["src"]), + app_dirs(LibDir, SrcDirs) end, LibDirs). %% @private find the directories for all apps based on their source dirs @@ -264,11 +265,11 @@ find_apps(LibDirs, Validate) -> %% @doc for each directory passed, with the configured source directories, %% find all apps according to the validity rule passed in. %% Returns all the related app info records. --spec find_apps([file:filename_all()], [file:filename_all()], valid | invalid | all) -> [rebar_app_info:t()]. -find_apps(LibDirs, SrcDirs, Validate) -> +-spec find_apps([file:filename_all()], [file:filename_all()], valid | invalid | all, rebar_state:t()) -> [rebar_app_info:t()]. +find_apps(LibDirs, SrcDirs, Validate, State) -> rebar_utils:filtermap( fun({AppDir, AppSrcDirs}) -> - find_app(rebar_app_info:new(), AppDir, AppSrcDirs, Validate) + find_app(rebar_app_info:new(), AppDir, AppSrcDirs, Validate, State) end, all_app_dirs(LibDirs, SrcDirs) ). @@ -278,8 +279,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,16 +293,35 @@ 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 %% the directories where source files can be located. Returns the related %% app info record. -spec find_app(rebar_app_info:t(), file:filename_all(), - [file:filename_all()], valid | invalid | all) -> + [file:filename_all()], valid | invalid | all, rebar_state:t()) -> {true, rebar_app_info:t()} | false. +find_app(AppInfo, AppDir, SrcDirs, Validate, State) -> + AppInfo1 = case ec_file:real_dir_path(rebar_dir:root_dir(State)) of + AppDir -> + Opts = rebar_state:opts(State), + rebar_app_info:default(rebar_app_info:opts(AppInfo, Opts), Opts); + _ -> + Config = rebar_config:consult(AppDir), + rebar_app_info:update_opts(AppInfo, rebar_app_info:opts(AppInfo), Config) + end, + find_app_(AppInfo1, AppDir, SrcDirs, Validate). + 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 +352,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 +421,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, rebar_app_info:project_type(AppInfo, mix)}; + 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 +463,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..9dfe278 100644 --- a/src/rebar_app_info.erl +++ b/src/rebar_app_info.erl @@ -7,6 +7,8 @@ new/4, new/5, update_opts/3, + update_opts/2, + update_opts_deps/2, discover/1, name/1, name/2, @@ -22,7 +24,6 @@ parent/2, original_vsn/1, original_vsn/2, - ebin_dir/1, priv_dir/1, applications/1, applications/2, @@ -36,6 +37,8 @@ dir/2, out_dir/1, out_dir/2, + ebin_dir/1, + ebin_dir/2, default/1, default/2, opts/1, @@ -43,16 +46,18 @@ get/2, get/3, set/3, - resource_type/1, - resource_type/2, source/1, source/2, + project_type/1, + project_type/2, is_lock/1, is_lock/2, is_checkout/1, is_checkout/2, valid/1, valid/2, + is_available/1, + is_available/2, verify_otp_vsn/1, has_all_artifacts/1, @@ -66,13 +71,16 @@ -include("rebar.hrl"). -include_lib("providers/include/providers.hrl"). --export_type([t/0]). +-export_type([t/0, + project_type/0]). + +-type project_type() :: rebar3 | mix | undefined. -record(app_info_t, {name :: binary() | undefined, 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 +91,13 @@ dep_level=0 :: integer(), dir :: file:name(), out_dir :: file:name(), - resource_type :: pkg | src | undefined, + ebin_dir :: file:name(), source :: string() | tuple() | checkout | undefined, is_lock=false :: boolean(), is_checkout=false :: boolean(), - valid :: boolean() | undefined}). + valid :: boolean() | undefined, + project_type :: project_type(), + is_available=false :: boolean()}). %%============================================================================ %% types @@ -123,7 +133,8 @@ new(AppName, Vsn, Dir) -> {ok, #app_info_t{name=rebar_utils:to_binary(AppName), original_vsn=Vsn, dir=rebar_utils:to_list(Dir), - out_dir=rebar_utils:to_list(Dir)}}. + out_dir=rebar_utils:to_list(Dir), + ebin_dir=filename:join(rebar_utils:to_list(Dir), "ebin")}}. %% @doc build a complete version of the app info with all fields set. -spec new(atom() | binary() | string(), binary() | string(), file:name(), list()) -> @@ -133,6 +144,7 @@ new(AppName, Vsn, Dir, Deps) -> original_vsn=Vsn, dir=rebar_utils:to_list(Dir), out_dir=rebar_utils:to_list(Dir), + ebin_dir=filename:join(rebar_utils:to_list(Dir), "ebin"), deps=Deps}}. %% @doc build a complete version of the app info with all fields set. @@ -144,18 +156,21 @@ new(Parent, AppName, Vsn, Dir, Deps) -> original_vsn=Vsn, dir=rebar_utils:to_list(Dir), out_dir=rebar_utils:to_list(Dir), + ebin_dir=filename:join(rebar_utils:to_list(Dir), "ebin"), deps=Deps}}. %% @doc update the opts based on the contents of a config %% 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) + deps_from_config(dir(AppInfo), proplists:get_value(deps, Config, [])) end, Plugins = proplists:get_value(plugins, Config, []), @@ -165,15 +180,32 @@ 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 current app info opts by merging in a new dict of opts +-spec update_opts(t(), rebar_dict()) -> t(). +update_opts(AppInfo=#app_info_t{opts=LocalOpts}, Opts) -> + NewOpts = rebar_opts:merge_opts(LocalOpts, Opts), + 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()}, ...]. -deps_from_config(Dir, Config) -> +deps_from_config(Dir, ConfigDeps) -> case rebar_config:consult_lock_file(filename:join(Dir, ?LOCK_FILE)) of [] -> - [{{deps, default}, proplists:get_value(deps, Config, [])}]; + [{{deps, default}, ConfigDeps}]; D -> %% We want the top level deps only from the lock file. %% This ensures deterministic overrides for configs. @@ -350,13 +382,13 @@ 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}. @@ -426,28 +458,27 @@ out_dir(#app_info_t{out_dir=OutDir}) -> %% should go -spec out_dir(t(), file:name()) -> t(). out_dir(AppInfo=#app_info_t{}, OutDir) -> - AppInfo#app_info_t{out_dir=rebar_utils:to_list(OutDir)}. + AppInfo#app_info_t{out_dir=rebar_utils:to_list(OutDir), + ebin_dir=filename:join(rebar_utils:to_list(OutDir), "ebin")}. %% @doc gets the directory where ebin files for the app should go -spec ebin_dir(t()) -> file:name(). -ebin_dir(#app_info_t{out_dir=OutDir}) -> - rebar_utils:to_list(filename:join(OutDir, "ebin")). +ebin_dir(#app_info_t{ebin_dir=undefined, + out_dir=OutDir}) -> + filename:join(rebar_utils:to_list(OutDir), "ebin"); +ebin_dir(#app_info_t{ebin_dir=EbinDir}) -> + EbinDir. + +%% @doc sets the directory where beam files should go +-spec ebin_dir(t(), file:name()) -> t(). +ebin_dir(AppInfo, EbinDir) -> + AppInfo#app_info_t{ebin_dir=EbinDir}. %% @doc gets the directory where private files for the app should go -spec priv_dir(t()) -> file:name(). 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 +509,28 @@ 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 +-spec project_type(t()) -> atom(). +project_type(#app_info_t{project_type=ProjectType}) -> + ProjectType. + +%% @doc +-spec project_type(t(), atom()) -> t(). +project_type(AppInfo=#app_info_t{}, ProjectType) -> + AppInfo#app_info_t{project_type=ProjectType}. + + %% @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..605944e 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,38 @@ 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, + retired=Retired} = Package, + maybe_warn_retired(PkgName, PkgVsn1, Hash, Retired), + PkgVsn2 = list_to_binary(lists:flatten(ec_semver:format(PkgVsn1))), + AppInfo1 = rebar_app_info:source(AppInfo, {pkg, PkgName, PkgVsn2, Hash1, RepoConfig}), + AppInfo2 = rebar_app_info:update_opts_deps(AppInfo1, Deps), + rebar_app_info:original_vsn(AppInfo2, PkgVsn2); + 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, Constraint}) -> + io_lib:format("Package not found in any repo: ~ts ~ts", [Name, Constraint]); 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,17 +286,31 @@ 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. +maybe_warn_retired(_, _, _, false) -> + ok; +maybe_warn_retired(_, _, Hash, _) when is_binary(Hash) -> + %% don't warn if this is a lock + ok; +maybe_warn_retired(Name, Vsn, _, R=#{reason := Reason}) -> + Message = maps:get(message, R, ""), + ?WARN("Warning: package ~s-~s is retired: (~s) ~s", + [Name, ec_semver:format(Vsn), retire_reason(Reason), Message]); +maybe_warn_retired(_, _, _, _) -> + ok. + +%% TODO: move to hex_core +retire_reason('RETIRED_OTHER') -> + "other"; +retire_reason('RETIRED_INVALID') -> + "invalid"; +retire_reason('RETIRED_SECURITY') -> + "security"; +retire_reason('RETIRED_DEPRECATED') -> + "deprecated"; +retire_reason('RETIRED_RENAMED') -> + "renamed"; +retire_reason(_Other) -> + "other". %% @private checks that all the beam files have been properly %% created. diff --git a/src/rebar_base_compiler.erl b/src/rebar_base_compiler.erl index 3f273f1..2fb3a12 100644 --- a/src/rebar_base_compiler.erl +++ b/src/rebar_base_compiler.erl @@ -33,6 +33,8 @@ run/8, ok_tuple/2, error_tuple/4, + report/1, + maybe_report/1, format_error_source/2]). -type desc() :: term(). @@ -94,14 +96,14 @@ run(Config, FirstFiles, SourceDir, SourceExt, TargetDir, TargetExt, TargetDir :: file:filename(), SourceExt :: string(), TargetExt :: string(). -run(Config, FirstFiles, SourceDir, SourceExt, TargetDir, TargetExt, +run(Config, FirstFiles, SourceDirs, SourceExt, TargetDir, TargetExt, Compile3Fn, Opts) -> %% Convert simple extension to proper regex SourceExtRe = "^(?!\\._).*\\" ++ SourceExt ++ [$$], Recursive = proplists:get_value(recursive, Opts, true), %% Find all possible source files - FoundFiles = rebar_utils:find_files(SourceDir, SourceExtRe, Recursive), + FoundFiles = rebar_utils:find_files_in_dirs(SourceDirs, SourceExtRe, Recursive), %% Remove first files from found files RestFiles = [Source || Source <- FoundFiles, not lists:member(Source, FirstFiles)], @@ -111,7 +113,7 @@ run(Config, FirstFiles, SourceDir, SourceExt, TargetDir, TargetExt, run(Config, FirstFiles, RestFiles, fun(S, C) -> - Target = target_file(S, SourceDir, SourceExt, + Target = target_file(S, SourceExt, TargetDir, TargetExt), simple_compile_wrapper(S, Target, Compile3Fn, C, CheckLastMod) end). @@ -160,32 +162,15 @@ simple_compile_wrapper(Source, Target, Compile3Fn, Config, true) -> %% @private take a basic source set of file fragments and a target location, %% create a file path and name for a compile artifact. --spec target_file(SourceFile, SourceDir, SourceExt, TargetDir, TargetExt) -> File when +-spec target_file(SourceFile, SourceExt, TargetDir, TargetExt) -> File when SourceFile :: file:filename(), - SourceDir :: file:filename(), TargetDir :: file:filename(), SourceExt :: string(), TargetExt :: string(), File :: file:filename(). -target_file(SourceFile, SourceDir, SourceExt, TargetDir, TargetExt) -> - BaseFile = remove_common_path(SourceFile, SourceDir), - filename:join([TargetDir, filename:basename(BaseFile, SourceExt) ++ TargetExt]). - -%% @private removes the common prefix between two file paths. -%% The remainder of the first file path passed will have its ending returned -%% when either path starts diverging. --spec remove_common_path(file:filename(), file:filename()) -> file:filename(). -remove_common_path(Fname, Path) -> - remove_common_path1(filename:split(Fname), filename:split(Path)). - -%% @private given two lists of file fragments, discard the identical -%% prefixed sections, and return the final bit of the first operand -%% as a filename. --spec remove_common_path1([string()], [string()]) -> file:filename(). -remove_common_path1([Part | RestFilename], [Part | RestPath]) -> - remove_common_path1(RestFilename, RestPath); -remove_common_path1(FilenameParts, _) -> - filename:join(FilenameParts). +target_file(SourceFile, SourceExt, TargetDir, TargetExt) -> + %% BaseFile = remove_common_path(SourceFile, SourceDir), + filename:join([TargetDir, filename:basename(SourceFile, SourceExt) ++ TargetExt]). %% @private runs the compile function `CompileFn' on every file %% passed internally, along with the related project configuration. @@ -264,14 +249,24 @@ report(Messages) -> -spec format_errors(_, Extra, [err_or_warn()]) -> [string()] when Extra :: string(). format_errors(_MainSource, Extra, Errors) -> - [begin - [format_error(Source, Extra, Desc) || Desc <- Descs] - end + [[format_error(Source, Extra, Desc) || Desc <- Descs] || {Source, Descs} <- Errors]. %% @private format compiler errors into proper outputtable strings -spec format_error(file:filename(), Extra, err_or_warn()) -> string() when Extra :: string(). +format_error(Source, Extra, {Line, Mod=epp, Desc={include,lib,File}}) -> + %% Special case for include file errors, overtaking the default one + BaseDesc = Mod:format_error(Desc), + Friendly = case filename:split(File) of + [Lib, "include", _] -> + io_lib:format("; Make sure ~s is in your app " + "file's 'applications' list", [Lib]); + _ -> + "" + end, + FriendlyDesc = BaseDesc ++ Friendly, + ?FMT("~ts:~w: ~ts~ts~n", [Source, Line, Extra, FriendlyDesc]); format_error(Source, Extra, {{Line, Column}, Mod, Desc}) -> ErrorDesc = Mod:format_error(Desc), ?FMT("~ts:~w:~w: ~ts~ts~n", [Source, Line, Column, Extra, ErrorDesc]); diff --git a/src/rebar_compiler.erl b/src/rebar_compiler.erl new file mode 100644 index 0000000..6e94cb2 --- /dev/null +++ b/src/rebar_compiler.erl @@ -0,0 +1,303 @@ +-module(rebar_compiler). + +-export([compile_all/2, + clean/2, + + ok_tuple/2, + error_tuple/4, + maybe_report/1, + format_error_source/2, + report/1]). + +-include("rebar.hrl"). + +-type extension() :: string(). +-type out_mappings() :: [{extension(), file:filename()}]. + +-callback context(rebar_app_info:t()) -> #{src_dirs => [file:dirname()], + include_dirs => [file:dirname()], + src_ext => extension(), + out_mappings => out_mappings()}. +-callback needed_files(digraph:graph(), [file:filename()], rebar_app_info:t()) -> [file:filename()]. +-callback dependencies(file:filename(), file:dirname(), [file:dirname()]) -> [file:filename()]. +-callback compile(file:filename(), out_mappings(), rebar_dict(), list()) -> + ok | {ok, [string()]} | {ok, [string()], [string()]}. + +-define(DAG_VSN, 2). +-define(DAG_FILE, "source.dag"). +-type dag_v() :: {digraph:vertex(), term()} | 'false'. +-type dag_e() :: {digraph:vertex(), digraph:vertex()}. +-type dag() :: {list(dag_v()), list(dag_e()), list(string())}. +-record(dag, {vsn = ?DAG_VSN :: pos_integer(), + info = {[], [], []} :: dag()}). + +-define(RE_PREFIX, "^(?!\\._)"). + +compile_all(Compilers, AppInfo) -> + EbinDir = rebar_utils:to_list(rebar_app_info:ebin_dir(AppInfo)), + %% Make sure that outdir is on the path + ok = rebar_file_utils:ensure_dir(EbinDir), + true = code:add_patha(filename:absname(EbinDir)), + + %% necessary for erlang:function_exported/3 to work as expected + %% called here for clarity as it's required by both opts_changed/2 + %% and erl_compiler_opts_set/0 in needed_files + _ = code:ensure_loaded(compile), + + lists:foreach(fun(CompilerMod) -> + run(CompilerMod, AppInfo), + run_on_extra_src_dirs(CompilerMod, AppInfo, fun run/2) + end, Compilers), + ok. + +run(CompilerMod, AppInfo) -> + #{src_dirs := SrcDirs, + include_dirs := InclDirs, + src_ext := SrcExt, + out_mappings := Mappings} = CompilerMod:context(AppInfo), + + BaseDir = rebar_utils:to_list(rebar_app_info:dir(AppInfo)), + EbinDir = rebar_utils:to_list(rebar_app_info:ebin_dir(AppInfo)), + + BaseOpts = rebar_app_info:opts(AppInfo), + AbsInclDirs = [filename:join(BaseDir, InclDir) || InclDir <- InclDirs], + FoundFiles = find_source_files(BaseDir, SrcExt, SrcDirs, BaseOpts), + + OutDir = rebar_app_info:out_dir(AppInfo), + AbsSrcDirs = [filename:join(BaseDir, SrcDir) || SrcDir <- SrcDirs], + G = init_dag(CompilerMod, AbsInclDirs, AbsSrcDirs, FoundFiles, OutDir, EbinDir), + {{FirstFiles, FirstFileOpts}, {RestFiles, Opts}} = CompilerMod:needed_files(G, FoundFiles, AppInfo), + true = digraph:delete(G), + + compile_each(FirstFiles, FirstFileOpts, BaseOpts, Mappings, CompilerMod), + compile_each(RestFiles, Opts, BaseOpts, Mappings, CompilerMod). + +compile_each([], _Opts, _Config, _Outs, _CompilerMod) -> + ok; +compile_each([Source | Rest], Opts, Config, Outs, CompilerMod) -> + case CompilerMod:compile(Source, Outs, Config, Opts) of + ok -> + ?DEBUG("~tsCompiled ~ts", [rebar_utils:indent(1), filename:basename(Source)]); + {ok, Warnings} -> + report(Warnings), + ?DEBUG("~tsCompiled ~ts", [rebar_utils:indent(1), filename:basename(Source)]); + skipped -> + ?DEBUG("~tsSkipped ~ts", [rebar_utils:indent(1), filename:basename(Source)]); + Error -> + NewSource = format_error_source(Source, Config), + ?ERROR("Compiling ~ts failed", [NewSource]), + maybe_report(Error), + ?DEBUG("Compilation failed: ~p", [Error]), + ?FAIL + end, + compile_each(Rest, Opts, Config, Outs, CompilerMod). + +%% @doc remove compiled artifacts from an AppDir. +-spec clean([module()], rebar_app_info:t()) -> 'ok'. +clean(Compilers, AppInfo) -> + lists:foreach(fun(CompilerMod) -> + clean_(CompilerMod, AppInfo), + run_on_extra_src_dirs(CompilerMod, AppInfo, fun clean_/2) + end, Compilers). + +clean_(CompilerMod, AppInfo) -> + #{src_dirs := SrcDirs, + src_ext := SrcExt} = CompilerMod:context(AppInfo), + BaseDir = rebar_app_info:dir(AppInfo), + Opts = rebar_app_info:opts(AppInfo), + EbinDir = rebar_app_info:ebin_dir(AppInfo), + + FoundFiles = find_source_files(BaseDir, SrcExt, SrcDirs, Opts), + CompilerMod:clean(FoundFiles, AppInfo), + rebar_file_utils:rm_rf(dag_file(CompilerMod, EbinDir)). + + +run_on_extra_src_dirs(CompilerMod, AppInfo, Fun) -> + ExtraDirs = rebar_dir:extra_src_dirs(rebar_app_info:opts(AppInfo), []), + run_on_extra_src_dirs(ExtraDirs, CompilerMod, AppInfo, Fun). + +run_on_extra_src_dirs([], _CompilerMod, _AppInfo, _Fun) -> + ok; +run_on_extra_src_dirs([Dir | Rest], CompilerMod, AppInfo, Fun) -> + case filelib:is_dir(filename:join(rebar_app_info:dir(AppInfo), Dir)) of + true -> + EbinDir = filename:join(rebar_app_info:out_dir(AppInfo), Dir), + AppInfo1 = rebar_app_info:ebin_dir(AppInfo, EbinDir), + AppInfo2 = rebar_app_info:set(AppInfo1, src_dirs, [Dir]), + AppInfo3 = rebar_app_info:set(AppInfo2, extra_src_dirs, ["src"]), + Fun(CompilerMod, AppInfo3); + _ -> + ok + end, + run_on_extra_src_dirs(Rest, CompilerMod, AppInfo, Fun). + +%% These functions are here for the ultimate goal of getting rid of +%% rebar_base_compiler. This can't be done because of existing plugins. + +ok_tuple(Source, Ws) -> + rebar_base_compiler:ok_tuple(Source, Ws). + +error_tuple(Source, Es, Ws, Opts) -> + rebar_base_compiler:error_tuple(Source, Es, Ws, Opts). + +maybe_report(Reportable) -> + rebar_base_compiler:maybe_report(Reportable). + +format_error_source(Path, Opts) -> + rebar_base_compiler:format_error_source(Path, Opts). + +report(Messages) -> + rebar_base_compiler:report(Messages). + +%% private functions + +find_source_files(BaseDir, SrcExt, SrcDirs, Opts) -> + SourceExtRe = "^(?!\\._).*\\" ++ SrcExt ++ [$$], + lists:flatmap(fun(SrcDir) -> + Recursive = rebar_dir:recursive(Opts, SrcDir), + rebar_utils:find_files_in_dirs([filename:join(BaseDir, SrcDir)], SourceExtRe, Recursive) + end, SrcDirs). + +dag_file(CompilerMod, Dir) -> + filename:join([rebar_dir:local_cache_dir(Dir), CompilerMod, ?DAG_FILE]). + +%% private graph functions + +%% Get dependency graph of given Erls files and their dependencies (header files, +%% parse transforms, behaviours etc.) located in their directories or given +%% InclDirs. Note that last modification times stored in vertices already respect +%% dependencies induced by given graph G. +init_dag(Compiler, InclDirs, SrcDirs, Erls, Dir, EbinDir) -> + G = digraph:new([acyclic]), + try restore_dag(Compiler, G, InclDirs, Dir) + catch + _:_ -> + ?WARN("Failed to restore ~ts file. Discarding it.~n", [dag_file(Compiler, Dir)]), + file:delete(dag_file(Compiler, Dir)) + end, + Dirs = lists:usort(InclDirs ++ SrcDirs), + %% A source file may have been renamed or deleted. Remove it from the graph + %% and remove any beam file for that source if it exists. + Modified = maybe_rm_beams_and_edges(G, EbinDir, Erls), + Modified1 = lists:foldl(update_dag_fun(G, Compiler, Dirs), Modified, Erls), + if Modified1 -> store_dag(Compiler, G, InclDirs, Dir); not Modified1 -> ok end, + G. + +maybe_rm_beams_and_edges(G, Dir, Files) -> + Vertices = digraph:vertices(G), + case lists:filter(fun(File) -> + case filename:extension(File) =:= ".erl" of + true -> + maybe_rm_beam_and_edge(G, Dir, File); + false -> + false + end + end, lists:sort(Vertices) -- lists:sort(Files)) of + [] -> + false; + _ -> + true + end. + +maybe_rm_beam_and_edge(G, OutDir, Source) -> + %% This is NOT a double check it is the only check that the source file is actually gone + case filelib:is_regular(Source) of + true -> + %% Actually exists, don't delete + false; + false -> + Target = target_base(OutDir, Source) ++ ".beam", + ?DEBUG("Source ~ts is gone, deleting previous beam file if it exists ~ts", [Source, Target]), + file:delete(Target), + digraph:del_vertex(G, Source), + true + end. + + +target_base(OutDir, Source) -> + filename:join(OutDir, filename:basename(Source, ".erl")). + +restore_dag(Compiler, G, InclDirs, Dir) -> + case file:read_file(dag_file(Compiler, Dir)) of + {ok, Data} -> + % Since externally passed InclDirs can influence dependency graph (see + % modify_dag), we have to check here that they didn't change. + #dag{vsn=?DAG_VSN, info={Vs, Es, InclDirs}} = + binary_to_term(Data), + lists:foreach( + fun({V, LastUpdated}) -> + digraph:add_vertex(G, V, LastUpdated) + end, Vs), + lists:foreach( + fun({_, V1, V2, _}) -> + digraph:add_edge(G, V1, V2) + end, Es); + {error, _} -> + ok + end. + +store_dag(Compiler, G, InclDirs, Dir) -> + Vs = lists:map(fun(V) -> digraph:vertex(G, V) end, digraph:vertices(G)), + Es = lists:map(fun(E) -> digraph:edge(G, E) end, digraph:edges(G)), + File = dag_file(Compiler, Dir), + ok = filelib:ensure_dir(File), + Data = term_to_binary(#dag{info={Vs, Es, InclDirs}}, [{compressed, 2}]), + file:write_file(File, Data). + +update_dag(G, Compiler, Dirs, Source) -> + case digraph:vertex(G, Source) of + {_, LastUpdated} -> + case filelib:last_modified(Source) of + 0 -> + %% The file doesn't exist anymore, + %% erase it from the graph. + %% All the edges will be erased automatically. + digraph:del_vertex(G, Source), + modified; + LastModified when LastUpdated < LastModified -> + modify_dag(G, Compiler, Source, LastModified, filename:dirname(Source), Dirs); + _ -> + Modified = lists:foldl( + update_dag_fun(G, Compiler, Dirs), + false, digraph:out_neighbours(G, Source)), + MaxModified = update_max_modified_deps(G, Source), + case Modified orelse MaxModified > LastUpdated of + true -> modified; + false -> unmodified + end + end; + false -> + modify_dag(G, Compiler, Source, filelib:last_modified(Source), filename:dirname(Source), Dirs) + end. + +modify_dag(G, Compiler, Source, LastModified, SourceDir, Dirs) -> + AbsIncls = Compiler:dependencies(Source, SourceDir, Dirs), + digraph:add_vertex(G, Source, LastModified), + digraph:del_edges(G, digraph:out_edges(G, Source)), + lists:foreach( + fun(Incl) -> + update_dag(G, Compiler, Dirs, Incl), + digraph:add_edge(G, Source, Incl) + end, AbsIncls), + modified. + +update_dag_fun(G, Compiler, Dirs) -> + fun(Erl, Modified) -> + case update_dag(G, Compiler, Dirs, Erl) of + modified -> true; + unmodified -> Modified + end + end. + +update_max_modified_deps(G, Source) -> + MaxModified = + lists:foldl(fun(File, Acc) -> + case digraph:vertex(G, File) of + {_, MaxModified} when MaxModified > Acc -> + MaxModified; + _ -> + Acc + end + end, 0, [Source | digraph:out_neighbours(G, Source)]), + digraph:add_vertex(G, Source, MaxModified), + MaxModified. diff --git a/src/rebar_compiler_erl.erl b/src/rebar_compiler_erl.erl new file mode 100644 index 0000000..d9bc69b --- /dev/null +++ b/src/rebar_compiler_erl.erl @@ -0,0 +1,368 @@ +-module(rebar_compiler_erl). + +-behaviour(rebar_compiler). + +-export([context/1, + needed_files/3, + dependencies/3, + compile/4, + clean/2]). + +-include("rebar.hrl"). + +context(AppInfo) -> + EbinDir = rebar_app_info:ebin_dir(AppInfo), + Mappings = [{".beam", EbinDir}], + + OutDir = rebar_app_info:dir(AppInfo), + SrcDirs = rebar_dir:src_dirs(rebar_app_info:opts(AppInfo), ["src"]), + ExistingSrcDirs = lists:filter(fun(D) -> + ec_file:is_dir(filename:join(OutDir, D)) + end, SrcDirs), + + RebarOpts = rebar_app_info:opts(AppInfo), + ErlOpts = rebar_opts:erl_opts(RebarOpts), + ErlOptIncludes = proplists:get_all_values(i, ErlOpts), + InclDirs = lists:map(fun(Incl) -> filename:absname(Incl) end, ErlOptIncludes), + + #{src_dirs => ExistingSrcDirs, + include_dirs => [filename:join([OutDir, "include"]) | InclDirs], + src_ext => ".erl", + out_mappings => Mappings}. + + +needed_files(Graph, FoundFiles, AppInfo) -> + OutDir = rebar_app_info:out_dir(AppInfo), + Dir = rebar_app_info:dir(AppInfo), + EbinDir = rebar_app_info:ebin_dir(AppInfo), + RebarOpts = rebar_app_info:opts(AppInfo), + ErlOpts = rebar_opts:erl_opts(RebarOpts), + ?DEBUG("erlopts ~p", [ErlOpts]), + ?DEBUG("files to compile ~p", [FoundFiles]), + + %% Make sure that the ebin dir is on the path + ok = rebar_file_utils:ensure_dir(EbinDir), + true = code:add_patha(filename:absname(EbinDir)), + + {ParseTransforms, Rest} = split_source_files(FoundFiles, ErlOpts), + NeededErlFiles = case needed_files(Graph, ErlOpts, RebarOpts, OutDir, EbinDir, ParseTransforms) of + [] -> + needed_files(Graph, ErlOpts, RebarOpts, OutDir, EbinDir, Rest); + _ -> + %% at least one parse transform in the opts needs updating, so recompile all + FoundFiles + end, + {ErlFirstFiles, ErlOptsFirst} = erl_first_files(RebarOpts, ErlOpts, Dir, NeededErlFiles), + SubGraph = digraph_utils:subgraph(Graph, NeededErlFiles), + DepErlsOrdered = digraph_utils:topsort(SubGraph), + OtherErls = lists:reverse(DepErlsOrdered), + + PrivIncludes = [{i, filename:join(OutDir, Src)} + || Src <- rebar_dir:all_src_dirs(RebarOpts, ["src"], [])], + AdditionalOpts = PrivIncludes ++ [{i, filename:join(OutDir, "include")}, {i, OutDir}, return], + + true = digraph:delete(SubGraph), + + {{ErlFirstFiles, ErlOptsFirst ++ AdditionalOpts}, + {[Erl || Erl <- OtherErls, + not lists:member(Erl, ErlFirstFiles)], ErlOpts ++ AdditionalOpts}}. + +dependencies(Source, SourceDir, Dirs) -> + {ok, Fd} = file:open(Source, [read]), + Incls = parse_attrs(Fd, [], SourceDir), + AbsIncls = expand_file_names(Incls, Dirs), + ok = file:close(Fd), + AbsIncls. + +compile(Source, [{_, OutDir}], Config, ErlOpts) -> + case compile:file(Source, [{outdir, OutDir} | ErlOpts]) of + {ok, _Mod} -> + ok; + {ok, _Mod, []} -> + ok; + {ok, _Mod, Ws} -> + FormattedWs = format_error_sources(Ws, Config), + rebar_compiler:ok_tuple(Source, FormattedWs); + {error, Es, Ws} -> + error_tuple(Source, Es, Ws, Config, ErlOpts); + error -> + error + end. + +clean(Files, AppInfo) -> + EbinDir = rebar_app_info:ebin_dir(AppInfo), + [begin + Source = filename:basename(File, ".erl"), + Target = target_base(EbinDir, Source) ++ ".beam", + file:delete(Target) + end || File <- Files]. + +%% + +error_tuple(Module, Es, Ws, AllOpts, Opts) -> + FormattedEs = format_error_sources(Es, AllOpts), + FormattedWs = format_error_sources(Ws, AllOpts), + rebar_compiler:error_tuple(Module, FormattedEs, FormattedWs, Opts). + +format_error_sources(Es, Opts) -> + [{rebar_compiler:format_error_source(Src, Opts), Desc} + || {Src, Desc} <- Es]. + +%% Get files which need to be compiled first, i.e. those specified in erl_first_files +%% and parse_transform options. Also produce specific erl_opts for these first +%% files, so that yet to be compiled parse transformations are excluded from it. +erl_first_files(Opts, ErlOpts, Dir, NeededErlFiles) -> + ErlFirstFilesConf = rebar_opts:get(Opts, erl_first_files, []), + valid_erl_first_conf(ErlFirstFilesConf), + NeededSrcDirs = lists:usort(lists:map(fun filename:dirname/1, NeededErlFiles)), + %% NOTE: order of files here is important! + ErlFirstFiles = + [filename:join(Dir, File) || File <- ErlFirstFilesConf, + lists:member(filename:join(Dir, File), NeededErlFiles)], + {ParseTransforms, ParseTransformsErls} = + lists:unzip(lists:flatmap( + fun(PT) -> + PTerls = [filename:join(D, module_to_erl(PT)) || D <- NeededSrcDirs], + [{PT, PTerl} || PTerl <- PTerls, lists:member(PTerl, NeededErlFiles)] + end, proplists:get_all_values(parse_transform, ErlOpts))), + ErlOptsFirst = lists:filter(fun({parse_transform, PT}) -> + not lists:member(PT, ParseTransforms); + (_) -> + true + end, ErlOpts), + {ErlFirstFiles ++ ParseTransformsErls, ErlOptsFirst}. + +split_source_files(SourceFiles, ErlOpts) -> + ParseTransforms = proplists:get_all_values(parse_transform, ErlOpts), + lists:partition(fun(Source) -> + lists:member(filename_to_atom(Source), ParseTransforms) + end, SourceFiles). + +filename_to_atom(F) -> list_to_atom(filename:rootname(filename:basename(F))). + +%% Get subset of SourceFiles which need to be recompiled, respecting +%% dependencies induced by given graph G. +needed_files(Graph, ErlOpts, RebarOpts, Dir, OutDir, SourceFiles) -> + lists:filter(fun(Source) -> + TargetBase = target_base(OutDir, Source), + Target = TargetBase ++ ".beam", + PrivIncludes = [{i, filename:join(Dir, Src)} + || Src <- rebar_dir:all_src_dirs(RebarOpts, ["src"], [])], + AllOpts = [{outdir, filename:dirname(Target)} + ,{i, filename:join(Dir, "include")} + ,{i, Dir}] ++ PrivIncludes ++ ErlOpts, + digraph:vertex(Graph, Source) > {Source, filelib:last_modified(Target)} + orelse opts_changed(AllOpts, TargetBase) + orelse erl_compiler_opts_set() + end, SourceFiles). + +target_base(OutDir, Source) -> + filename:join(OutDir, filename:basename(Source, ".erl")). + +opts_changed(NewOpts, Target) -> + TotalOpts = case erlang:function_exported(compile, env_compiler_options, 0) of + true -> NewOpts ++ compile:env_compiler_options(); + false -> NewOpts + end, + case compile_info(Target) of + {ok, Opts} -> lists:any(fun effects_code_generation/1, lists:usort(TotalOpts) -- lists:usort(Opts)); + _ -> true + end. + +effects_code_generation(Option) -> + case Option of + beam -> false; + report_warnings -> false; + report_errors -> false; + return_errors-> false; + return_warnings-> false; + report -> false; + warnings_as_errors -> false; + binary -> false; + verbose -> false; + {cwd,_} -> false; + {outdir, _} -> false; + _ -> true + end. + +compile_info(Target) -> + case beam_lib:chunks(Target, [compile_info]) of + {ok, {_mod, Chunks}} -> + CompileInfo = proplists:get_value(compile_info, Chunks, []), + {ok, proplists:get_value(options, CompileInfo, [])}; + {error, beam_lib, Reason} -> + ?WARN("Couldn't read debug info from ~p for reason: ~p", [Target, Reason]), + {error, Reason} + end. + +erl_compiler_opts_set() -> + EnvSet = case os:getenv("ERL_COMPILER_OPTIONS") of + false -> false; + _ -> true + end, + %% return false if changed env opts would have been caught in opts_changed/2 + EnvSet andalso not erlang:function_exported(compile, env_compiler_options, 0). + +valid_erl_first_conf(FileList) -> + Strs = filter_file_list(FileList), + case rebar_utils:is_list_of_strings(Strs) of + true -> true; + false -> ?ABORT("An invalid file list (~p) was provided as part of your erl_first_files directive", + [FileList]) + end. + +filter_file_list(FileList) -> + Atoms = lists:filter( fun(X) -> is_atom(X) end, FileList), + case Atoms of + [] -> + FileList; + _ -> + atoms_in_erl_first_files_warning(Atoms), + lists:filter( fun(X) -> not(is_atom(X)) end, FileList) + end. + +atoms_in_erl_first_files_warning(Atoms) -> + W = "You have provided atoms as file entries in erl_first_files; " + "erl_first_files only expects lists of filenames as strings. " + "The following modules (~p) may not work as expected and it is advised " + "that you change these entires to string format " + "(e.g., \"src/module.erl\") ", + ?WARN(W, [Atoms]). + +module_to_erl(Mod) -> + atom_to_list(Mod) ++ ".erl". + +parse_attrs(Fd, Includes, Dir) -> + case io:parse_erl_form(Fd, "") of + {ok, Form, _Line} -> + case erl_syntax:type(Form) of + attribute -> + NewIncludes = process_attr(Form, Includes, Dir), + parse_attrs(Fd, NewIncludes, Dir); + _ -> + parse_attrs(Fd, Includes, Dir) + end; + {eof, _} -> + Includes; + _Err -> + parse_attrs(Fd, Includes, Dir) + end. + +process_attr(Form, Includes, Dir) -> + AttrName = erl_syntax:atom_value(erl_syntax:attribute_name(Form)), + process_attr(AttrName, Form, Includes, Dir). + +process_attr(import, Form, Includes, _Dir) -> + case erl_syntax_lib:analyze_import_attribute(Form) of + {Mod, _Funs} -> + [module_to_erl(Mod)|Includes]; + Mod -> + [module_to_erl(Mod)|Includes] + end; +process_attr(file, Form, Includes, _Dir) -> + {File, _} = erl_syntax_lib:analyze_file_attribute(Form), + [File|Includes]; +process_attr(include, Form, Includes, _Dir) -> + [FileNode] = erl_syntax:attribute_arguments(Form), + File = erl_syntax:string_value(FileNode), + [File|Includes]; +process_attr(include_lib, Form, Includes, Dir) -> + [FileNode] = erl_syntax:attribute_arguments(Form), + RawFile = erl_syntax:string_value(FileNode), + maybe_expand_include_lib_path(RawFile, Dir) ++ Includes; +process_attr(behavior, Form, Includes, _Dir) -> + process_attr(behaviour, Form, Includes, _Dir); +process_attr(behaviour, Form, Includes, _Dir) -> + [FileNode] = erl_syntax:attribute_arguments(Form), + File = module_to_erl(erl_syntax:atom_value(FileNode)), + [File|Includes]; +process_attr(compile, Form, Includes, _Dir) -> + [Arg] = erl_syntax:attribute_arguments(Form), + case erl_syntax:concrete(Arg) of + {parse_transform, Mod} -> + [module_to_erl(Mod)|Includes]; + {core_transform, Mod} -> + [module_to_erl(Mod)|Includes]; + L when is_list(L) -> + lists:foldl( + fun({parse_transform, Mod}, Acc) -> + [module_to_erl(Mod)|Acc]; + ({core_transform, Mod}, Acc) -> + [module_to_erl(Mod)|Acc]; + (_, Acc) -> + Acc + end, Includes, L); + _ -> + Includes + end; +process_attr(_, _Form, Includes, _Dir) -> + Includes. + +%% NOTE: If, for example, one of the entries in Files, refers to +%% gen_server.erl, that entry will be dropped. It is dropped because +%% such an entry usually refers to the beam file, and we don't pass a +%% list of OTP src dirs for finding gen_server.erl's full path. Also, +%% if gen_server.erl was modified, it's not rebar's task to compile a +%% new version of the beam file. Therefore, it's reasonable to drop +%% such entries. Also see process_attr(behaviour, Form, Includes). +-spec expand_file_names([file:filename()], + [file:filename()]) -> [file:filename()]. +expand_file_names(Files, Dirs) -> + %% We check if Files exist by itself or within the directories + %% listed in Dirs. + %% Return the list of files matched. + lists:flatmap( + fun(Incl) -> + case filelib:is_regular(Incl) of + true -> + [Incl]; + false -> + lists:flatmap( + fun(Dir) -> + FullPath = filename:join(Dir, Incl), + case filelib:is_regular(FullPath) of + true -> + [FullPath]; + false -> + [] + end + end, Dirs) + end + end, Files). + +%% Given a path like "stdlib/include/erl_compile.hrl", return +%% "OTP_INSTALL_DIR/lib/erlang/lib/stdlib-x.y.z/include/erl_compile.hrl". +%% Usually a simple [Lib, SubDir, File1] = filename:split(File) should +%% work, but to not crash when an unusual include_lib path is used, +%% utilize more elaborate logic. +maybe_expand_include_lib_path(File, Dir) -> + File1 = filename:basename(File), + case filename:split(filename:dirname(File)) of + [_] -> + warn_and_find_path(File, Dir); + [Lib | SubDir] -> + case code:lib_dir(list_to_atom(Lib), list_to_atom(filename:join(SubDir))) of + {error, bad_name} -> + warn_and_find_path(File, Dir); + AppDir -> + [filename:join(AppDir, File1)] + end + end. + +%% The use of -include_lib was probably incorrect by the user but lets try to make it work. +%% We search in the outdir and outdir/../include to see if the header exists. +warn_and_find_path(File, Dir) -> + SrcHeader = filename:join(Dir, File), + case filelib:is_regular(SrcHeader) of + true -> + [SrcHeader]; + false -> + IncludeDir = filename:join(rebar_utils:droplast(filename:split(Dir))++["include"]), + IncludeHeader = filename:join(IncludeDir, File), + case filelib:is_regular(IncludeHeader) of + true -> + [filename:join(IncludeDir, File)]; + false -> + [] + end + end. diff --git a/src/rebar_compiler_mib.erl b/src/rebar_compiler_mib.erl new file mode 100644 index 0000000..32516bf --- /dev/null +++ b/src/rebar_compiler_mib.erl @@ -0,0 +1,70 @@ +-module(rebar_compiler_mib). + +-behaviour(rebar_compiler). + +-export([context/1, + needed_files/3, + dependencies/3, + compile/4, + clean/2]). + +-include("rebar.hrl"). +-include_lib("stdlib/include/erl_compile.hrl"). + +context(AppInfo) -> + Dir = rebar_app_info:dir(AppInfo), + Mappings = [{".bin", filename:join([Dir, "priv", "mibs"])}, + {".hrl", filename:join(Dir, "include")}], + + #{src_dirs => ["mibs"], + include_dirs => [], + src_ext => ".mib", + out_mappings => Mappings}. + +needed_files(_, FoundFiles, AppInfo) -> + FirstFiles = [], + + %% Remove first files from found files + RestFiles = [Source || Source <- FoundFiles, not lists:member(Source, FirstFiles)], + + Opts = rebar_opts:get(rebar_app_info:opts(AppInfo), mib_opts, []), + {{FirstFiles, Opts}, {RestFiles, Opts}}. + +dependencies(_, _, _) -> + []. + +compile(Source, OutDirs, _, Opts) -> + {_, BinOut} = lists:keyfind(".bin", 1, OutDirs), + {_, HrlOut} = lists:keyfind(".hrl", 1, OutDirs), + + ok = rebar_file_utils:ensure_dir(BinOut), + ok = rebar_file_utils:ensure_dir(HrlOut), + Mib = filename:join(BinOut, filename:basename(Source, ".mib")), + HrlFilename = Mib ++ ".hrl", + + AllOpts = [{outdir, BinOut}, {i, [BinOut]}] ++ Opts, + + case snmpc:compile(Source, AllOpts) of + {ok, _} -> + MibToHrlOpts = + case proplists:get_value(verbosity, AllOpts, undefined) of + undefined -> + #options{specific = [], + cwd = rebar_dir:get_cwd()}; + Verbosity -> + #options{specific = [{verbosity, Verbosity}], + cwd = rebar_dir:get_cwd()} + end, + ok = snmpc:mib_to_hrl(Mib, Mib, MibToHrlOpts), + rebar_file_utils:mv(HrlFilename, HrlOut), + ok; + {error, compilation_failed} -> + ?FAIL + end. + +clean(MibFiles, AppInfo) -> + AppDir = rebar_app_info:dir(AppInfo), + MIBs = [filename:rootname(filename:basename(MIB)) || MIB <- MibFiles], + rebar_file_utils:delete_each( + [filename:join([AppDir, "include", MIB++".hrl"]) || MIB <- MIBs]), + ok = rebar_file_utils:rm_rf(filename:join([AppDir, "priv/mibs/*.bin"])). diff --git a/src/rebar_compiler_xrl.erl b/src/rebar_compiler_xrl.erl new file mode 100644 index 0000000..5c023f0 --- /dev/null +++ b/src/rebar_compiler_xrl.erl @@ -0,0 +1,50 @@ +-module(rebar_compiler_xrl). + +-behaviour(rebar_compiler). + +-export([context/1, + needed_files/3, + dependencies/3, + compile/4, + clean/2]). + +context(AppInfo) -> + Dir = rebar_app_info:dir(AppInfo), + Mappings = [{".erl", filename:join([Dir, "src"])}], + #{src_dirs => ["src"], + include_dirs => [], + src_ext => ".xrl", + out_mappings => Mappings}. + +needed_files(_, FoundFiles, AppInfo) -> + FirstFiles = [], + + %% Remove first files from found files + RestFiles = [Source || Source <- FoundFiles, not lists:member(Source, FirstFiles)], + + Opts = rebar_opts:get(rebar_app_info:opts(AppInfo), xrl_opts, []), + + {{FirstFiles, Opts}, {RestFiles, Opts}}. + +dependencies(_, _, _) -> + []. + +compile(Source, [{_, OutDir}], _, Opts) -> + BaseName = filename:basename(Source), + Target = filename:join([OutDir, BaseName]), + AllOpts = [{parserfile, Target} | Opts], + AllOpts1 = [{includefile, filename:join(OutDir, I)} || {includefile, I} <- AllOpts, + filename:pathtype(I) =:= relative], + case leex:file(Source, AllOpts1 ++ [{return, true}]) of + {ok, _} -> + ok; + {ok, _Mod, Ws} -> + rebar_compiler:ok_tuple(Source, Ws); + {error, Es, Ws} -> + rebar_compiler:error_tuple(Source, Es, Ws, AllOpts1) + end. + +clean(XrlFiles, _AppInfo) -> + rebar_file_utils:delete_each( + [rebar_utils:to_list(re:replace(F, "\\.xrl$", ".erl", [unicode])) + || F <- XrlFiles]). diff --git a/src/rebar_compiler_yrl.erl b/src/rebar_compiler_yrl.erl new file mode 100644 index 0000000..41d93b1 --- /dev/null +++ b/src/rebar_compiler_yrl.erl @@ -0,0 +1,49 @@ +-module(rebar_compiler_yrl). + +-behaviour(rebar_compiler). + +-export([context/1, + needed_files/3, + dependencies/3, + compile/4, + clean/2]). + +context(AppInfo) -> + Dir = rebar_app_info:dir(AppInfo), + Mappings = [{".erl", filename:join([Dir, "src"])}], + #{src_dirs => ["src"], + include_dirs => [], + src_ext => ".yrl", + out_mappings => Mappings}. + +needed_files(_, FoundFiles, AppInfo) -> + FirstFiles = [], + + %% Remove first files from found files + RestFiles = [Source || Source <- FoundFiles, not lists:member(Source, FirstFiles)], + + Opts = rebar_opts:get(rebar_app_info:opts(AppInfo), yrl_opts, []), + {{FirstFiles, Opts}, {RestFiles, Opts}}. + +dependencies(_, _, _) -> + []. + +compile(Source, [{_, OutDir}], _, Opts) -> + BaseName = filename:basename(Source), + Target = filename:join([OutDir, BaseName]), + AllOpts = [{parserfile, Target} | Opts], + AllOpts1 = [{includefile, filename:join(OutDir, I)} || {includefile, I} <- AllOpts, + filename:pathtype(I) =:= relative], + case yecc:file(Source, AllOpts1 ++ [{return, true}]) of + {ok, _} -> + ok; + {ok, _Mod, Ws} -> + rebar_compiler:ok_tuple(Source, Ws); + {error, Es, Ws} -> + rebar_compiler:error_tuple(Source, Es, Ws, AllOpts1) + end. + +clean(YrlFiles, _AppInfo) -> + rebar_file_utils:delete_each( + [rebar_utils:to_list(re:replace(F, "\\.yrl$", ".erl", [unicode])) + || F <- YrlFiles]). 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_dir.erl b/src/rebar_dir.erl index d7be423..17bc48e 100644 --- a/src/rebar_dir.erl +++ b/src/rebar_dir.erl @@ -301,9 +301,8 @@ all_src_dirs(Opts, SrcDefault, ExtraDefault) -> src_dir_opts(Opts, Dir) -> RawSrcDirs = raw_src_dirs(src_dirs, Opts, []), RawExtraSrcDirs = raw_src_dirs(extra_src_dirs, Opts, []), - AllOpts = [Opt || {D,Opt} <- RawSrcDirs++RawExtraSrcDirs, - D==Dir], - lists:ukeysort(1,proplists:unfold(lists:append(AllOpts))). + AllOpts = [Opt || {D, Opt} <- RawSrcDirs++RawExtraSrcDirs, D==Dir], + lists:ukeysort(1, proplists:unfold(lists:append(AllOpts))). %%% @doc %%% Return the value of the 'recursive' option for the given directory. diff --git a/src/rebar_erlc_compiler.erl b/src/rebar_erlc_compiler.erl index 920c3b4..e52791c 100644 --- a/src/rebar_erlc_compiler.erl +++ b/src/rebar_erlc_compiler.erl @@ -92,6 +92,7 @@ compile(AppInfo) when element(1, AppInfo) == app_info_t -> %% @doc compile an individual application. -spec compile(rebar_app_info:t(), compile_opts()) -> ok. compile(AppInfo, CompileOpts) when element(1, AppInfo) == app_info_t -> + warn_deprecated(), Dir = rebar_utils:to_list(rebar_app_info:out_dir(AppInfo)), RebarOpts = rebar_app_info:opts(AppInfo), @@ -99,6 +100,7 @@ compile(AppInfo, CompileOpts) when element(1, AppInfo) == app_info_t -> {recursive, dir_recursive(RebarOpts, "src", CompileOpts)}], MibsOpts = [check_last_mod, {recursive, dir_recursive(RebarOpts, "mibs", CompileOpts)}], + rebar_base_compiler:run(RebarOpts, check_files([filename:join(Dir, File) || File <- rebar_opts:get(RebarOpts, xrl_first_files, [])]), @@ -147,6 +149,7 @@ compile(RebarOpts, BaseDir, OutDir) -> compile(State, BaseDir, OutDir, CompileOpts) when element(1, State) == state_t -> compile(rebar_state:opts(State), BaseDir, OutDir, CompileOpts); compile(RebarOpts, BaseDir, OutDir, CompileOpts) -> + warn_deprecated(), SrcDirs = lists:map(fun(SrcDir) -> filename:join(BaseDir, SrcDir) end, rebar_dir:src_dirs(RebarOpts, ["src"])), compile_dirs(RebarOpts, BaseDir, SrcDirs, OutDir, CompileOpts), @@ -363,6 +366,7 @@ effects_code_generation(Option) -> report_errors -> false; return_errors-> false; return_warnings-> false; + report -> false; warnings_as_errors -> false; binary -> false; verbose -> false; @@ -802,7 +806,7 @@ valid_erl_first_conf(FileList) -> Strs = filter_file_list(FileList), case rebar_utils:is_list_of_strings(Strs) of true -> true; - false -> ?ABORT("An invalid file list (~p) was provided as part of your erl_files_first directive", + false -> ?ABORT("An invalid file list (~p) was provided as part of your erl_first_files directive", [FileList]) end. @@ -823,3 +827,15 @@ atoms_in_erl_first_files_warning(Atoms) -> "that you change these entires to string format " "(e.g., \"src/module.erl\") ", ?WARN(W, [Atoms]). + +warn_deprecated() -> + case get({deprecate_warn, ?MODULE}) of + undefined -> + ?WARN("Calling deprecated ~p compiler module. This module has been " + "replaced by rebar_compiler and rebar_compiler_erl, but will " + "remain available.", [?MODULE]), + put({deprecate_warn, ?MODULE}, true), + ok; + _ -> + ok + end. 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_file_utils.erl b/src/rebar_file_utils.erl index 492d690..a51a557 100644 --- a/src/rebar_file_utils.erl +++ b/src/rebar_file_utils.erl @@ -43,7 +43,8 @@ path_from_ancestor/2, canonical_path/1, resolve_link/1, - split_dirname/1]). + split_dirname/1, + ensure_dir/1]). -include("rebar.hrl"). @@ -386,7 +387,7 @@ reset_dir(Path) -> %% delete the directory if it exists _ = ec_file:remove(Path, [recursive]), %% recreate the directory - filelib:ensure_dir(filename:join([Path, "dummy.beam"])). + ensure_dir(Path). %% Linux touch but using erlang functions to work in bot *nix os and @@ -440,6 +441,10 @@ resolve_link(Path) -> split_dirname(Path) -> {filename:dirname(Path), filename:basename(Path)}. +-spec ensure_dir(filelib:dirname_all()) -> ok | {error, file:posix()}. +ensure_dir(Path) -> + filelib:ensure_dir(filename:join(Path, "fake_file")). + %% =================================================================== %% Internal functions %% =================================================================== @@ -505,7 +510,7 @@ cp_r_win32({true, SourceDir}, {false, DestDir}) -> false -> %% Specifying a target directory that doesn't currently exist. %% So let's attempt to create this directory - case filelib:ensure_dir(filename:join(DestDir, "dummy")) of + case ensure_dir(DestDir) of ok -> ok = xcopy_win32(SourceDir, DestDir); {error, Reason} -> diff --git a/src/rebar_git_resource.erl b/src/rebar_git_resource.erl index 0286762..cec7dfc 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), @@ -201,7 +223,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_hooks.erl b/src/rebar_hooks.erl index ec6fe31..358458e 100644 --- a/src/rebar_hooks.erl +++ b/src/rebar_hooks.erl @@ -42,8 +42,7 @@ run_provider_hooks_(Dir, Type, Command, Providers, TypeHooks, State) -> [] -> State; HookProviders -> - PluginDepsPaths = lists:usort(rebar_state:code_paths(State, all_plugin_deps)), - code:add_pathsa(PluginDepsPaths), + rebar_paths:set_paths([plugins], State), Providers1 = rebar_state:providers(State), State1 = rebar_state:providers(rebar_state:dir(State, Dir), Providers++Providers1), case rebar_core:do(HookProviders, State1) of @@ -51,7 +50,7 @@ run_provider_hooks_(Dir, Type, Command, Providers, TypeHooks, State) -> ?DEBUG(format_error({bad_provider, Type, Command, ProviderName}), []), throw(?PRV_ERROR({bad_provider, Type, Command, ProviderName})); {ok, State2} -> - rebar_utils:remove_from_code_path(PluginDepsPaths), + rebar_paths:set_paths([deps], State2), State2 end end. diff --git a/src/rebar_otp_app.erl b/src/rebar_otp_app.erl index f5bb9cf..e14975f 100644 --- a/src/rebar_otp_app.erl +++ b/src/rebar_otp_app.erl @@ -59,8 +59,9 @@ 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 +80,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 +111,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} @@ -125,13 +126,13 @@ preprocess(State, AppInfo, AppSrcFile) -> %% Setup file .app filename and write new contents EbinDir = rebar_app_info:ebin_dir(AppInfo), - filelib:ensure_dir(filename:join(EbinDir, "dummy.beam")), + rebar_file_utils:ensure_dir(EbinDir), AppFile = rebar_app_utils:app_src_to_app(OutDir, AppSrcFile), ok = rebar_file_utils:write_file_if_contents_differ(AppFile, Spec, utf8), 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) -> @@ -163,32 +164,17 @@ validate_name(AppName, File) -> ebin_modules(AppInfo, Dir) -> Beams = lists:sort(rebar_utils:beams(filename:join(Dir, "ebin"))), - ExtraDirs = extra_dirs(AppInfo), - F = fun(Beam) -> not in_extra_dir(AppInfo, Beam, ExtraDirs) end, - Filtered = lists:filter(F, Beams), + SrcDirs = rebar_dir:src_dirs(rebar_app_info:opts(AppInfo), ["src"]), + FindSourceRules = [{".beam", ".erl", + [{"ebin", SrcDir} || SrcDir <- SrcDirs]}], + Filtered = lists:filter(fun(Beam) -> + rebar_utils:find_source(filename:basename(Beam), + filename:dirname(Beam), + FindSourceRules) + =/= {error, not_found} + end, Beams), [rebar_utils:beam_to_mod(N) || N <- Filtered]. -extra_dirs(State) -> - Extras = rebar_dir:extra_src_dirs(rebar_app_info:opts(State)), - SrcDirs = rebar_dir:src_dirs(rebar_app_info:opts(State), ["src"]), - %% remove any dirs that are defined in `src_dirs` from `extra_src_dirs` - Extras -- SrcDirs. - -in_extra_dir(AppInfo, Beam, Dirs) -> - lists:any(fun(Dir) -> lists:prefix(filename:join([rebar_app_info:out_dir(AppInfo), Dir]), - beam_src(Beam)) end, - Dirs). - -beam_src(Beam) -> - case beam_lib:chunks(Beam, [compile_info]) of - {ok, {_mod, Chunks}} -> - CompileInfo = proplists:get_value(compile_info, Chunks, []), - proplists:get_value(source, CompileInfo, []); - {error, beam_lib, Reason} -> - ?WARN("Couldn't read debug info from ~p for reason: ~p", [Beam, Reason]), - [] - end. - ensure_registered(AppData) -> case lists:keyfind(registered, 1, AppData) of false -> @@ -226,10 +212,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..757eb86 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,131 @@ -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(), ec_semver:semver(), + unicode:unicode_binary(), + ets:tid(), rebar_state:t()) -> [vsn()]. +get_package_versions(Dep, {_, AlphaInfo}, Repo, Table, State) -> + ?MODULE:verify_table(State), + AllowPreRelease = rebar_state:get(State, deps_allow_prerelease, false) + orelse AlphaInfo =/= {[],[]}, + ets:select(Table, [{#package{key={Dep, {'$1', '$2'}, Repo}, + _='_'}, + [{'==', '$2', {{[],[]}}} || not AllowPreRelease], [{{'$1', '$2'}}]}]). + +-spec get_package(unicode:unicode_binary(), unicode:unicode_binary(), + binary() | undefined | '_', + [unicode:unicode_binary()] | ['_'], ets:tab(), rebar_state:t()) + -> {ok, #package{}} | not_found. +get_package(Dep, Vsn, undefined, Repos, Table, State) -> + get_package(Dep, Vsn, '_', Repos, Table, State); +get_package(Dep, Vsn, Hash, Repos, Table, State) -> + ?MODULE:verify_table(State), + case ets:select(Table, [{#package{key={Dep, ec_semver:parse(Vsn), Repo}, + checksum=Hash, + _='_'}, [], ['$_']} || 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(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, -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, - - {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 +164,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, Constraint, Repo, Table, State) of + [Vsn] -> + handle_single_vsn(Vsn, Constraint); + Vsns -> case handle_vsns(Constraint, Vsns) of none -> none; @@ -188,18 +197,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 +208,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, ec_semver:parse(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>>) -> + ec_semver:parse(rm_ws(R)); +rm_ws(R) -> + ec_semver:parse(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, Vsn, 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, Vsn, 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_paths.erl b/src/rebar_paths.erl new file mode 100644 index 0000000..82c0218 --- /dev/null +++ b/src/rebar_paths.erl @@ -0,0 +1,208 @@ +-module(rebar_paths). +-include("rebar.hrl"). + +-type target() :: deps | plugins. +-type targets() :: [target(), ...]. +-export_type([target/0, targets/0]). +-export([set_paths/2, unset_paths/2]). +-export([clashing_apps/2]). + +-ifdef(TEST). +-export([misloaded_modules/2]). +-endif. + +-spec set_paths(targets(), rebar_state:t()) -> ok. +set_paths(UserTargets, State) -> + Targets = normalize_targets(UserTargets), + GroupPaths = path_groups(Targets, State), + Paths = lists:append(lists:reverse([P || {_, P} <- GroupPaths])), + code:add_pathsa(Paths), + AppGroups = app_groups(Targets, State), + purge_and_load(AppGroups, sets:new()), + ok. + +-spec unset_paths(targets(), rebar_state:t()) -> ok. +unset_paths(UserTargets, State) -> + Targets = normalize_targets(UserTargets), + GroupPaths = path_groups(Targets, State), + Paths = lists:append([P || {_, P} <- GroupPaths]), + [code:del_path(P) || P <- Paths], + purge(Paths, code:all_loaded()), + ok. + +-spec clashing_apps(targets(), rebar_state:t()) -> [{target(), [binary()]}]. +clashing_apps(Targets, State) -> + AppGroups = app_groups(Targets, State), + AppNames = [{G, sets:from_list( + [rebar_app_info:name(App) || App <- Apps] + )} || {G, Apps} <- AppGroups], + clashing_app_names(sets:new(), AppNames, []). + +%%%%%%%%%%%%%%% +%%% PRIVATE %%% +%%%%%%%%%%%%%%% + +%% The paths are to be set in the reverse order; i.e. the default +%% path is always last when possible (minimize cases where a build +%% tool version clashes with an app's), and put the highest priorities +%% first. +-spec normalize_targets(targets()) -> targets(). +normalize_targets(List) -> + %% Plan for the eventuality of getting values piped in + %% from future versions of rebar3, possibly from plugins and so on, + %% which means we'd risk failing kind of violently. We only support + %% deps and plugins + TmpList = lists:foldl( + fun(deps, [deps | _] = Acc) -> Acc; + (plugins, [plugins | _] = Acc) -> Acc; + (deps, Acc) -> [deps | Acc -- [deps]]; + (plugins, Acc) -> [plugins | Acc -- [plugins]]; + (_, Acc) -> Acc + end, + [], + List + ), + lists:reverse(TmpList). + +purge_and_load([], _) -> + ok; +purge_and_load([{_Group, Apps}|Rest], Seen) -> + %% We have: a list of all applications in the current priority group, + %% a list of all loaded modules with their active path, and a list of + %% seen applications. + %% + %% We do the following: + %% 1. identify the apps that have not been solved yet + %% 2. find the paths for all apps in the current group + %% 3. unload and reload apps that may have changed paths in order + %% to get updated module lists and specs + %% (we ignore started apps and apps that have not run for this) + %% This part turns out to be the bottleneck of this module, so + %% to speed it up, using clash detection proves useful: + %% only reload apps that clashed since others are unlikely to + %% conflict in significant ways + %% 4. create a list of modules to check from that app list—only loaded + %% modules make sense to check. + %% 5. check the modules to match their currently loaded paths with + %% the path set from the apps in the current group; modules + %% that differ must be purged; others can stay + + %% 1) + AppNames = [AppName || App <- Apps, + AppName <- [rebar_app_info:name(App)], + not sets:is_element(AppName, Seen)], + GoodApps = [App || AppName <- AppNames, + App <- Apps, + rebar_app_info:name(App) =:= AppName], + %% 2) + %% (no need for extra_src_dirs since those get put into ebin; + %% also no need for OTP libs; we want to allow overtaking them) + GoodAppPaths = [rebar_app_info:ebin_dir(App) || App <- GoodApps], + %% 3) + [begin + AtomApp = binary_to_atom(AppName, utf8), + %% blind load/unload won't interrupt an already-running app, + %% preventing odd errors, maybe! + case application:unload(AtomApp) of + ok -> application:load(AtomApp); + _ -> ok + end + end || AppName <- AppNames, + %% Shouldn't unload ourselves; rebar runs without ever + %% being started and unloading breaks logging! + AppName =/= <<"rebar">>], + + %% 4) + CandidateMods = lists:append( + %% Start by asking the currently loaded app (if loaded) + %% since it would be the primary source of conflicting modules + [case application:get_key(AppName, modules) of + {ok, Mods} -> + Mods; + undefined -> + %% if not found, parse the app file on disk, in case + %% the app's modules are used without it being loaded + case rebar_app_info:app_details(App) of + [] -> []; + Details -> proplists:get_value(modules, Details, []) + end + end || App <- GoodApps, + AppName <- [binary_to_atom(rebar_app_info:name(App), utf8)]] + ), + ModPaths = [{Mod,Path} || Mod <- CandidateMods, + erlang:function_exported(Mod, module_info, 0), + {file, Path} <- [code:is_loaded(Mod)]], + + %% 5) + Mods = misloaded_modules(GoodAppPaths, ModPaths), + [purge_mod(Mod) || Mod <- Mods], + + purge_and_load(Rest, sets:union(Seen, sets:from_list(AppNames))). + +purge(Paths, ModPaths) -> + SortedPaths = lists:sort(Paths), + lists:map(fun purge_mod/1, + [Mod || {Mod, Path} <- ModPaths, + is_list(Path), % not 'preloaded' or mocked + any_prefix(Path, SortedPaths)] + ). + +misloaded_modules(GoodAppPaths, ModPaths) -> + %% Identify paths that are invalid; i.e. app paths that cover an + %% app in the desired group, but are not in the desired group. + lists:usort( + [Mod || {Mod, Path} <- ModPaths, + is_list(Path), % not 'preloaded' or mocked + not any_prefix(Path, GoodAppPaths)] + ). + +any_prefix(Path, Paths) -> + lists:any(fun(P) -> lists:prefix(P, Path) end, Paths). + +%% assume paths currently set are good; only unload a module so next call +%% uses the correctly set paths +purge_mod(Mod) -> + code:soft_purge(Mod) andalso code:delete(Mod). + + +%% This is a tricky O(n²) check since we want to +%% know whether an app clashes with any of the top priority groups. +%% +%% For example, let's say we have `[deps, plugins]', then we want +%% to find the plugins that clash with deps: +%% +%% `[{deps, [ClashingPlugins]}, {plugins, []}]' +%% +%% In case we'd ever have alternative or additional types, we can +%% find all clashes from other 'groups'. +clashing_app_names(_, [], Acc) -> + lists:reverse(Acc); +clashing_app_names(PrevNames, [{G,AppNames} | Rest], Acc) -> + CurrentNames = sets:subtract(AppNames, PrevNames), + NextNames = sets:subtract(sets:union([A || {_, A} <- Rest]), PrevNames), + Clashes = sets:intersection(CurrentNames, NextNames), + NewAcc = [{G, sets:to_list(Clashes)} | Acc], + clashing_app_names(sets:union(PrevNames, CurrentNames), Rest, NewAcc). + +path_groups(Targets, State) -> + [{Target, get_paths(Target, State)} || Target <- Targets]. + +app_groups(Targets, State) -> + [{Target, get_apps(Target, State)} || Target <- Targets]. + +get_paths(deps, State) -> + rebar_state:code_paths(State, all_deps); +get_paths(plugins, State) -> + rebar_state:code_paths(State, all_plugin_deps). + +get_apps(deps, State) -> + %% The code paths for deps also include the top level apps + %% and the extras, which we don't have here; we have to + %% add the apps by hand + case rebar_state:project_apps(State) of + undefined -> []; + List -> List + end ++ + rebar_state:all_deps(State); +get_apps(plugins, State) -> + rebar_state:all_plugin_deps(State). diff --git a/src/rebar_pkg_resource.erl b/src/rebar_pkg_resource.erl index 2cf167e..823b7fc 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_utils:to_binary(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_plugins.erl b/src/rebar_plugins.erl index bc6a1e0..2a78c6e 100644 --- a/src/rebar_plugins.erl +++ b/src/rebar_plugins.erl @@ -39,13 +39,18 @@ project_apps_install(State) -> Profiles = rebar_state:current_profiles(State), ProjectApps = rebar_state:project_apps(State), lists:foldl(fun(Profile, StateAcc) -> - Plugins = rebar_state:get(State, {plugins, Profile}, []), - StateAcc1 = handle_plugins(Profile, Plugins, StateAcc), + StateAcc1 = case Profile of + default -> + %% default profile top level plugins + %% are installed in run_aux + StateAcc; + _ -> + Plugins = rebar_state:get(State, {plugins, Profile}, []), + handle_plugins(Profile, Plugins, StateAcc) + end, lists:foldl(fun(AppInfo, StateAcc2) -> - C = rebar_config:consult(rebar_app_info:dir(AppInfo)), - AppInfo0 = rebar_app_info:update_opts(AppInfo, rebar_app_info:opts(AppInfo), C), - Plugins2 = rebar_app_info:get(AppInfo0, {plugins, Profile}, []), + Plugins2 = rebar_app_info:get(AppInfo, {plugins, Profile}, []), handle_plugins(Profile, Plugins2, StateAcc2) end, StateAcc1, ProjectApps) end, State, Profiles). @@ -62,12 +67,25 @@ install(State, AppInfo) -> State2 = lists:foldl(fun(Profile, StateAcc) -> Plugins = rebar_app_info:get(AppInfo, {plugins, Profile}, []), - handle_plugins(Profile, Plugins, StateAcc) + Plugins1 = filter_existing_plugins(Plugins, StateAcc), + handle_plugins(Profile, Plugins1, StateAcc) end, State1, Profiles), %% Reset the overrides after processing the dep rebar_state:set(State2, overrides, StateOverrides). +filter_existing_plugins(Plugins, State) -> + PluginNames = lists:zip(Plugins, rebar_state:deps_names(Plugins)), + AllPlugins = rebar_state:all_plugin_deps(State), + lists:filtermap(fun({Plugin, PluginName}) -> + case rebar_app_utils:find(PluginName, AllPlugins) of + {ok, _} -> + false; + _ -> + {true, Plugin} + end + end, PluginNames). + handle_plugins(Profile, Plugins, State) -> handle_plugins(Profile, Plugins, State, false). @@ -76,7 +94,6 @@ handle_plugins(Profile, Plugins, State, Upgrade) -> Locks = rebar_state:lock(State), DepsDir = rebar_state:get(State, deps_dir, ?DEFAULT_DEPS_DIR), State1 = rebar_state:set(State, deps_dir, ?DEFAULT_PLUGINS_DIR), - %% Install each plugin individually so if one fails to install it doesn't effect the others {_PluginProviders, State2} = lists:foldl(fun(Plugin, {PluginAcc, StateAcc}) -> @@ -105,12 +122,10 @@ handle_plugin(Profile, Plugin, State, Upgrade) -> %% Add newly built deps and plugin to code path State3 = rebar_state:update_all_plugin_deps(State2, Apps), NewCodePaths = [rebar_app_info:ebin_dir(A) || A <- ToBuild], - AllPluginEbins = filelib:wildcard(filename:join([rebar_dir:plugins_dir(State), "*", "ebin"])), - CodePaths = PreBuiltPaths++(AllPluginEbins--ToBuild), - code:add_pathsa(NewCodePaths++CodePaths), %% Store plugin code paths so we can remove them when compiling project apps State4 = rebar_state:update_code_paths(State3, all_plugin_deps, PreBuiltPaths++NewCodePaths), + rebar_paths:set_paths([plugins], State4), {plugin_providers(Plugin), State4} catch @@ -122,8 +137,6 @@ handle_plugin(Profile, Plugin, State, Upgrade) -> build_plugin(AppInfo, Apps, State) -> Providers = rebar_state:providers(State), - %Providers1 = rebar_state:providers(rebar_app_info:state(AppInfo)), - %rebar_app_info:state_or_new(State, AppInfo) S = rebar_state:all_deps(State, Apps), S1 = rebar_state:set(S, deps_dir, ?DEFAULT_PLUGINS_DIR), rebar_prv_compile:compile(S1, Providers, AppInfo). diff --git a/src/rebar_prv_clean.erl b/src/rebar_prv_clean.erl index 4da0a64..3c8a0c3 100644 --- a/src/rebar_prv_clean.erl +++ b/src/rebar_prv_clean.erl @@ -67,11 +67,12 @@ format_error(Reason) -> %% =================================================================== clean_apps(State, Providers, Apps) -> + Compilers = rebar_state:compilers(State), [begin ?INFO("Cleaning out ~ts...", [rebar_app_info:name(AppInfo)]), AppDir = rebar_app_info:dir(AppInfo), AppInfo1 = rebar_hooks:run_all_hooks(AppDir, pre, ?PROVIDER, Providers, AppInfo, State), - rebar_erlc_compiler:clean(AppInfo1), + rebar_compiler:clean(Compilers, AppInfo1), rebar_hooks:run_all_hooks(AppDir, post, ?PROVIDER, Providers, AppInfo1, State) end || AppInfo <- Apps]. diff --git a/src/rebar_prv_common_test.erl b/src/rebar_prv_common_test.erl index 9e71ee7..3d3bd8a 100644 --- a/src/rebar_prv_common_test.erl +++ b/src/rebar_prv_common_test.erl @@ -58,7 +58,7 @@ do(State) -> do(State, Tests) -> ?INFO("Running Common Test suites...", []), - rebar_utils:update_code(rebar_state:code_paths(State, all_deps), [soft_purge]), + rebar_paths:set_paths([deps, plugins], State), %% Run ct provider prehooks Providers = rebar_state:providers(State), @@ -73,14 +73,14 @@ do(State, Tests) -> ok -> %% Run ct provider post hooks for all project apps and top level project hooks rebar_hooks:run_project_and_app_hooks(Cwd, post, ?PROVIDER, Providers, State), - rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)), + rebar_paths:set_paths([plugins, deps], State), {ok, State}; Error -> - rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)), + rebar_paths:set_paths([plugins, deps], State), Error end; Error -> - rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)), + rebar_paths:set_paths([plugins, deps], State), Error end. @@ -250,11 +250,9 @@ select_tests(State, ProjectApps, CmdOpts, CfgOpts) -> end, SysConfigs), %% NB: load the applications (from user directories too) to support OTP < 17 %% to our best ability. - OldPath = code:get_path(), - code:add_pathsa(rebar_state:code_paths(State, all_deps)), + rebar_paths:set_paths([deps, plugins], State), [application:load(Application) || Config <- Configs, {Application, _} <- Config], rebar_utils:reread_config(Configs), - code:set_path(OldPath), Opts = merge_opts(CmdOpts,CfgOpts), discover_tests(State, ProjectApps, Opts). diff --git a/src/rebar_prv_compile.erl b/src/rebar_prv_compile.erl index 0b4fa5f..ee96d9f 100644 --- a/src/rebar_prv_compile.erl +++ b/src/rebar_prv_compile.erl @@ -30,22 +30,37 @@ init(State) -> {example, "rebar3 compile"}, {short_desc, "Compile apps .app.src and .erl files."}, {desc, "Compile apps .app.src and .erl files."}, - {opts, []}])), + {opts, [{deps_only, $d, "deps_only", undefined, + "Only compile dependencies, no project apps will be built."}]}])), {ok, State1}. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> - DepsPaths = rebar_state:code_paths(State, all_deps), - PluginDepsPaths = rebar_state:code_paths(State, all_plugin_deps), - rebar_utils:remove_from_code_path(PluginDepsPaths), - code:add_pathsa(DepsPaths), + IsDepsOnly = is_deps_only(State), + rebar_paths:set_paths([deps], State), - ProjectApps = rebar_state:project_apps(State), Providers = rebar_state:providers(State), Deps = rebar_state:deps_to_build(State), - Cwd = rebar_state:dir(State), - copy_and_build_apps(State, Providers, Deps), + + State1 = case IsDepsOnly of + true -> + State; + false -> + handle_project_apps(Providers, State) + end, + + rebar_paths:set_paths([plugins], State1), + + {ok, State1}. + +is_deps_only(State) -> + {Args, _} = rebar_state:command_parsed_args(State), + proplists:get_value(deps_only, Args, false). + +handle_project_apps(Providers, State) -> + Cwd = rebar_state:dir(State), + ProjectApps = rebar_state:project_apps(State), {ok, ProjectApps1} = rebar_digraph:compile_order(ProjectApps), %% Run top level hooks *before* project apps compiled but *after* deps are @@ -57,7 +72,7 @@ do(State) -> %% projects with structures like /apps/foo,/apps/bar,/test build_extra_dirs(State, ProjectApps2), - State3 = update_code_paths(State2, ProjectApps2, DepsPaths), + State3 = update_code_paths(State2, ProjectApps2), rebar_hooks:run_all_hooks(Cwd, post, ?PROVIDER, Providers, State2), case rebar_state:has_all_artifacts(State3) of @@ -66,14 +81,19 @@ do(State) -> true -> true end, - rebar_utils:cleanup_code_path(rebar_state:code_paths(State3, default) - ++ rebar_state:code_paths(State, all_plugin_deps)), - {ok, State3}. + State3. + -spec format_error(any()) -> iolist(). format_error({missing_artifact, File}) -> io_lib:format("Missing artifact ~ts", [File]); +format_error({bad_project_builder, Name, Type, Module}) -> + io_lib:format("Error building application ~s:~n Required project builder ~s function " + "~s:build/1 not found", [Name, Type, Module]); +format_error({unknown_project_type, Name, Type}) -> + io_lib:format("Error building application ~s:~n " + "No project builder is configured for type ~s", [Name, Type]); format_error(Reason) -> io_lib:format("~p", [Reason]). @@ -95,7 +115,7 @@ copy_and_build_project_apps(State, Providers, Apps) -> rebar_app_info:dir(AppInfo), rebar_app_info:out_dir(AppInfo)) || AppInfo <- Apps], - code:add_pathsa([rebar_app_info:out_dir(AppInfo) || AppInfo <- Apps]), + code:add_pathsa([rebar_app_info:ebin_dir(AppInfo) || AppInfo <- Apps]), [compile(State, Providers, AppInfo) || AppInfo <- Apps]. @@ -117,10 +137,19 @@ build_extra_dir(State, Dir) -> true -> BaseDir = filename:join([rebar_dir:base_dir(State), "extras"]), OutDir = filename:join([BaseDir, Dir]), - filelib:ensure_dir(filename:join([OutDir, "dummy.beam"])), + rebar_file_utils:ensure_dir(OutDir), copy(rebar_state:dir(State), BaseDir, Dir), - rebar_erlc_compiler:compile_dir(State, BaseDir, OutDir); - false -> ok + + Compilers = rebar_state:compilers(State), + FakeApp = rebar_app_info:new(), + FakeApp1 = rebar_app_info:out_dir(FakeApp, BaseDir), + FakeApp2 = rebar_app_info:ebin_dir(FakeApp1, OutDir), + Opts = rebar_state:opts(State), + FakeApp3 = rebar_app_info:opts(FakeApp2, Opts), + FakeApp4 = rebar_app_info:set(FakeApp3, src_dirs, [OutDir]), + rebar_compiler:compile_all(Compilers, FakeApp4); + false -> + ok end. compile(State, AppInfo) -> @@ -132,7 +161,9 @@ compile(State, Providers, AppInfo) -> AppInfo1 = rebar_hooks:run_all_hooks(AppDir, pre, ?PROVIDER, Providers, AppInfo, State), AppInfo2 = rebar_hooks:run_all_hooks(AppDir, pre, ?ERLC_HOOK, Providers, AppInfo1, State), - rebar_erlc_compiler:compile(AppInfo2), + + build_app(AppInfo2, State), + AppInfo3 = rebar_hooks:run_all_hooks(AppDir, post, ?ERLC_HOOK, Providers, AppInfo2, State), AppInfo4 = rebar_hooks:run_all_hooks(AppDir, pre, ?APP_HOOK, Providers, AppInfo3, State), @@ -141,11 +172,10 @@ compile(State, Providers, AppInfo) -> %% The rebar_otp_app compilation step is safe regarding the %% overall path management, so we can just load all plugins back %% in memory. - PluginDepsPaths = rebar_state:code_paths(State, all_plugin_deps), - code:add_pathsa(PluginDepsPaths), + rebar_paths:set_paths([plugins], State), AppFileCompileResult = rebar_otp_app:compile(State, AppInfo4), - %% Clean up after ourselves, leave things as they were. - rebar_utils:remove_from_code_path(PluginDepsPaths), + %% Clean up after ourselves, leave things as they were with deps first + rebar_paths:set_paths([deps], State), case AppFileCompileResult of {ok, AppInfo5} -> @@ -161,9 +191,32 @@ compile(State, Providers, AppInfo) -> %% Internal functions %% =================================================================== -update_code_paths(State, ProjectApps, DepsPaths) -> +build_app(AppInfo, State) -> + case rebar_app_info:project_type(AppInfo) of + Type when Type =:= rebar3 ; Type =:= undefined -> + Compilers = rebar_state:compilers(State), + rebar_compiler:compile_all(Compilers, AppInfo); + Type -> + ProjectBuilders = rebar_state:project_builders(State), + case lists:keyfind(Type, 1, ProjectBuilders) of + {_, Module} -> + %% load plugins since thats where project builders would be + rebar_paths:set_paths([plugins, deps], State), + Res = Module:build(AppInfo), + rebar_paths:set_paths([deps], State), + case Res of + ok -> ok; + {error, Reason} -> throw({error, {Module, Reason}}) + end; + _ -> + throw(?PRV_ERROR({unknown_project_type, rebar_app_info:name(AppInfo), Type})) + end + end. + +update_code_paths(State, ProjectApps) -> ProjAppsPaths = paths_for_apps(ProjectApps), ExtrasPaths = paths_for_extras(State, ProjectApps), + DepsPaths = rebar_state:code_paths(State, all_deps), rebar_state:code_paths(State, all_deps, DepsPaths ++ ProjAppsPaths ++ ExtrasPaths). paths_for_apps(Apps) -> paths_for_apps(Apps, []). 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_dialyzer.erl b/src/rebar_prv_dialyzer.erl index 99a7698..585051c 100644 --- a/src/rebar_prv_dialyzer.erl +++ b/src/rebar_prv_dialyzer.erl @@ -85,7 +85,8 @@ short_desc() -> do(State) -> maybe_fix_env(), ?INFO("Dialyzer starting, this may take a while...", []), - code:add_pathsa(rebar_state:code_paths(State, all_deps)), + rebar_paths:unset_paths([plugins], State), % no plugins in analysis + rebar_paths:set_paths([deps], State), Plt = get_plt(State), try @@ -104,7 +105,7 @@ do(State) -> throw:{output_file_error, _, _} = Error -> ?PRV_ERROR(Error) after - rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)) + rebar_paths:set_paths([plugins,deps], State) end. %% This is used to workaround dialyzer quirk discussed here diff --git a/src/rebar_prv_edoc.erl b/src/rebar_prv_edoc.erl index 9517335..c78296a 100644 --- a/src/rebar_prv_edoc.erl +++ b/src/rebar_prv_edoc.erl @@ -32,7 +32,7 @@ init(State) -> -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()} | {error, {module(), any()}}. do(State) -> - code:add_pathsa(rebar_state:code_paths(State, all_deps)), + rebar_paths:set_paths([deps, plugins], State), ProjectApps = rebar_state:project_apps(State), Providers = rebar_state:providers(State), EdocOpts = rebar_state:get(State, edoc_opts, []), @@ -64,7 +64,7 @@ do(State) -> {app_failed, AppName} end, rebar_hooks:run_all_hooks(Cwd, post, ?PROVIDER, Providers, State), - rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)), + rebar_paths:set_paths([plugins, deps], State), case Res of {app_failed, App} -> ?PRV_ERROR({app_failed, App}); diff --git a/src/rebar_prv_eunit.erl b/src/rebar_prv_eunit.erl index 4b71416..f120926 100644 --- a/src/rebar_prv_eunit.erl +++ b/src/rebar_prv_eunit.erl @@ -54,7 +54,7 @@ do(State, Tests) -> ?INFO("Performing EUnit tests...", []), setup_name(State), - rebar_utils:update_code(rebar_state:code_paths(State, all_deps), [soft_purge]), + rebar_paths:set_paths([deps, plugins], State), %% Run eunit provider prehooks Providers = rebar_state:providers(State), @@ -67,14 +67,14 @@ do(State, Tests) -> {ok, State1} -> %% Run eunit provider posthooks rebar_hooks:run_project_and_app_hooks(Cwd, post, ?PROVIDER, Providers, State1), - rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)), + rebar_paths:set_paths([plugins, deps], State), {ok, State1}; Error -> - rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)), + rebar_paths:set_paths([plugins, deps], State), Error end; Error -> - rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)), + rebar_paths:set_paths([plugins, deps], State), Error end. diff --git a/src/rebar_prv_install_deps.erl b/src/rebar_prv_install_deps.erl index b735ed0..068c4c8 100644 --- a/src/rebar_prv_install_deps.erl +++ b/src/rebar_prv_install_deps.erl @@ -259,9 +259,21 @@ update_seen_dep(AppInfo, _Profile, _Level, Deps, Apps, State, Upgrade, Seen, Loc %% If seen from lock file or user requested an upgrade %% don't print warning about skipping case lists:keymember(Name, 1, Locks) of - false when Upgrade -> ok; - false when not Upgrade -> warn_skip_deps(AppInfo, State); - true -> ok + false when Upgrade -> + ok; + false when not Upgrade -> + {ok, SeenApp} = rebar_app_utils:find(Name, Apps), + Source = rebar_app_info:source(AppInfo), + case rebar_app_info:source(SeenApp) of + Source -> + %% dep is the same version and checksum as the one we already saw. + %% meaning there is no conflict, so don't warn about it. + skip; + _ -> + warn_skip_deps(Name, Source, State) + end; + true -> + ok end, {Deps, Apps, State, Seen}. @@ -277,10 +289,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 +307,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 +381,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) -> +warn_skip_deps(Name, Source, 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)], + Args = [Name, + rebar_resource_v2:format_source(Source)], 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_plugins.erl b/src/rebar_prv_plugins.erl index 4bea3b3..d66b645 100644 --- a/src/rebar_prv_plugins.erl +++ b/src/rebar_prv_plugins.erl @@ -36,7 +36,7 @@ do(State) -> GlobalPlugins = rebar_state:get(GlobalConfig, plugins, []), GlobalSrcDirs = rebar_state:get(GlobalConfig, src_dirs, ["src"]), GlobalPluginsDir = filename:join([rebar_dir:global_cache_dir(rebar_state:opts(State)), "plugins", "*"]), - GlobalApps = rebar_app_discover:find_apps([GlobalPluginsDir], GlobalSrcDirs, all), + GlobalApps = rebar_app_discover:find_apps([GlobalPluginsDir], GlobalSrcDirs, all, State), display_plugins("Global plugins", GlobalApps, GlobalPlugins), RebarOpts = rebar_state:opts(State), @@ -44,7 +44,7 @@ do(State) -> Plugins = rebar_state:get(State, plugins, []), PluginsDirs = filelib:wildcard(filename:join(rebar_dir:plugins_dir(State), "*")), CheckoutsDirs = filelib:wildcard(filename:join(rebar_dir:checkouts_dir(State), "*")), - Apps = rebar_app_discover:find_apps(CheckoutsDirs++PluginsDirs, SrcDirs, all), + Apps = rebar_app_discover:find_apps(CheckoutsDirs++PluginsDirs, SrcDirs, all, State), display_plugins("Local plugins", Apps, Plugins), {ok, State}. 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 af8d99f..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 %% =================================================================== @@ -385,7 +387,7 @@ reread_config(AppsToStart, State) -> lists:member(App, Running), lists:member(App, AppsToStart), not lists:member(App, BlackList)], - _ = rebar_utils:reread_config(ConfigList), + _ = rebar_utils:reread_config(ConfigList, [update_logger]), ok end. 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_prv_xref.erl b/src/rebar_prv_xref.erl index 2405ebb..12063d5 100644 --- a/src/rebar_prv_xref.erl +++ b/src/rebar_prv_xref.erl @@ -36,8 +36,7 @@ init(State) -> -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> - OldPath = code:get_path(), - code:add_pathsa(rebar_state:code_paths(State, all_deps)), + rebar_paths:set_paths([deps], State), XrefChecks = prepare(State), XrefIgnores = rebar_state:get(State, xref_ignores, []), %% Run xref checks @@ -48,7 +47,6 @@ do(State) -> QueryChecks = rebar_state:get(State, xref_queries, []), QueryResults = lists:foldl(fun check_query/2, [], QueryChecks), stopped = xref:stop(xref), - rebar_utils:cleanup_code_path(OldPath), case XrefResults =:= [] andalso QueryResults =:= [] of true -> {ok, State}; diff --git a/src/rebar_relx.erl b/src/rebar_relx.erl index 4548761..431e1bc 100644 --- a/src/rebar_relx.erl +++ b/src/rebar_relx.erl @@ -40,7 +40,8 @@ do(Module, Command, Provider, State) -> ,{caller, api} ,{log_level, LogLevel} | output_dir(OutputDir, Options)] ++ ErlOpts, AllOptions); Config -> - Config1 = merge_overlays(Config), + Config1 = [{overlay_vars, [{base_dir, rebar_dir:base_dir(State)}]} + | merge_overlays(Config)], relx:main([{lib_dirs, LibDirs} ,{config, Config1} ,{caller, api} 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..31d3a08 100644 --- a/src/rebar_state.erl +++ b/src/rebar_state.erl @@ -38,6 +38,12 @@ to_list/1, + compilers/1, compilers/2, + prepend_compilers/2, append_compilers/2, + + project_builders/1, add_project_builder/3, + + create_resources/2, set_resources/2, resources/1, resources/2, add_resource/2, providers/1, providers/2, add_provider/2, allow_provider_overrides/1, allow_provider_overrides/2 @@ -65,6 +71,8 @@ all_plugin_deps = [] :: [rebar_app_info:t()], all_deps = [] :: [rebar_app_info:t()], + compilers = [] :: [{compiler_type(), extension(), extension(), compile_fun()}], + project_builders = [] :: [{rebar_app_info:project_type(), module()}], resources = [], providers = [], allow_provider_overrides = false :: boolean()}). @@ -73,28 +81,30 @@ -type t() :: #state_t{}. +-type compiler_type() :: atom(). +-type extension() :: string(). +-type compile_fun() :: fun(([file:filename()], rebar_app_info:t(), list()) -> ok). + -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 +139,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 +364,80 @@ 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(). -resources(State, NewResources) -> - State#state_t{resources=NewResources}. +-spec set_resources(t(), [{rebar_resource_v2:type(), module()}]) -> t(). +set_resources(State, Resources) -> + State#state_t{resources=Resources}. --spec add_resource(t(), {rebar_resource:type(), module()}) -> t(). -add_resource(State=#state_t{resources=Resources}, Resource) -> +-spec resources(t(), [{rebar_resource_v2:type(), module()}]) -> t(). +resources(State, NewResources) -> + 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]). + +compilers(#state_t{compilers=Compilers}) -> + Compilers. + +prepend_compilers(State=#state_t{compilers=Compilers}, NewCompilers) -> + State#state_t{compilers=NewCompilers++Compilers}. + +append_compilers(State=#state_t{compilers=Compilers}, NewCompilers) -> + State#state_t{compilers=Compilers++NewCompilers}. + +compilers(State, Compilers) -> + State#state_t{compilers=Compilers}. + +project_builders(#state_t{project_builders=ProjectBuilders}) -> + ProjectBuilders. + +add_project_builder(State=#state_t{project_builders=ProjectBuilders}, Type, Module) -> + _ = code:ensure_loaded(Module), + case erlang:function_exported(Module, build, 1) of + true -> + State#state_t{project_builders=[{Type, Module} | ProjectBuilders]}; + false -> + ?WARN("Unable to add project builder for type ~s, required function ~s:build/1 not found.", + [Type, Module]), + State + end. + +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 2ded481..1769b79 100644 --- a/src/rebar_utils.erl +++ b/src/rebar_utils.erl @@ -37,8 +37,9 @@ escript_foldl/3, find_files/2, find_files/3, + find_files_in_dirs/3, + find_source/3, beam_to_mod/1, - beam_to_mod/2, erl_to_mod/1, beams/1, find_executable/1, @@ -72,15 +73,17 @@ info_useless/2, list_dir/1, user_agent/0, - reread_config/1, + 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). @@ -204,6 +207,12 @@ sh(Command0, Options0) -> find_files(Dir, Regex) -> find_files(Dir, Regex, true). +find_files_in_dirs([], _Regex, _Recursive) -> + []; +find_files_in_dirs([Dir | T], Regex, Recursive) -> + find_files(Dir, Regex, Recursive) ++ find_files_in_dirs(T, Regex, Recursive). + + find_files(Dir, Regex, Recursive) -> filelib:fold_files(Dir, Regex, Recursive, fun(F, Acc) -> [F | Acc] end, []). @@ -436,6 +445,18 @@ user_agent() -> ?FMT("Rebar/~ts (OTP/~ts)", [Vsn, otp_release()]). reread_config(ConfigList) -> + %% Default to not re-configuring the logger for now; + %% this can leak logs in CT redirection when setting up hooks + %% for example. If we want to turn it on by default, we may + %% want to disable it in CT at the same time or figure out a + %% way to silence it. + %% The same pattern may apply to other tasks, so let's enable + %% case-by-case. + reread_config(ConfigList, []). + +reread_config(ConfigList, Opts) -> + UpdateLoggerConfig = erlang:function_exported(logger, module_info, 0) andalso + proplists:get_value(update_logger, Opts, false), %% NB: we attempt to mimic -config here, which survives app reload, %% hence {persistent, true}. SetEnv = case version_tuple(?MODULE:otp_release()) of @@ -445,15 +466,52 @@ reread_config(ConfigList) -> fun (App, Key, Val) -> application:set_env(App, Key, Val, [{persistent, true}]) end end, try + Res = [SetEnv(Application, Key, Val) || Config <- ConfigList, {Application, Items} <- Config, - {Key, Val} <- Items] + {Key, Val} <- Items], + case UpdateLoggerConfig of + true -> reread_logger_config(); + false -> ok + end, + Res catch _:_ -> ?ERROR("The configuration file submitted could not be read " "and will be ignored.", []) end. +%% @private since the kernel app is already booted, re-reading its config +%% requires doing some magic to dynamically patch running handlers to +%% deal with the current value. +reread_logger_config() -> + KernelCfg = application:get_all_env(kernel), + LogCfg = proplists:get_value(logger, KernelCfg), + case LogCfg of + undefined -> + ok; + _ -> + %% Extract and apply settings related to primary configuration + %% -- primary config is used for settings shared across handlers + LogLvlPrimary = proplists:get_value(logger_info, KernelCfg, all), + {FilterDefault, Filters} = + case lists:keyfind(filters, 1, KernelCfg) of + false -> {log, []}; + {filters, FoundDef, FoundFilter} -> {FoundDef, FoundFilter} + end, + Primary = #{level => LogLvlPrimary, + filter_default => FilterDefault, + filters => Filters}, + %% Load the correct handlers based on their individual config. + [case Id of + default -> logger:update_handler_config(Id, Cfg); + _ -> logger:add_handler(Id, Mod, Cfg) + end || {handler, Id, Mod, Cfg} <- LogCfg], + logger:set_primary_config(Primary), + ok + end. + + %% @doc Given env. variable `FOO' we want to expand all references to %% it in `InStr'. References can have two forms: `$FOO' and `${FOO}' %% The end of form `$FOO' is delimited with whitespace or EOL @@ -620,10 +678,6 @@ sh_loop(Port, Fun, Acc) -> end end. -beam_to_mod(Dir, Filename) -> - [Dir | Rest] = filename:split(Filename), - list_to_atom(filename:basename(rebar_string:join(Rest, "."), ".beam")). - beam_to_mod(Filename) -> list_to_atom(filename:basename(Filename, ".beam")). @@ -661,12 +715,21 @@ escript_foldl(Fun, Acc, File) -> Error end. -vcs_vsn(Vcs, Dir, Resources) -> - case vcs_vsn_cmd(Vcs, Dir, Resources) of +%% TODO: this is just for rebar3_hex and maybe other plugins +%% but eventually it should be dropped +vcs_vsn(OriginalVsn, Dir, Resources) when is_list(Dir) , + is_list(Resources) -> + ?WARN("Using deprecated rebar_utils:vcs_vsn/3. Please upgrade your plugins.", []), + FakeState = rebar_state:new(), + {ok, AppInfo} = rebar_app_info:new(fake, OriginalVsn, Dir), + vcs_vsn(AppInfo, OriginalVsn, + rebar_state:set_resources(FakeState, Resources)); +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} -> @@ -674,23 +737,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(_, 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 @@ -705,19 +763,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) -> @@ -928,3 +973,190 @@ 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)}. + + +-ifdef(filelib_find_source). +find_source(Filename, Dir, Rules) -> + filelib:find_source(Filename, Dir, Rules). +-else. +%% Looks for a file relative to a given directory + +-type find_file_rule() :: {ObjDirSuffix::string(), SrcDirSuffix::string()}. + +%% Looks for a source file relative to the object file name and directory + +-type find_source_rule() :: {ObjExtension::string(), SrcExtension::string(), + [find_file_rule()]}. + +keep_suffix_search_rules(Rules) -> + [T || {_,_,_}=T <- Rules]. + +-spec find_source(file:filename(), file:filename(), [find_source_rule()]) -> + {ok, file:filename()} | {error, not_found}. +find_source(Filename, Dir, Rules) -> + try_suffix_rules(keep_suffix_search_rules(Rules), Filename, Dir). + +try_suffix_rules(Rules, Filename, Dir) -> + Ext = filename:extension(Filename), + try_suffix_rules(Rules, filename:rootname(Filename, Ext), Dir, Ext). + +try_suffix_rules([{Ext,Src,Rules}|Rest], Root, Dir, Ext) + when is_list(Src), is_list(Rules) -> + case try_dir_rules(add_local_search(Rules), Root ++ Src, Dir) of + {ok, File} -> {ok, File}; + _Other -> + try_suffix_rules(Rest, Root, Dir, Ext) + end; +try_suffix_rules([_|Rest], Root, Dir, Ext) -> + try_suffix_rules(Rest, Root, Dir, Ext); +try_suffix_rules([], _Root, _Dir, _Ext) -> + {error, not_found}. + +%% ensuring we check the directory of the object file before any other directory +add_local_search(Rules) -> + Local = {"",""}, + [Local] ++ lists:filter(fun (X) -> X =/= Local end, Rules). + +try_dir_rules([{From, To}|Rest], Filename, Dir) + when is_list(From), is_list(To) -> + case try_dir_rule(Dir, Filename, From, To) of + {ok, File} -> {ok, File}; + error -> try_dir_rules(Rest, Filename, Dir) + end; +try_dir_rules([], _Filename, _Dir) -> + {error, not_found}. + +try_dir_rule(Dir, Filename, From, To) -> + case lists:suffix(From, Dir) of + true -> + NewDir = lists:sublist(Dir, 1, length(Dir)-length(From))++To, + Src = filename:join(NewDir, Filename), + case filelib:is_regular(Src) of + true -> {ok, Src}; + false -> find_regular_file(filelib:wildcard(Src)) + end; + false -> + error + end. + +find_regular_file([]) -> + error; +find_regular_file([File|Files]) -> + case filelib:is_regular(File) of + true -> {ok, File}; + false -> find_regular_file(Files) + end. +-endif. |