aboutsummaryrefslogblamecommitdiffstats
path: root/plugins/hex.mk
blob: b0a5339bef3dc52227405678a50541084b0d22f5 (plain) (tree)















































































































































































































































































































































                                                                                                                                              
# Copyright (c) 2020, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.

HEX_CORE_GIT ?= https://github.com/hexpm/hex_core
HEX_CORE_COMMIT ?= v0.7.0

PACKAGES += hex_core
pkg_hex_core_name = hex_core
pkg_hex_core_description = Reference implementation of Hex specifications
pkg_hex_core_homepage = $(HEX_CORE_GIT)
pkg_hex_core_fetch = git
pkg_hex_core_repo = $(HEX_CORE_GIT)
pkg_hex_core_commit = $(HEX_CORE_COMMIT)

# We automatically depend on hex_core when the project isn't already.
$(if $(filter hex_core,$(DEPS) $(BUILD_DEPS) $(DOC_DEPS) $(REL_DEPS) $(TEST_DEPS)),,\
	$(eval $(call dep_target,hex_core)))

hex-core: $(DEPS_DIR)/hex_core
	$(verbose) if [ ! -e $(DEPS_DIR)/hex_core/ebin/dep_built ]; then \
		$(MAKE) -C $(DEPS_DIR)/hex_core IS_DEP=1; \
		touch $(DEPS_DIR)/hex_core/ebin/dep_built; \
	fi

# @todo This must also apply to fetching.
HEX_CONFIG ?=

define hex_config.erl
	begin
		Config0 = hex_core:default_config(),
		Config0$(HEX_CONFIG)
	end
endef

