-module(rebar3_hex_pkg). -export([init/1, do/1, format_error/1]). -export([publish/2 ,publish/6 ,validate_app_details/1]). -include("rebar3_hex.hrl"). -define(PROVIDER, publish). -define(DEPS, [{default, lock}]). -define(ENDPOINT, "packages"). %% =================================================================== %% Public API %% =================================================================== -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> Provider = providers:create([ {name, ?PROVIDER}, {module, ?MODULE}, {namespace, hex}, {bare, true}, {deps, ?DEPS}, {example, "rebar3 hex publish"}, {short_desc, "Publish a new version of your package and update the package"}, {desc, ""}, {opts, [{revert, undefined, "revert", string, "Revert given version."}]} ]), State1 = rebar_state:add_provider(State, Provider), {ok, State1}. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> Apps = rebar3_hex_utils:select_apps(rebar_state:project_apps(State)), lists:foldl(fun(App, {ok, StateAcc}) -> do_(App, StateAcc) end, {ok, State}, Apps). -spec format_error(any()) -> iolist(). format_error({non_hex_deps, Excluded}) -> io_lib:format("Can not publish package because the following deps are not available in hex: ~s", [string:join(Excluded, ", ")]); format_error(has_contributors) -> "The contributors field is deprecated, please change to maintainers and rerun."; format_error(undefined_server_error) -> "Unknown server error"; format_error({status, Status}) -> rebar3_hex_http:pretty_print_status(Status); format_error({status, Status, undefined_server_error}) -> "Unknown server error: " ++ rebar3_hex_http:pretty_print_status(Status); format_error({status, Status, Error}) -> Message = maps:get(<<"message">>, Error, ""), Errors = maps:get(<<"errors">>, Error, ""), ErrorString = errors_to_string(Errors), io_lib:format("Status Code: ~s~nHex Error: ~s~n\t~s", [rebar3_hex_http:pretty_print_status(Status), Message, ErrorString]). %% =================================================================== %% Public API %% =================================================================== do_(App, State) -> Name = rebar_app_info:name(App), {Args, _} = rebar_state:command_parsed_args(State), Revert = proplists:get_value(revert, Args, undefined), case Revert of undefined -> case publish(App, State) of ok -> {ok, State}; stopped -> {ok, State}; Error -> Error end; Version -> case delete(App, Name, Version) of ok -> {ok, State}; Error -> Error end end. publish(App, State) -> AppDir = rebar_app_info:dir(App), Name = rebar_app_info:name(App), Version = rebar_app_info:original_vsn(App), ResolvedVersion = rebar_utils:vcs_vsn(Version, rebar_app_info:dir(App), rebar_state:resources(State)), {application, _, AppDetails} = rebar3_hex_utils:update_app_src(App, ResolvedVersion), Deps = rebar_state:get(State, {locks, default}, []), TopLevel = [{N, [{<<"app">>, N}, {<<"optional">>, false}, {<<"requirement">>, V}]} || {_,{pkg,N,V},0} <- Deps], Excluded = [binary_to_list(N) || {N,{T,_,_},0} <- Deps, T =/= pkg], case validate_app_details(AppDetails) of ok -> publish(AppDir, Name, ResolvedVersion, TopLevel, Excluded, AppDetails); Error -> Error end. publish(AppDir, Name, Version, Deps, [], AppDetails) -> Config = rebar_config:consult(AppDir), ConfigDeps = proplists:get_value(deps, Config, []), Deps1 = update_versions(ConfigDeps, Deps), Description = list_to_binary(proplists:get_value(description, AppDetails, "")), FilePaths = proplists:get_value(files, AppDetails, ?DEFAULT_FILES), AppSrc = {application, ec_cnv:to_atom(Name), AppDetails}, Files = rebar3_hex_utils:expand_paths(FilePaths, AppDir), AppFileSrc = filename:join("src", ec_cnv:to_list(Name)++".app.src"), AppSrcBinary = ec_cnv:to_binary(lists:flatten(io_lib:format("~tp.\n", [AppSrc]))), Files1 = [{AppFileSrc, AppSrcBinary} | lists:keydelete(AppFileSrc, 1, Files)], MetaDataFiles = [File || {File, _} <- Files], Maintainers = proplists:get_value(maintainers, AppDetails, []), Licenses = proplists:get_value(licenses, AppDetails, []), Links = proplists:get_value(links, AppDetails, []), BuildTools = proplists:get_value(build_tools, AppDetails, [<<"rebar3">>]), %% We check the app file for the 'pkg' key wich allows us to select %% a package name other then the app anem, if it is not set we default %% back to the app name. PkgName = ec_cnv:to_binary(proplists:get_value(pkg_name, AppDetails, Name)), Optional = [{app, Name} ,{requirements, Deps1} ,{maintainers, Maintainers} ,{precompiled, false} ,{parameters, []} ,{description, Description} ,{files, MetaDataFiles} ,{licenses, Licenses} ,{links, Links} ,{build_tools, BuildTools}], OptionalFiltered = [{Key, Value} || {Key, Value} <- Optional, Value =/= []], Meta = [{name, PkgName}, {version, Version} | OptionalFiltered], Filenames = [F || {_, F} <- Files], {ok, Auth} = rebar3_hex_config:auth(), ec_talk:say("Publishing ~s ~s", [PkgName, Version]), ec_talk:say(" Description: ~s", [Description]), ec_talk:say(" Dependencies:~n ~s", [format_deps(Deps1)]), ec_talk:say(" Included files:~n ~s", [string:join(Filenames, "\n ")]), ec_talk:say(" Maintainers:~n ~s", [format_maintainers(Maintainers)]), ec_talk:say(" Licenses: ~s", [format_licenses(Licenses)]), ec_talk:say(" Links:~n ~s", [format_links(Links)]), ec_talk:say(" Build tools: ~s", [format_build_tools(BuildTools)]), ec_talk:say("Before publishing, please read Hex CoC: https://hex.pm/docs/codeofconduct", []), case ec_talk:ask_default("Proceed?", boolean, "Y") of true -> upload_package(Auth, PkgName, Version, Meta, Files1); _ -> ec_talk:say("Goodbye..."), stopped end; publish(_AppDir, _Name, _Version, _Deps, Excluded, _AppDetails) -> ?PRV_ERROR({non_hex_deps, Excluded}). %% Internal functions validate_app_details(AppDetails) -> case proplists:is_defined(contributors, AppDetails) of true -> {error, {rebar3_hex_pkg, has_contributors}}; false -> ok end. delete(App, Name, Version) -> {ok, Auth} = rebar3_hex_config:auth(), {application, _, AppDetails} = rebar3_hex_utils:update_app_src(App, Version), PkgName = ec_cnv:to_binary(proplists:get_value(pkg_name, AppDetails, Name)), case rebar3_hex_http:delete(filename:join([?ENDPOINT, PkgName, "releases", Version]), Auth) of ok -> rebar_api:info("Successfully deleted package ~s ~s", [PkgName, Version]), case ec_talk:ask_default(io_lib:format("Also delete tag v~s?", [Version]), boolean, "N") of true -> rebar_utils:sh(io_lib:format("git tag -d v~s", [Version]), []); _ -> ok end; {error, Status} -> rebar_api:error("Unable to delete package ~s ~s (~p)", [PkgName, Version, Status]) end. upload_package(Auth, Name, Version, Meta, Files) -> {ok, Tar} = rebar3_hex_tar:create(Name, Version, Meta, Files), case rebar3_hex_http:post(filename:join([?ENDPOINT, Name, "releases"]) ,Auth ,Tar ,integer_to_list(byte_size(Tar))) of ok -> rebar_api:info("Published ~s ~s", [Name, Version]), ok; {error, Status, <<>>} -> ?PRV_ERROR({status, Status}); {error, Status, Error} -> ?PRV_ERROR({status, Status, Error}) end. errors_to_string(Value) when is_binary(Value) -> Value; errors_to_string({Key, Value}) -> io_lib:format("~s: ~s", [Key, errors_to_string(Value)]); errors_to_string(Errors) when is_list(Errors) -> lists:flatten([io_lib:format("~s", [errors_to_string(Values)]) || Values <- Errors]). format_deps(Deps) -> string:join([binary_to_list(<>) || {N, [{_, N}, {_, _}, {<<"requirement">>, V}]} <- Deps], "\n "). format_maintainers(Maintainers) -> string:join(Maintainers, "\n "). format_licenses(Licenses) -> string:join(Licenses, ", "). format_links(Links) -> string:join([lists:flatten([Name, ": ", Url]) || {Name, Url} <- Links], "\n "). format_build_tools(BuildTools) -> string:join([io_lib:format("~s", [Tool]) || Tool <- BuildTools], ", "). update_versions(ConfigDeps, Deps) -> [begin case lists:keyfind(binary_to_atom(N, utf8), 1, ConfigDeps) of {_, V} -> {N, lists:keyreplace(<<"requirement">>, 1, M, {<<"requirement">>, list_to_binary(V)})}; _ -> {N, M} end end || {N, M} <- Deps].