define hex_user_create.erl
	{ok, _} = application:ensure_all_started(ssl),
	{ok, _} = application:ensure_all_started(inets),
	Config = $(hex_config.erl),
	case hex_api_user:create(Config, <<"$(strip $1)">>, <<"$(strip $2)">>, <<"$(strip $3)">>) of
		{ok, {201, _, #{<<"email">> := Email, <<"url">> := URL, <<"username">> := Username}}} ->
			io:format("User ~s (~s) created at ~s~n"
				"Please check your inbox for a confirmation email.~n"
				"You must confirm before you are allowed to publish packages.~n",
				[Username, Email, URL]),
			halt(0);
		{ok, {Status, _, Errors}} ->
			io:format("Error ~b: ~0p~n", [Status, Errors]),
			halt(80)
	end
endef

# The $(info ) call inserts a new line after the password prompt.
hex-user-create: hex-core
	$(if $(HEX_USERNAME),,$(eval HEX_USERNAME := $(shell read -p "Username: " username; echo $$username)))
	$(if $(HEX_PASSWORD),,$(eval HEX_PASSWORD := $(shell stty -echo; read -p "Password: " password; stty echo; echo $$password) $(info )))
	$(if $(HEX_EMAIL),,$(eval HEX_EMAIL := $(shell read -p "Email: " email; echo $$email)))
	$(gen_verbose) $(call erlang,$(call hex_user_create.erl,$(HEX_USERNAME),$(HEX_PASSWORD),$(HEX_EMAIL)))

define hex_key_add.erl
	{ok, _} = application:ensure_all_started(ssl),
	{ok, _} = application:ensure_all_started(inets),
	Config = $(hex_config.erl),
	ConfigF = Config#{api_key => iolist_to_binary([<<"Basic ">>, base64:encode(<<"$(strip $1):$(strip $2)">>)])},
	Permissions = [
		case string:split(P, <<":">>) of
			[D] -> #{domain => D};
			[D, R] -> #{domain => D, resource => R}
		end
	|| P <- string:split(<<"$(strip $4)">>, <<",">>, all)],
	case hex_api_key:add(ConfigF, <<"$(strip $3)">>, Permissions) of
		{ok, {201, _, #{<<"secret">> := Secret}}} ->
			io:format("Key ~s created for user ~s~nSecret: ~s~n"
				"Please store the secret in a secure location, such as a password store.~n"
				"The secret will be requested for most Hex-related operations.~n",
				[<<"$(strip $3)">>, <<"$(strip $1)">>, Secret]),
			halt(0);
		{ok, {Status, _, Errors}} ->
			io:format("Error ~b: ~0p~n", [Status, Errors]),
			halt(81)
	end
endef

hex-key-add: hex-core
	$(if $(HEX_USERNAME),,$(eval HEX_USERNAME := $(shell read -p "Username: " username; echo $$username)))
	$(if $(HEX_PASSWORD),,$(eval HEX_PASSWORD := $(shell stty -echo; read -p "Password: " password; stty echo; echo $$password) $(info )))
	$(gen_verbose) $(call erlang,$(call hex_key_add.erl,$(HEX_USERNAME),$(HEX_PASSWORD),\
		$(if $(name),$(name),$(shell hostname)-erlang-mk),\
		$(if $(perm),$(perm),api)))

HEX_TARBALL_EXTRA_METADATA ?=

# @todo Check that we can += files
HEX_TARBALL_FILES ?= \
	$(wildcard early-plugins.mk) \
	$(wildcard ebin/$(PROJECT).app) \
	$(wildcard ebin/$(PROJECT).appup) \
	$(wildcard $(notdir $(ERLANG_MK_FILENAME))) \
	$(sort $(call core_find,include/,*.hrl)) \
	$(wildcard LICENSE*) \
	$(wildcard Makefile) \
	$(wildcard plugins.mk) \
	$(sort $(call core_find,priv/,*)) \
	$(wildcard README*) \
	$(wildcard rebar.config) \
	$(sort $(call core_find,src/,*))

HEX_TARBALL_OUTPUT_FILE ?= $(ERLANG_MK_TMP)/$(PROJECT).tar

# @todo Need to check for rebar.config and/or the absence of DEPS to know
# whether a project will work with Rebar.
#
# @todo contributors licenses links in HEX_TARBALL_EXTRA_METADATA

# In order to build the requirements metadata we look into DEPS.
# We do not require that the project use Hex dependencies, however
# Hex.pm does require that the package name and version numbers
# correspond to a real Hex package.
define hex_tarball_create.erl
	Files0 = [$(call comma_list,$(patsubst %,"%",$(HEX_TARBALL_FILES)))],
	Requirements0 = #{
		$(foreach d,$(DEPS),
			<<"$(if $(subst hex,,$(call query_fetch_method,$d)),$d,$(if $(word 3,$(dep_$d)),$(word 3,$(dep_$d)),$d))">> => #{
				<<"app">> => <<"$d">>,
				<<"optional">> => false,
				<<"requirement">> => <<"$(call query_version,$d)">>
			},)
		$(if $(DEPS),dummy => dummy)
	},
	Requirements = maps:remove(dummy, Requirements0),
	Metadata0 = #{
		app => <<"$(strip $(PROJECT))">>,
		build_tools => [<<"make">>, <<"rebar3">>],
		description => <<"$(strip $(PROJECT_DESCRIPTION))">>,
		files => [unicode:characters_to_binary(F) || F <- Files0],
		name => <<"$(strip $(PROJECT))">>,
		requirements => Requirements,
		version => <<"$(strip $(PROJECT_VERSION))">>
	},
	Metadata = Metadata0$(HEX_TARBALL_EXTRA_METADATA),
	Files = [case file:read_file(F) of
		{ok, Bin} ->
			{F, Bin};
		{error, Reason} ->
			io:format("Error trying to open file ~0p: ~0p~n", [F, Reason]),
			halt(82)
	end || F <- Files0],
	case hex_tarball:create(Metadata, Files) of
		{ok, #{tarball := Tarball}} ->
			ok = file:write_file("$(strip $(HEX_TARBALL_OUTPUT_FILE))", Tarball),
			halt(0);
		{error, Reason} ->
			io:format("Error ~0p~n", [Reason]),
			halt(83)
	end
endef

hex_tar_verbose_0 = @echo " TAR    $(notdir $(ERLANG_MK_TMP))/$(@F)";
hex_tar_verbose_2 = set -x;
hex_tar_verbose = $(hex_tar_verbose_$(V))

$(HEX_TARBALL_OUTPUT_FILE): hex-core app
	$(hex_tar_verbose) $(call erlang,$(call hex_tarball_create.erl))

hex-tarball-create: $(HEX_TARBALL_OUTPUT_FILE)

define hex_release_publish_summary.erl
	{ok, Tarball} = erl_tar:open("$(strip $(HEX_TARBALL_OUTPUT_FILE))", [read]),
	ok = erl_tar:extract(Tarball, [{cwd, "$(ERLANG_MK_TMP)"}, {files, ["metadata.config"]}]),
	{ok, Metadata} = file:consult("$(ERLANG_MK_TMP)/metadata.config"),
	#{
		<<"name">> := Name,
		<<"version">> := Version,
		<<"files">> := Files,
		<<"requirements">> := Deps
	} = maps:from_list(Metadata),
	io:format("Publishing ~s ~s~n  Dependencies:~n", [Name, Version]),
	case Deps of
		[] ->
			io:format("    (none)~n");
		_ ->
			[begin
				#{<<"app">> := DA, <<"requirement">> := DR} = maps:from_list(D),
				io:format("    ~s ~s~n", [DA, DR])
			end || {_, D} <- Deps]
	end,
	io:format("  Included files:~n"),
	[io:format("    ~s~n", [F]) || F <- Files],
	io:format("You may also review the contents of the tarball file.~n"
		"Please enter your secret key to proceed.~n"),
	halt(0)
endef

define hex_release_publish.erl
	{ok, _} = application:ensure_all_started(ssl),
	{ok, _} = application:ensure_all_started(inets),
	Config = $(hex_config.erl),
	ConfigF = Config#{api_key => <<"$(strip $1)">>},
	{ok, Tarball} = file:read_file("$(strip $(HEX_TARBALL_OUTPUT_FILE))"),
	case hex_api_release:publish(ConfigF, Tarball, [{replace, $2}]) of
		{ok, {200, _, #{}}} ->
			io:format("Release replaced~n"),
			halt(0);
		{ok, {201, _, #{}}} ->
			io:format("Release published~n"),
			halt(0);
		{ok, {Status, _, Errors}} ->
			io:format("Error ~b: ~0p~n", [Status, Errors]),
			halt(84)
	end
endef

hex-release-tarball: hex-core $(HEX_TARBALL_OUTPUT_FILE)
	$(verbose) $(call erlang,$(call hex_release_publish_summary.erl))

hex-release-publish: hex-core hex-release-tarball
	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
	$(gen_verbose) $(call erlang,$(call hex_release_publish.erl,$(HEX_SECRET),false))

hex-release-replace: hex-core hex-release-tarball
	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
	$(gen_verbose) $(call erlang,$(call hex_release_publish.erl,$(HEX_SECRET),true))

define hex_release_delete.erl
	{ok, _} = application:ensure_all_started(ssl),
	{ok, _} = application:ensure_all_started(inets),
	Config = $(hex_config.erl),
	ConfigF = Config#{api_key => <<"$(strip $1)">>},
	case hex_api_release:delete(ConfigF, <<"$(strip $(PROJECT))">>, <<"$(strip $(PROJECT_VERSION))">>) of
		{ok, {204, _, _}} ->
			io:format("Release $(strip $(PROJECT_VERSION)) deleted~n"),
			halt(0);
		{ok, {Status, _, Errors}} ->
			io:format("Error ~b: ~0p~n", [Status, Errors]),
			halt(85)
	end
endef

hex-release-delete: hex-core
	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
	$(gen_verbose) $(call erlang,$(call hex_release_delete.erl,$(HEX_SECRET)))

define hex_release_retire.erl
	{ok, _} = application:ensure_all_started(ssl),
	{ok, _} = application:ensure_all_started(inets),
	Config = $(hex_config.erl),
	ConfigF = Config#{api_key => <<"$(strip $1)">>},
	Params = #{<<"reason">> => <<"$(strip $3)">>, <<"message">> => <<"$(strip $4)">>},
	case hex_api_release:retire(ConfigF, <<"$(strip $(PROJECT))">>, <<"$(strip $2)">>, Params) of
		{ok, {204, _, _}} ->
			io:format("Release $(strip $2) has been retired~n"),
			halt(0);
		{ok, {Status, _, Errors}} ->
			io:format("Error ~b: ~0p~n", [Status, Errors]),
			halt(86)
	end
endef

hex-release-retire: hex-core
	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
	$(gen_verbose) $(call erlang,$(call hex_release_retire.erl,$(HEX_SECRET),\
		$(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION)),\
		$(if $(HEX_REASON),$(HEX_REASON),invalid),\
		$(HEX_MESSAGE)))

define hex_release_unretire.erl
	{ok, _} = application:ensure_all_started(ssl),
	{ok, _} = application:ensure_all_started(inets),
	Config = $(hex_config.erl),
	ConfigF = Config#{api_key => <<"$(strip $1)">>},
	case hex_api_release:unretire(ConfigF, <<"$(strip $(PROJECT))">>, <<"$(strip $2)">>) of
		{ok, {204, _, _}} ->
			io:format("Release $(strip $2) is not retired anymore~n"),
			halt(0);
		{ok, {Status, _, Errors}} ->
			io:format("Error ~b: ~0p~n", [Status, Errors]),
			halt(87)
	end
endef

hex-release-unretire: hex-core
	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
	$(gen_verbose) $(call erlang,$(call hex_release_unretire.erl,$(HEX_SECRET),\
		$(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION))))

HEX_DOCS_DOC_DIR ?= doc/
HEX_DOCS_TARBALL_FILES ?= $(sort $(call core_find,$(HEX_DOCS_DOC_DIR),*))
HEX_DOCS_TARBALL_OUTPUT_FILE ?= $(ERLANG_MK_TMP)/$(PROJECT)-docs.tar.gz

$(HEX_DOCS_TARBALL_OUTPUT_FILE): hex-core app docs
	$(hex_tar_verbose) tar czf $(HEX_DOCS_TARBALL_OUTPUT_FILE) -C $(HEX_DOCS_DOC_DIR) \
		$(HEX_DOCS_TARBALL_FILES:$(HEX_DOCS_DOC_DIR)%=%)

hex-docs-tarball-create: $(HEX_DOCS_TARBALL_OUTPUT_FILE)

define hex_docs_publish.erl
	{ok, _} = application:ensure_all_started(ssl),
	{ok, _} = application:ensure_all_started(inets),
	Config = $(hex_config.erl),
	ConfigF = Config#{api_key => <<"$(strip $1)">>},
	{ok, Tarball} = file:read_file("$(strip $(HEX_DOCS_TARBALL_OUTPUT_FILE))"),
	case hex_api:post(ConfigF,
			["packages", "$(strip $(PROJECT))", "releases", "$(strip $(PROJECT_VERSION))", "docs"],
			{"application/octet-stream", Tarball}) of
		{ok, {Status, _, _}} when Status >= 200, Status < 300 ->
			io:format("Docs published~n"),
			halt(0);
		{ok, {Status, _, Errors}} ->
			io:format("Error ~b: ~0p~n", [Status, Errors]),
			halt(88)
	end
endef

hex-docs-publish: hex-core hex-docs-tarball-create
	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
	$(gen_verbose) $(call erlang,$(call hex_docs_publish.erl,$(HEX_SECRET)))

define hex_docs_delete.erl
	{ok, _} = application:ensure_all_started(ssl),
	{ok, _} = application:ensure_all_started(inets),
	Config = $(hex_config.erl),
	ConfigF = Config#{api_key => <<"$(strip $1)">>},
	case hex_api:delete(ConfigF,
			["packages", "$(strip $(PROJECT))", "releases", "$(strip $2)", "docs"]) of
		{ok, {Status, _, _}} when Status >= 200, Status < 300 ->
			io:format("Docs removed~n"),
			halt(0);
		{ok, {Status, _, Errors}} ->
			io:format("Error ~b: ~0p~n", [Status, Errors]),
			halt(89)
	end
endef

hex-docs-delete: hex-core
	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
	$(gen_verbose) $(call erlang,$(call hex_docs_delete.erl,$(HEX_SECRET),\
		$(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION))))