Browse Source

Add hex metadata and prepare_tag target

Also update Erlang.mk.
Loïc Hoguin 4 years ago
parent
commit
4bec14fbf0
2 changed files with 1201 additions and 299 deletions
  1. 33 0
      Makefile
  2. 1168 299
      erlang.mk

+ 33 - 0
Makefile

@@ -29,6 +29,20 @@ AUTO_CI_HIPE ?= OTP-LATEST
 # AUTO_CI_ERLLVM ?= OTP-LATEST
 AUTO_CI_WINDOWS ?= OTP-21+
 
+# Hex configuration.
+
+define HEX_TARBALL_EXTRA_METADATA
+#{
+	licenses => [<<"ISC">>],
+	links => #{
+		<<"User guide">> => <<"https://ninenines.eu/docs/en/ranch/1.8/guide/">>,
+		<<"Function reference">> => <<"https://ninenines.eu/docs/en/ranch/1.8/manual/">>,
+		<<"GitHub">> => <<"https://github.com/ninenines/ranch">>,
+		<<"Sponsor">> => <<"https://github.com/sponsors/essen">>
+	}
+}
+endef
+
 # Standard targets.
 
 include erlang.mk
@@ -50,3 +64,22 @@ DIALYZER_OPTS += --src -r test
 ci-setup:: $(DEPS_DIR)/ct_helper
 	$(gen_verbose) cp ~/.kerl/builds/$(CI_OTP_RELEASE)/otp_src_git/lib/ssl/test/erl_make_certs.erl deps/ct_helper/src/ || true
 	$(gen_verbose) $(MAKE) -C $(DEPS_DIR)/ct_helper clean app
+
+# Prepare for the release.
+
+prepare_tag:
+	$(verbose) $(warning Hex metadata: $(HEX_TARBALL_EXTRA_METADATA))
+	$(verbose) echo
+	$(verbose) echo -n "Most recent tag:            "
+	$(verbose) git tag --sort taggerdate | tail -n1
+	$(verbose) git verify-tag `git tag --sort taggerdate | tail -n1`
+	$(verbose) echo -n "MAKEFILE: "
+	$(verbose) grep -m1 PROJECT_VERSION Makefile
+	$(verbose) echo -n "APP:                 "
+	$(verbose) grep -m1 vsn ebin/$(PROJECT).app | sed 's/	//g'
+	$(verbose) echo -n "GUIDE:  "
+	$(verbose) grep -h dep_$(PROJECT)_commit doc/src/guide/*.asciidoc || true
+	$(verbose) echo
+	$(verbose) echo "Dependencies:"
+	$(verbose) grep ^DEPS Makefile || echo "DEPS ="
+	$(verbose) grep ^dep_ Makefile || true

+ 1168 - 299
erlang.mk

@@ -17,16 +17,16 @@
 ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST)))
 export ERLANG_MK_FILENAME
 
-ERLANG_MK_VERSION = 6c8664c
+ERLANG_MK_VERSION = d80984c
 ERLANG_MK_WITHOUT = 
 
 # Make 3.81 and 3.82 are deprecated.
 
-ifeq ($(MAKE_VERSION),3.81)
+ifeq ($(MAKELEVEL)$(MAKE_VERSION),03.81)
 $(warning Please upgrade to GNU Make 4 or later: https://erlang.mk/guide/installation.html)
 endif
 
-ifeq ($(MAKE_VERSION),3.82)
+ifeq ($(MAKELEVEL)$(MAKE_VERSION),03.82)
 $(warning Please upgrade to GNU Make 4 or later: https://erlang.mk/guide/installation.html)
 endif
 
@@ -47,10 +47,18 @@ verbose_0 = @
 verbose_2 = set -x;
 verbose = $(verbose_$(V))
 
+ifeq ($(V),3)
+SHELL := $(SHELL) -x
+endif
+
 gen_verbose_0 = @echo " GEN   " $@;
 gen_verbose_2 = set -x;
 gen_verbose = $(gen_verbose_$(V))
 
+gen_verbose_esc_0 = @echo " GEN   " $$@;
+gen_verbose_esc_2 = set -x;
+gen_verbose_esc = $(gen_verbose_esc_$(V))
+
 # Temporary files directory.
 
 ERLANG_MK_TMP ?= $(CURDIR)/.erlang.mk
@@ -58,7 +66,7 @@ export ERLANG_MK_TMP
 
 # "erl" command.
 
-ERL = erl +A0 -noinput -boot start_clean
+ERL = erl +A1 -noinput -boot no_dot_erlang
 
 # Platform detection.
 
@@ -111,6 +119,9 @@ endif
 
 distclean:: clean distclean-tmp
 
+$(ERLANG_MK_TMP):
+	$(verbose) mkdir -p $(ERLANG_MK_TMP)
+
 distclean-tmp:
 	$(gen_verbose) rm -rf $(ERLANG_MK_TMP)
 
@@ -164,7 +175,7 @@ $(ERL) $2 -pz $(ERLANG_MK_TMP)/rebar/ebin -eval "$(subst $(newline),,$(call esca
 endef
 
 ifeq ($(PLATFORM),msys2)
-core_native_path = $(subst \,\\\\,$(shell cygpath -w $1))
+core_native_path = $(shell cygpath -m $1)
 else
 core_native_path = $1
 endif
@@ -173,7 +184,8 @@ core_http_get = curl -Lf$(if $(filter-out 0,$(V)),,s)o $(call core_native_path,$
 
 core_eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1)))
 
-core_find = $(if $(wildcard $1),$(shell find $(1:%/=%) -type f -name $(subst *,\*,$2)))
+# We skip files that contain spaces because they end up causing issues.
+core_find = $(if $(wildcard $1),$(shell find $(1:%/=%) \( -type l -o -type f \) -name $(subst *,\*,$2) | grep -v " "))
 
 core_lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(1)))))))))))))))))))))))))))
 
@@ -182,6 +194,10 @@ core_ls = $(filter-out $(1),$(shell echo $(1)))
 # @todo Use a solution that does not require using perl.
 core_relpath = $(shell perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $1 $2)
 
+define core_render
+	printf -- '$(subst $(newline),\n,$(subst %,%%,$(subst ','\'',$(subst $(tab),$(WS),$(call $(1))))))\n' > $(2)
+endef
+
 # Automated update.
 
 ERLANG_MK_REPO ?= https://github.com/ninenines/erlang.mk
@@ -192,15 +208,16 @@ ERLANG_MK_BUILD_DIR ?= .erlang.mk.build
 erlang-mk: WITHOUT ?= $(ERLANG_MK_WITHOUT)
 erlang-mk:
 ifdef ERLANG_MK_COMMIT
-	git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR)
-	cd $(ERLANG_MK_BUILD_DIR) && git checkout $(ERLANG_MK_COMMIT)
+	$(verbose) git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR)
+	$(verbose) cd $(ERLANG_MK_BUILD_DIR) && git checkout $(ERLANG_MK_COMMIT)
 else
-	git clone --depth 1 $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR)
+	$(verbose) git clone --depth 1 $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR)
 endif
-	if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR)/build.config; fi
-	$(MAKE) -C $(ERLANG_MK_BUILD_DIR) WITHOUT='$(strip $(WITHOUT))'
-	cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk
-	rm -rf $(ERLANG_MK_BUILD_DIR)
+	$(verbose) if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR)/build.config; fi
+	$(gen_verbose) $(MAKE) --no-print-directory -C $(ERLANG_MK_BUILD_DIR) WITHOUT='$(strip $(WITHOUT))' UPGRADE=1
+	$(verbose) cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk
+	$(verbose) rm -rf $(ERLANG_MK_BUILD_DIR)
+	$(verbose) rm -rf $(ERLANG_MK_TMP)
 
 # The erlang.mk package index is bundled in the default erlang.mk build.
 # Search for the string "copyright" to skip to the rest of the code.
@@ -216,6 +233,8 @@ ifeq ($(strip $(KERL)),)
 KERL := $(ERLANG_MK_TMP)/kerl/kerl
 endif
 
+KERL_DIR = $(ERLANG_MK_TMP)/kerl
+
 export KERL
 
 KERL_GIT ?= https://github.com/kerl/kerl
@@ -226,24 +245,25 @@ KERL_MAKEFLAGS ?=
 OTP_GIT ?= https://github.com/erlang/otp
 
 define kerl_otp_target
-ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(1)),)
 $(KERL_INSTALL_DIR)/$(1): $(KERL)
-	MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $(1) $(1)
-	$(KERL) install $(1) $(KERL_INSTALL_DIR)/$(1)
-endif
+	$(verbose) if [ ! -d $$@ ]; then \
+		MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $(1) $(1); \
+		$(KERL) install $(1) $(KERL_INSTALL_DIR)/$(1); \
+	fi
 endef
 
 define kerl_hipe_target
-ifeq ($(wildcard $(KERL_INSTALL_DIR)/$1-native),)
 $(KERL_INSTALL_DIR)/$1-native: $(KERL)
-	KERL_CONFIGURE_OPTIONS=--enable-native-libs \
-		MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $1 $1-native
-	$(KERL) install $1-native $(KERL_INSTALL_DIR)/$1-native
-endif
+	$(verbose) if [ ! -d $$@ ]; then \
+		KERL_CONFIGURE_OPTIONS=--enable-native-libs \
+			MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $1 $1-native; \
+		$(KERL) install $1-native $(KERL_INSTALL_DIR)/$1-native; \
+	fi
 endef
 
-$(KERL):
-	$(verbose) mkdir -p $(ERLANG_MK_TMP)
+$(KERL): $(KERL_DIR)
+
+$(KERL_DIR): | $(ERLANG_MK_TMP)
 	$(gen_verbose) git clone --depth 1 $(KERL_GIT) $(ERLANG_MK_TMP)/kerl
 	$(verbose) cd $(ERLANG_MK_TMP)/kerl && git checkout $(KERL_COMMIT)
 	$(verbose) chmod +x $(KERL)
@@ -251,12 +271,14 @@ $(KERL):
 distclean:: distclean-kerl
 
 distclean-kerl:
-	$(gen_verbose) rm -rf $(KERL)
+	$(gen_verbose) rm -rf $(KERL_DIR)
 
 # Allow users to select which version of Erlang/OTP to use for a project.
 
 ifneq ($(strip $(LATEST_ERLANG_OTP)),)
-ERLANG_OTP := $(notdir $(lastword $(sort $(filter-out $(KERL_INSTALL_DIR)/OTP_R%,\
+# In some environments it is necessary to filter out master.
+ERLANG_OTP := $(notdir $(lastword $(sort\
+	$(filter-out $(KERL_INSTALL_DIR)/master $(KERL_INSTALL_DIR)/OTP_R%,\
 	$(filter-out %-rc1 %-rc2 %-rc3,$(wildcard $(KERL_INSTALL_DIR)/*[^-native]))))))
 endif
 
@@ -283,9 +305,9 @@ SHELL := env PATH=$(PATH) $(SHELL)
 $(eval $(call kerl_hipe_target,$(ERLANG_HIPE)))
 
 # Build Erlang/OTP only if it doesn't already exist.
-ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(ERLANG_HIPE))$(BUILD_ERLANG_OTP),)
+ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(ERLANG_HIPE)-native)$(BUILD_ERLANG_OTP),)
 $(info Building HiPE-enabled Erlang/OTP $(ERLANG_OTP)... Please wait...)
-$(shell $(MAKE) $(KERL_INSTALL_DIR)/$(ERLANG_HIPE) ERLANG_HIPE=$(ERLANG_HIPE) BUILD_ERLANG_OTP=1 >&2)
+$(shell $(MAKE) $(KERL_INSTALL_DIR)/$(ERLANG_HIPE)-native ERLANG_HIPE=$(ERLANG_HIPE) BUILD_ERLANG_OTP=1 >&2)
 endif
 
 endif
@@ -933,10 +955,10 @@ pkg_cr_commit = master
 
 PACKAGES += cuttlefish
 pkg_cuttlefish_name = cuttlefish
-pkg_cuttlefish_description = never lose your childlike sense of wonder baby cuttlefish, promise me?
-pkg_cuttlefish_homepage = https://github.com/basho/cuttlefish
+pkg_cuttlefish_description = cuttlefish configuration abstraction
+pkg_cuttlefish_homepage = https://github.com/Kyorai/cuttlefish
 pkg_cuttlefish_fetch = git
-pkg_cuttlefish_repo = https://github.com/basho/cuttlefish
+pkg_cuttlefish_repo = https://github.com/Kyorai/cuttlefish
 pkg_cuttlefish_commit = master
 
 PACKAGES += damocles
@@ -1203,12 +1225,20 @@ pkg_eleveldb_fetch = git
 pkg_eleveldb_repo = https://github.com/basho/eleveldb
 pkg_eleveldb_commit = master
 
+PACKAGES += elixir
+pkg_elixir_name = elixir
+pkg_elixir_description = Elixir is a dynamic, functional language designed for building scalable and maintainable applications
+pkg_elixir_homepage = https://elixir-lang.org/
+pkg_elixir_fetch = git
+pkg_elixir_repo = https://github.com/elixir-lang/elixir
+pkg_elixir_commit = master
+
 PACKAGES += elli
 pkg_elli_name = elli
 pkg_elli_description = Simple, robust and performant Erlang web server
-pkg_elli_homepage = https://github.com/knutin/elli
+pkg_elli_homepage = https://github.com/elli-lib/elli
 pkg_elli_fetch = git
-pkg_elli_repo = https://github.com/knutin/elli
+pkg_elli_repo = https://github.com/elli-lib/elli
 pkg_elli_commit = master
 
 PACKAGES += elvis
@@ -1603,6 +1633,14 @@ pkg_escalus_fetch = git
 pkg_escalus_repo = https://github.com/esl/escalus
 pkg_escalus_commit = master
 
+PACKAGES += esh_mk
+pkg_esh_mk_name = esh_mk
+pkg_esh_mk_description = esh template engine plugin for erlang.mk
+pkg_esh_mk_homepage = https://github.com/crownedgrouse/esh.mk
+pkg_esh_mk_fetch = git
+pkg_esh_mk_repo = https://github.com/crownedgrouse/esh.mk.git
+pkg_esh_mk_commit = master
+
 PACKAGES += espec
 pkg_espec_name = espec
 pkg_espec_description = ESpec: Behaviour driven development framework for Erlang
@@ -1899,6 +1937,14 @@ pkg_gen_icmp_fetch = git
 pkg_gen_icmp_repo = https://github.com/msantos/gen_icmp
 pkg_gen_icmp_commit = master
 
+PACKAGES += gen_leader
+pkg_gen_leader_name = gen_leader
+pkg_gen_leader_description = leader election behavior
+pkg_gen_leader_homepage = https://github.com/garret-smith/gen_leader_revival
+pkg_gen_leader_fetch = git
+pkg_gen_leader_repo = https://github.com/garret-smith/gen_leader_revival
+pkg_gen_leader_commit = master
+
 PACKAGES += gen_nb_server
 pkg_gen_nb_server_name = gen_nb_server
 pkg_gen_nb_server_description = OTP behavior for writing non-blocking servers
@@ -1915,6 +1961,14 @@ pkg_gen_paxos_fetch = git
 pkg_gen_paxos_repo = https://github.com/gburd/gen_paxos
 pkg_gen_paxos_commit = master
 
+PACKAGES += gen_rpc
+pkg_gen_rpc_name = gen_rpc
+pkg_gen_rpc_description = A scalable RPC library for Erlang-VM based languages
+pkg_gen_rpc_homepage = https://github.com/priestjim/gen_rpc.git
+pkg_gen_rpc_fetch = git
+pkg_gen_rpc_repo = https://github.com/priestjim/gen_rpc.git
+pkg_gen_rpc_commit = master
+
 PACKAGES += gen_smtp
 pkg_gen_smtp_name = gen_smtp
 pkg_gen_smtp_description = A generic Erlang SMTP server and client that can be extended via callback modules
@@ -2281,7 +2335,7 @@ pkg_jsx_description = An Erlang application for consuming, producing and manipul
 pkg_jsx_homepage = https://github.com/talentdeficit/jsx
 pkg_jsx_fetch = git
 pkg_jsx_repo = https://github.com/talentdeficit/jsx
-pkg_jsx_commit = master
+pkg_jsx_commit = main
 
 PACKAGES += kafka
 pkg_kafka_name = kafka
@@ -2765,11 +2819,11 @@ pkg_myproto_commit = master
 
 PACKAGES += mysql
 pkg_mysql_name = mysql
-pkg_mysql_description = Erlang MySQL Driver (from code.google.com)
-pkg_mysql_homepage = https://github.com/dizzyd/erlang-mysql-driver
+pkg_mysql_description = MySQL client library for Erlang/OTP
+pkg_mysql_homepage = https://github.com/mysql-otp/mysql-otp
 pkg_mysql_fetch = git
-pkg_mysql_repo = https://github.com/dizzyd/erlang-mysql-driver
-pkg_mysql_commit = master
+pkg_mysql_repo = https://github.com/mysql-otp/mysql-otp
+pkg_mysql_commit = 1.7.0
 
 PACKAGES += n2o
 pkg_n2o_name = n2o
@@ -2979,6 +3033,14 @@ pkg_parsexml_fetch = git
 pkg_parsexml_repo = https://github.com/maxlapshin/parsexml
 pkg_parsexml_commit = master
 
+PACKAGES += partisan
+pkg_partisan_name = partisan
+pkg_partisan_description = High-performance, high-scalability distributed computing with Erlang and Elixir.
+pkg_partisan_homepage = http://partisan.cloud
+pkg_partisan_fetch = git
+pkg_partisan_repo = https://github.com/lasp-lang/partisan
+pkg_partisan_commit = master
+
 PACKAGES += pegjs
 pkg_pegjs_name = pegjs
 pkg_pegjs_description = An implementation of PEG.js grammar for Erlang.
@@ -2995,6 +3057,14 @@ pkg_percept2_fetch = git
 pkg_percept2_repo = https://github.com/huiqing/percept2
 pkg_percept2_commit = master
 
+PACKAGES += pgo
+pkg_pgo_name = pgo
+pkg_pgo_description = Erlang Postgres client and connection pool
+pkg_pgo_homepage = https://github.com/erleans/pgo.git
+pkg_pgo_fetch = git
+pkg_pgo_repo = https://github.com/erleans/pgo.git
+pkg_pgo_commit = master
+
 PACKAGES += pgsql
 pkg_pgsql_name = pgsql
 pkg_pgsql_description = Erlang PostgreSQL driver
@@ -3091,6 +3161,14 @@ pkg_procket_fetch = git
 pkg_procket_repo = https://github.com/msantos/procket
 pkg_procket_commit = master
 
+PACKAGES += prometheus
+pkg_prometheus_name = prometheus
+pkg_prometheus_description = Prometheus.io client in Erlang
+pkg_prometheus_homepage = https://github.com/deadtrickster/prometheus.erl
+pkg_prometheus_fetch = git
+pkg_prometheus_repo = https://github.com/deadtrickster/prometheus.erl
+pkg_prometheus_commit = master
+
 PACKAGES += prop
 pkg_prop_name = prop
 pkg_prop_description = An Erlang code scaffolding and generator system.
@@ -4253,6 +4331,9 @@ export DEPS_DIR
 REBAR_DEPS_DIR = $(DEPS_DIR)
 export REBAR_DEPS_DIR
 
+REBAR_GIT ?= https://github.com/rebar/rebar
+REBAR_COMMIT ?= 576e12171ab8d69b048b827b92aa65d067deea01
+
 # External "early" plugins (see core/plugins.mk for regular plugins).
 # They both use the core_dep_plugin macro.
 
@@ -4273,15 +4354,78 @@ $(foreach p,$(DEP_EARLY_PLUGINS),\
 		$(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\
 		$(call core_dep_plugin,$p/early-plugins.mk,$p))))
 
-dep_name = $(if $(dep_$(1)),$(1),$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$(1)))
-dep_repo = $(patsubst git://github.com/%,https://github.com/%, \
-	$(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_repo)))
-dep_commit = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 3,$(dep_$(1))),$(pkg_$(1)_commit)))
+# Query functions.
+
+query_fetch_method = $(if $(dep_$(1)),$(call _qfm_dep,$(word 1,$(dep_$(1)))),$(call _qfm_pkg,$(1)))
+_qfm_dep = $(if $(dep_fetch_$(1)),$(1),$(if $(IS_DEP),legacy,fail))
+_qfm_pkg = $(if $(pkg_$(1)_fetch),$(pkg_$(1)_fetch),fail)
+
+query_name = $(if $(dep_$(1)),$(1),$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$(1)))
+
+query_repo = $(call _qr,$(1),$(call query_fetch_method,$(1)))
+_qr = $(if $(query_repo_$(2)),$(call query_repo_$(2),$(1)),$(call dep_repo,$(1)))
+
+query_repo_default = $(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_repo))
+query_repo_git = $(patsubst git://github.com/%,https://github.com/%,$(call query_repo_default,$(1)))
+query_repo_git-subfolder = $(call query_repo_git,$(1))
+query_repo_git-submodule = -
+query_repo_hg = $(call query_repo_default,$(1))
+query_repo_svn = $(call query_repo_default,$(1))
+query_repo_cp = $(call query_repo_default,$(1))
+query_repo_ln = $(call query_repo_default,$(1))
+query_repo_hex = https://hex.pm/packages/$(if $(word 3,$(dep_$(1))),$(word 3,$(dep_$(1))),$(1))
+query_repo_fail = -
+query_repo_legacy = -
+
+query_version = $(call _qv,$(1),$(call query_fetch_method,$(1)))
+_qv = $(if $(query_version_$(2)),$(call query_version_$(2),$(1)),$(call dep_commit,$(1)))
+
+query_version_default = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 3,$(dep_$(1))),$(pkg_$(1)_commit)))
+query_version_git = $(call query_version_default,$(1))
+query_version_git-subfolder = $(call query_version_git,$(1))
+query_version_git-submodule = -
+query_version_hg = $(call query_version_default,$(1))
+query_version_svn = -
+query_version_cp = -
+query_version_ln = -
+query_version_hex = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_commit)))
+query_version_fail = -
+query_version_legacy = -
+
+query_extra = $(call _qe,$(1),$(call query_fetch_method,$(1)))
+_qe = $(if $(query_extra_$(2)),$(call query_extra_$(2),$(1)),-)
+
+query_extra_git = -
+query_extra_git-subfolder = $(if $(dep_$(1)),subfolder=$(word 4,$(dep_$(1))),-)
+query_extra_git-submodule = -
+query_extra_hg = -
+query_extra_svn = -
+query_extra_cp = -
+query_extra_ln = -
+query_extra_hex = $(if $(dep_$(1)),package-name=$(word 3,$(dep_$(1))),-)
+query_extra_fail = -
+query_extra_legacy = -
+
+query_absolute_path = $(addprefix $(DEPS_DIR)/,$(call query_name,$(1)))
+
+# Deprecated legacy query functions.
+dep_fetch = $(call query_fetch_method,$(1))
+dep_name = $(call query_name,$(1))
+dep_repo = $(call query_repo_git,$(1))
+dep_commit = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(if $(filter hex,$(word 1,$(dep_$(1)))),$(word 2,$(dep_$(1))),$(word 3,$(dep_$(1)))),$(pkg_$(1)_commit)))
 
 LOCAL_DEPS_DIRS = $(foreach a,$(LOCAL_DEPS),$(if $(wildcard $(APPS_DIR)/$(a)),$(APPS_DIR)/$(a)))
-ALL_APPS_DIRS = $(if $(wildcard $(APPS_DIR)/),$(filter-out $(APPS_DIR),$(shell find $(APPS_DIR) -maxdepth 1 -type d)))
 ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call dep_name,$(dep))))
 
+# When we are calling an app directly we don't want to include it here
+# otherwise it'll be treated both as an apps and a top-level project.
+ALL_APPS_DIRS = $(if $(wildcard $(APPS_DIR)/),$(filter-out $(APPS_DIR),$(shell find $(APPS_DIR) -maxdepth 1 -type d)))
+ifdef ROOT_DIR
+ifndef IS_APP
+ALL_APPS_DIRS := $(filter-out $(APPS_DIR)/$(notdir $(CURDIR)),$(ALL_APPS_DIRS))
+endif
+endif
+
 ifeq ($(filter $(APPS_DIR) $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),)
 ifeq ($(ERL_LIBS),)
 	ERL_LIBS = $(APPS_DIR):$(DEPS_DIR)
@@ -4299,50 +4443,82 @@ dep_verbose_0 = @echo " DEP    $1 ($(call dep_commit,$1))";
 dep_verbose_2 = set -x;
 dep_verbose = $(dep_verbose_$(V))
 
-# Core targets.
+# Optimization: don't recompile deps unless truly necessary.
 
-apps:: $(ALL_APPS_DIRS) clean-tmp-deps.log
-ifeq ($(IS_APP)$(IS_DEP),)
-	$(verbose) rm -f $(ERLANG_MK_TMP)/apps.log
+ifndef IS_DEP
+ifneq ($(MAKELEVEL),0)
+$(shell rm -f ebin/dep_built)
 endif
-	$(verbose) mkdir -p $(ERLANG_MK_TMP)
+endif
+
+# Core targets.
+
+ALL_APPS_DIRS_TO_BUILD = $(if $(LOCAL_DEPS_DIRS)$(IS_APP),$(LOCAL_DEPS_DIRS),$(ALL_APPS_DIRS))
+
+apps:: $(ALL_APPS_DIRS) clean-tmp-deps.log | $(ERLANG_MK_TMP)
 # Create ebin directory for all apps to make sure Erlang recognizes them
 # as proper OTP applications when using -include_lib. This is a temporary
 # fix, a proper fix would be to compile apps/* in the right order.
 ifndef IS_APP
+ifneq ($(ALL_APPS_DIRS),)
 	$(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \
 		mkdir -p $$dep/ebin; \
 	done
 endif
-# at the toplevel: if LOCAL_DEPS is defined with at least one local app, only
-# compile that list of apps. otherwise, compile everything.
-# within an app: compile all LOCAL_DEPS that are (uncompiled) local apps
-	$(verbose) set -e; for dep in $(if $(LOCAL_DEPS_DIRS)$(IS_APP),$(LOCAL_DEPS_DIRS),$(ALL_APPS_DIRS)) ; do \
+endif
+# At the toplevel: if LOCAL_DEPS is defined with at least one local app, only
+# compile that list of apps. Otherwise, compile everything.
+# Within an app: compile all LOCAL_DEPS that are (uncompiled) local apps.
+ifneq ($(ALL_APPS_DIRS_TO_BUILD),)
+	$(verbose) set -e; for dep in $(ALL_APPS_DIRS_TO_BUILD); do \
 		if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/apps.log; then \
 			:; \
 		else \
 			echo $$dep >> $(ERLANG_MK_TMP)/apps.log; \
-			$(MAKE) -C $$dep IS_APP=1; \
+			$(MAKE) -C $$dep $(if $(IS_TEST),test-build-app) IS_APP=1; \
 		fi \
 	done
+endif
 
 clean-tmp-deps.log:
 ifeq ($(IS_APP)$(IS_DEP),)
-	$(verbose) rm -f $(ERLANG_MK_TMP)/deps.log
+	$(verbose) rm -f $(ERLANG_MK_TMP)/apps.log $(ERLANG_MK_TMP)/deps.log
+endif
+
+# Erlang.mk does not rebuild dependencies after they were compiled
+# once. If a developer is working on the top-level project and some
+# dependencies at the same time, he may want to change this behavior.
+# There are two solutions:
+#     1. Set `FULL=1` so that all dependencies are visited and
+#        recursively recompiled if necessary.
+#     2. Set `FORCE_REBUILD=` to the specific list of dependencies that
+#        should be recompiled (instead of the whole set).
+
+FORCE_REBUILD ?=
+
+ifeq ($(origin FULL),undefined)
+ifneq ($(strip $(force_rebuild_dep)$(FORCE_REBUILD)),)
+define force_rebuild_dep
+echo "$(FORCE_REBUILD)" | grep -qw "$$(basename "$1")"
+endef
+endif
 endif
 
 ifneq ($(SKIP_DEPS),)
 deps::
 else
-deps:: $(ALL_DEPS_DIRS) apps clean-tmp-deps.log
-	$(verbose) mkdir -p $(ERLANG_MK_TMP)
-	$(verbose) set -e; for dep in $(ALL_DEPS_DIRS) ; do \
+deps:: $(ALL_DEPS_DIRS) apps clean-tmp-deps.log | $(ERLANG_MK_TMP)
+ifneq ($(ALL_DEPS_DIRS),)
+	$(verbose) set -e; for dep in $(ALL_DEPS_DIRS); do \
 		if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \
 			:; \
 		else \
 			echo $$dep >> $(ERLANG_MK_TMP)/deps.log; \
-			if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ]; then \
+			if [ -z "$(strip $(FULL))" ] $(if $(force_rebuild_dep),&& ! ($(call force_rebuild_dep,$$dep)),) && [ ! -L $$dep ] && [ -f $$dep/ebin/dep_built ]; then \
+				:; \
+			elif [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ]; then \
 				$(MAKE) -C $$dep IS_DEP=1; \
+				if [ ! -L $$dep ] && [ -d $$dep/ebin ]; then touch $$dep/ebin/dep_built; fi; \
 			else \
 				echo "Error: No Makefile to build dependency $$dep." >&2; \
 				exit 2; \
@@ -4350,6 +4526,7 @@ deps:: $(ALL_DEPS_DIRS) apps clean-tmp-deps.log
 		fi \
 	done
 endif
+endif
 
 # Deps related targets.
 
@@ -4422,13 +4599,23 @@ define dep_autopatch_gen
 		"include ../../erlang.mk" > $(DEPS_DIR)/$(1)/Makefile
 endef
 
+# We use flock/lockf when available to avoid concurrency issues.
 define dep_autopatch_fetch_rebar
-	mkdir -p $(ERLANG_MK_TMP); \
+	if command -v flock >/dev/null; then \
+		flock $(ERLANG_MK_TMP)/rebar.lock sh -c "$(call dep_autopatch_fetch_rebar2)"; \
+	elif command -v lockf >/dev/null; then \
+		lockf $(ERLANG_MK_TMP)/rebar.lock sh -c "$(call dep_autopatch_fetch_rebar2)"; \
+	else \
+		$(call dep_autopatch_fetch_rebar2); \
+	fi
+endef
+
+define dep_autopatch_fetch_rebar2
 	if [ ! -d $(ERLANG_MK_TMP)/rebar ]; then \
-		git clone -q -n -- https://github.com/rebar/rebar $(ERLANG_MK_TMP)/rebar; \
+		git clone -q -n -- $(REBAR_GIT) $(ERLANG_MK_TMP)/rebar; \
 		cd $(ERLANG_MK_TMP)/rebar; \
-		git checkout -q 576e12171ab8d69b048b827b92aa65d067deea01; \
-		$(MAKE); \
+		git checkout -q $(REBAR_COMMIT); \
+		./bootstrap; \
 		cd -; \
 	fi
 endef
@@ -4501,34 +4688,60 @@ define dep_autopatch_rebar.erl
 		end,
 		Write("\n")
 	end(),
-	GetHexVsn = fun(N) ->
+	GetHexVsn = fun(N, NP) ->
 		case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.lock)") of
 			{ok, Lock} ->
 				io:format("~p~n", [Lock]),
-				case lists:keyfind("1.1.0", 1, Lock) of
-					{_, LockPkgs} ->
+				LockPkgs = case lists:keyfind("1.2.0", 1, Lock) of
+					{_, LP} ->
+						LP;
+					_ ->
+						case lists:keyfind("1.1.0", 1, Lock) of
+							{_, LP} ->
+								LP;
+							_ ->
+								false
+						end
+				end,
+				if
+					is_list(LockPkgs) ->
 						io:format("~p~n", [LockPkgs]),
 						case lists:keyfind(atom_to_binary(N, latin1), 1, LockPkgs) of
 							{_, {pkg, _, Vsn}, _} ->
 								io:format("~p~n", [Vsn]),
-								{N, {hex, binary_to_list(Vsn)}};
+								{N, {hex, NP, binary_to_list(Vsn)}};
 							_ ->
 								false
 						end;
-					_ ->
+					true ->
 						false
 				end;
 			_ ->
 				false
 		end
 	end,
+	SemVsn = fun
+		("~>" ++ S0) ->
+			S = case S0 of
+				" " ++ S1 -> S1;
+				_ -> S0
+			end,
+			case length([ok || $$. <- S]) of
+				0 -> S ++ ".0.0";
+				1 -> S ++ ".0";
+				_ -> S
+			end;
+		(S) -> S
+	end,
 	fun() ->
 		File = case lists:keyfind(deps, 1, Conf) of
 			false -> [];
 			{_, Deps} ->
 				[begin case case Dep of
-							N when is_atom(N) -> GetHexVsn(N);
-							{N, S} when is_atom(N), is_list(S) -> {N, {hex, S}};
+							N when is_atom(N) -> GetHexVsn(N, N);
+							{N, S} when is_atom(N), is_list(S) -> {N, {hex, N, SemVsn(S)}};
+							{N, {pkg, NP}} when is_atom(N) -> GetHexVsn(N, NP);
+							{N, S, {pkg, NP}} -> {N, {hex, NP, S}};
 							{N, S} when is_tuple(S) -> {N, S};
 							{N, _, S} -> {N, S};
 							{N, _, S, _} -> {N, S};
@@ -4537,7 +4750,7 @@ define dep_autopatch_rebar.erl
 					false -> ok;
 					{Name, Source} ->
 						{Method, Repo, Commit} = case Source of
-							{hex, V} -> {hex, V, undefined};
+							{hex, NPV, V} -> {hex, V, NPV};
 							{git, R} -> {git, R, master};
 							{M, R, {branch, C}} -> {M, R, C};
 							{M, R, {ref, C}} -> {M, R, C};
@@ -4554,6 +4767,8 @@ define dep_autopatch_rebar.erl
 			{_, Files} ->
 				Names = [[" ", case lists:reverse(F) of
 					"lre." ++ Elif -> lists:reverse(Elif);
+					"lrx." ++ Elif -> lists:reverse(Elif);
+					"lry." ++ Elif -> lists:reverse(Elif);
 					Elif -> lists:reverse(Elif)
 				end] || "src/" ++ F <- Files],
 				Write(io_lib:format("COMPILE_FIRST +=~s\n", [Names]))
@@ -4591,9 +4806,10 @@ define dep_autopatch_rebar.erl
 				end || H <- Hooks]
 		end
 	end(),
-	ShellToMk = fun(V) ->
-		re:replace(re:replace(V, "(\\\\$$)(\\\\w*)", "\\\\1(\\\\2)", [global]),
-			"-Werror\\\\b", "", [{return, list}, global])
+	ShellToMk = fun(V0) ->
+		V1 = re:replace(V0, "[$$][(]", "$$\(shell ", [global]),
+		V = re:replace(V1, "([$$])(?![(])(\\\\w*)", "\\\\1(\\\\2)", [global]),
+		re:replace(V, "-Werror\\\\b", "", [{return, list}, global])
 	end,
 	PortSpecs = fun() ->
 		case lists:keyfind(port_specs, 1, Conf) of
@@ -4626,10 +4842,10 @@ define dep_autopatch_rebar.erl
 	case PortSpecs of
 		[] -> ok;
 		_ ->
-			Write("\npre-app::\n\t$$\(MAKE) -f c_src/Makefile.erlang.mk\n"),
+			Write("\npre-app::\n\t@$$\(MAKE) --no-print-directory -f c_src/Makefile.erlang.mk\n"),
 			PortSpecWrite(io_lib:format("ERL_CFLAGS ?= -finline-functions -Wall -fPIC -I \\"~s/erts-~s/include\\" -I \\"~s\\"\n",
 				[code:root_dir(), erlang:system_info(version), code:lib_dir(erl_interface, include)])),
-			PortSpecWrite(io_lib:format("ERL_LDFLAGS ?= -L \\"~s\\" -lerl_interface -lei\n",
+			PortSpecWrite(io_lib:format("ERL_LDFLAGS ?= -L \\"~s\\" -lei\n",
 				[code:lib_dir(erl_interface, lib)])),
 			[PortSpecWrite(["\n", E, "\n"]) || E <- OsEnv],
 			FilterEnv = fun(Env) ->
@@ -4663,7 +4879,7 @@ define dep_autopatch_rebar.erl
 						darwin -> "\n\nLDFLAGS += -flat_namespace -undefined suppress";
 						_ -> ""
 					end,
-					"\n\nall:: ", Output, "\n\n",
+					"\n\nall:: ", Output, "\n\t@:\n\n",
 					"%.o: %.c\n\t$$\(CC) -c -o $$\@ $$\< $$\(CFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n",
 					"%.o: %.C\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n",
 					"%.o: %.cc\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n",
@@ -4680,6 +4896,17 @@ define dep_autopatch_rebar.erl
 			end,
 			[PortSpec(S) || S <- PortSpecs]
 	end,
+	fun() ->
+		case lists:keyfind(plugins, 1, Conf) of
+			false -> ok;
+			{_, Plugins0} ->
+				Plugins = [P || P <- Plugins0, is_tuple(P)],
+				case lists:keyfind('lfe-compile', 1, Plugins) of
+					false -> ok;
+					_ -> Write("\nBUILD_DEPS = lfe lfe.mk\ndep_lfe.mk = git https://github.com/ninenines/lfe.mk master\nDEP_PLUGINS = lfe.mk\n")
+				end
+		end
+	end(),
 	Write("\ninclude $$\(if $$\(ERLANG_MK_FILENAME),$$\(ERLANG_MK_FILENAME),erlang.mk)"),
 	RunPlugin = fun(Plugin, Step) ->
 		case erlang:function_exported(Plugin, Step, 2) of
@@ -4694,7 +4921,8 @@ define dep_autopatch_rebar.erl
 	fun() ->
 		case lists:keyfind(plugins, 1, Conf) of
 			false -> ok;
-			{_, Plugins} ->
+			{_, Plugins0} ->
+				Plugins = [P || P <- Plugins0, is_atom(P)],
 				[begin
 					case lists:keyfind(deps, 1, Conf) of
 						false -> ok;
@@ -4752,7 +4980,7 @@ define dep_autopatch_appsrc.erl
 			{ok, [{application, $(1), L0}]} = file:consult(AppSrcIn),
 			L1 = lists:keystore(modules, 1, L0, {modules, []}),
 			L2 = case lists:keyfind(vsn, 1, L1) of
-				{_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, "git"});
+				{_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, lists:droplast(os:cmd("git -C $(DEPS_DIR)/$1 describe --dirty --tags --always"))});
 				{_, {cmd, _}} -> lists:keyreplace(vsn, 1, L1, {vsn, "cmd"});
 				_ -> L1
 			end,
@@ -4768,6 +4996,16 @@ define dep_fetch_git
 	cd $(DEPS_DIR)/$(call dep_name,$(1)) && git checkout -q $(call dep_commit,$(1));
 endef
 
+define dep_fetch_git-subfolder
+	mkdir -p $(ERLANG_MK_TMP)/git-subfolder; \
+	git clone -q -n -- $(call dep_repo,$1) \
+		$(ERLANG_MK_TMP)/git-subfolder/$(call dep_name,$1); \
+	cd $(ERLANG_MK_TMP)/git-subfolder/$(call dep_name,$1) \
+		&& git checkout -q $(call dep_commit,$1); \
+	ln -s $(ERLANG_MK_TMP)/git-subfolder/$(call dep_name,$1)/$(word 4,$(dep_$(1))) \
+		$(DEPS_DIR)/$(call dep_name,$1);
+endef
+
 define dep_fetch_git-submodule
 	git submodule update --init -- $(DEPS_DIR)/$1;
 endef
@@ -4793,7 +5031,7 @@ endef
 define dep_fetch_hex
 	mkdir -p $(ERLANG_MK_TMP)/hex $(DEPS_DIR)/$1; \
 	$(call core_http_get,$(ERLANG_MK_TMP)/hex/$1.tar,\
-		https://repo.hex.pm/tarballs/$1-$(strip $(word 2,$(dep_$1))).tar); \
+		https://repo.hex.pm/tarballs/$(if $(word 3,$(dep_$1)),$(word 3,$(dep_$1)),$1)-$(strip $(word 2,$(dep_$1))).tar); \
 	tar -xOf $(ERLANG_MK_TMP)/hex/$1.tar contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -;
 endef
 
@@ -4809,20 +5047,10 @@ define dep_fetch_legacy
 	cd $(DEPS_DIR)/$(1) && git checkout -q $(if $(word 2,$(dep_$(1))),$(word 2,$(dep_$(1))),master);
 endef
 
-define dep_fetch
-	$(if $(dep_$(1)), \
-		$(if $(dep_fetch_$(word 1,$(dep_$(1)))), \
-			$(word 1,$(dep_$(1))), \
-			$(if $(IS_DEP),legacy,fail)), \
-		$(if $(filter $(1),$(PACKAGES)), \
-			$(pkg_$(1)_fetch), \
-			fail))
-endef
-
 define dep_target
-$(DEPS_DIR)/$(call dep_name,$1):
+$(DEPS_DIR)/$(call dep_name,$1): | $(ERLANG_MK_TMP)
 	$(eval DEP_NAME := $(call dep_name,$1))
-	$(eval DEP_STR := $(if $(filter-out $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))"))
+	$(eval DEP_STR := $(if $(filter $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))"))
 	$(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \
 		echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)." >&2; \
 		exit 17; \
@@ -4831,7 +5059,7 @@ $(DEPS_DIR)/$(call dep_name,$1):
 	$(dep_verbose) $(call dep_fetch_$(strip $(call dep_fetch,$(1))),$(1))
 	$(verbose) if [ -f $(DEPS_DIR)/$(1)/configure.ac -o -f $(DEPS_DIR)/$(1)/configure.in ] \
 			&& [ ! -f $(DEPS_DIR)/$(1)/configure ]; then \
-		echo " AUTO  " $(1); \
+		echo " AUTO  " $(DEP_STR); \
 		cd $(DEPS_DIR)/$(1) && autoreconf -Wall -vif -I m4; \
 	fi
 	- $(verbose) if [ -f $(DEPS_DIR)/$(DEP_NAME)/configure ]; then \
@@ -4839,6 +5067,12 @@ $(DEPS_DIR)/$(call dep_name,$1):
 		cd $(DEPS_DIR)/$(DEP_NAME) && ./configure; \
 	fi
 ifeq ($(filter $(1),$(NO_AUTOPATCH)),)
+	$(verbose) $$(MAKE) --no-print-directory autopatch-$(DEP_NAME)
+endif
+
+.PHONY: autopatch-$(call dep_name,$1)
+
+autopatch-$(call dep_name,$1)::
 	$(verbose) if [ "$(1)" = "amqp_client" -a "$(RABBITMQ_CLIENT_PATCH)" ]; then \
 		if [ ! -d $(DEPS_DIR)/rabbitmq-codegen ]; then \
 			echo " PATCH  Downloading rabbitmq-codegen"; \
@@ -4854,10 +5088,11 @@ ifeq ($(filter $(1),$(NO_AUTOPATCH)),)
 			echo " PATCH  Downloading rabbitmq-codegen"; \
 			git clone https://github.com/rabbitmq/rabbitmq-codegen.git $(DEPS_DIR)/rabbitmq-codegen; \
 		fi \
+	elif [ "$1" = "elixir" -a "$(ELIXIR_PATCH)" ]; then \
+		ln -s lib/elixir/ebin $(DEPS_DIR)/elixir/; \
 	else \
-		$$(call dep_autopatch,$(DEP_NAME)) \
+		$$(call dep_autopatch,$(call dep_name,$1)) \
 	fi
-endif
 endef
 
 $(foreach dep,$(BUILD_DEPS) $(DEPS),$(eval $(call dep_target,$(dep))))
@@ -4894,36 +5129,11 @@ ERLANG_MK_RECURSIVE_REL_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-rel-deps-list.log
 ERLANG_MK_RECURSIVE_TEST_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-test-deps-list.log
 ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-shell-deps-list.log
 
-# Copyright (c) 2015-2016, Loïc Hoguin <essen@ninenines.eu>
-# This file is part of erlang.mk and subject to the terms of the ISC License.
-
-# Verbosity.
-
-proto_verbose_0 = @echo " PROTO " $(filter %.proto,$(?F));
-proto_verbose = $(proto_verbose_$(V))
-
-# Core targets.
-
-define compile_proto
-	$(verbose) mkdir -p ebin/ include/
-	$(proto_verbose) $(call erlang,$(call compile_proto.erl,$(1)))
-	$(proto_verbose) erlc +debug_info -o ebin/ ebin/*.erl
-	$(verbose) rm ebin/*.erl
-endef
-
-define compile_proto.erl
-	[begin
-		protobuffs_compile:generate_source(F,
-			[{output_include_dir, "./include"},
-				{output_src_dir, "./ebin"}])
-	end || F <- string:tokens("$(1)", " ")],
-	halt().
-endef
-
-ifneq ($(wildcard src/),)
-ebin/$(PROJECT).app:: $(sort $(call core_find,src/,*.proto))
-	$(if $(strip $?),$(call compile_proto,$?))
-endif
+ERLANG_MK_QUERY_DEPS_FILE = $(ERLANG_MK_TMP)/query-deps.log
+ERLANG_MK_QUERY_DOC_DEPS_FILE = $(ERLANG_MK_TMP)/query-doc-deps.log
+ERLANG_MK_QUERY_REL_DEPS_FILE = $(ERLANG_MK_TMP)/query-rel-deps.log
+ERLANG_MK_QUERY_TEST_DEPS_FILE = $(ERLANG_MK_TMP)/query-test-deps.log
+ERLANG_MK_QUERY_SHELL_DEPS_FILE = $(ERLANG_MK_TMP)/query-shell-deps.log
 
 # Copyright (c) 2013-2016, Loïc Hoguin <essen@ninenines.eu>
 # This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -4980,13 +5190,9 @@ ifneq ($(wildcard src/),)
 
 # Targets.
 
-ifeq ($(wildcard ebin/test),)
-app:: deps $(PROJECT).d
+app:: $(if $(wildcard ebin/test),clean) deps
+	$(verbose) $(MAKE) --no-print-directory $(PROJECT).d
 	$(verbose) $(MAKE) --no-print-directory app-build
-else
-app:: clean deps $(PROJECT).d
-	$(verbose) $(MAKE) --no-print-directory app-build
-endif
 
 ifeq ($(wildcard src/$(PROJECT_MOD).erl),)
 define app_file
@@ -5035,7 +5241,7 @@ define compile_asn1
 	$(verbose) mkdir -p include/
 	$(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(ERLC_ASN1_OPTS) $(1)
 	$(verbose) mv asn1/*.erl src/
-	$(verbose) mv asn1/*.hrl include/
+	-$(verbose) mv asn1/*.hrl include/
 	$(verbose) mv asn1/*.asn1db include/
 endef
 
@@ -5172,8 +5378,10 @@ define makedep.erl
 	end,
 	[begin
 		Mod = list_to_atom(filename:basename(F, ".erl")),
-		{ok, Fd} = file:open(F, [read]),
-		MakeDepend(MakeDepend, Fd, Mod,0)
+		case file:open(F, [read]) of
+			{ok, Fd} -> MakeDepend(MakeDepend, Fd, Mod,0);
+			{error, enoent} -> ok
+		end
 	end || F <- ErlFiles],
 	Depend = sofs:to_external(sofs:relation_to_family(sofs:relation(ets:tab2list(E)))),
 	CompileFirst = [X || X <- lists:reverse(digraph_utils:topsort(G)), [] =/= digraph:in_neighbours(G, X)],
@@ -5185,10 +5393,16 @@ define makedep.erl
 				string:join(DirSubname ++ [atom_to_list(Target)], "/")
 		end
 	end,
-	ok = file:write_file("$(1)", [
+	Output0 = [
+		"# Generated by Erlang.mk. Edit at your own risk!\n\n",
 		[[F, "::", [[" ", D] || D <- Deps], "; @touch \$$@\n"] || {F, Deps} <- Depend],
 		"\nCOMPILE_FIRST +=", [[" ", TargetPath(CF)] || CF <- CompileFirst], "\n"
-	]),
+	],
+	Output = case "é" of
+		[233] -> unicode:characters_to_binary(Output0);
+		_ -> Output0
+	end,
+	ok = file:write_file("$(1)", Output),
 	halt()
 endef
 
@@ -5197,10 +5411,10 @@ $(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl) $(MAKEFILE_LIST)
 	$(makedep_verbose) $(call erlang,$(call makedep.erl,$@))
 endif
 
+ifeq ($(IS_APP)$(IS_DEP),)
 ifneq ($(words $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES)),0)
 # Rebuild everything when the Makefile changes.
-$(ERLANG_MK_TMP)/last-makefile-change: $(MAKEFILE_LIST)
-	$(verbose) mkdir -p $(ERLANG_MK_TMP)
+$(ERLANG_MK_TMP)/last-makefile-change: $(MAKEFILE_LIST) | $(ERLANG_MK_TMP)
 	$(verbose) if test -f $@; then \
 		touch $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES); \
 		touch -c $(PROJECT).d; \
@@ -5210,6 +5424,10 @@ $(ERLANG_MK_TMP)/last-makefile-change: $(MAKEFILE_LIST)
 $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(ERLANG_MK_TMP)/last-makefile-change
 ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change
 endif
+endif
+
+$(PROJECT).d::
+	$(verbose) :
 
 include $(wildcard $(PROJECT).d)
 
@@ -5223,18 +5441,31 @@ define compile_erl
 		-pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),$(COMPILE_FIRST_PATHS) $(1))
 endef
 
+define validate_app_file
+	case file:consult("ebin/$(PROJECT).app") of
+		{ok, _} -> halt();
+		_ -> halt(1)
+	end
+endef
+
 ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.src)
 	$(eval FILES_TO_COMPILE := $(filter-out src/$(PROJECT).app.src,$?))
 	$(if $(strip $(FILES_TO_COMPILE)),$(call compile_erl,$(FILES_TO_COMPILE)))
-	$(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null || true))
+# Older git versions do not have the --first-parent flag. Do without in that case.
+	$(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null \
+		|| git describe --dirty --abbrev=7 --tags --always 2>/dev/null || true))
 	$(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \
 		$(filter-out $(ERLC_EXCLUDE_PATHS),$(ERL_FILES) $(CORE_FILES) $(BEAM_FILES)))))))
 ifeq ($(wildcard src/$(PROJECT).app.src),)
 	$(app_verbose) printf '$(subst %,%%,$(subst $(newline),\n,$(subst ','\'',$(call app_file,$(GITDESCRIBE),$(MODULES)))))' \
 		> ebin/$(PROJECT).app
+	$(verbose) if ! $(call erlang,$(call validate_app_file)); then \
+		echo "The .app file produced is invalid. Please verify the value of PROJECT_ENV." >&2; \
+		exit 1; \
+	fi
 else
 	$(verbose) if [ -z "$$(grep -e '^[^%]*{\s*modules\s*,' src/$(PROJECT).app.src)" ]; then \
-		echo "Empty modules entry not found in $(PROJECT).app.src. Please consult the erlang.mk README for instructions." >&2; \
+		echo "Empty modules entry not found in $(PROJECT).app.src. Please consult the erlang.mk documentation for instructions." >&2; \
 		exit 1; \
 	fi
 	$(appsrc_verbose) cat src/$(PROJECT).app.src \
@@ -5242,6 +5473,9 @@ else
 		| sed "s/{id,[[:space:]]*\"git\"}/{id, \"$(subst /,\/,$(GITDESCRIBE))\"}/" \
 		> ebin/$(PROJECT).app
 endif
+ifneq ($(wildcard src/$(PROJECT).appup),)
+	$(verbose) cp src/$(PROJECT).appup ebin/
+endif
 
 clean:: clean-app
 
@@ -5317,37 +5551,71 @@ ifneq ($(SKIP_DEPS),)
 test-deps:
 else
 test-deps: $(ALL_TEST_DEPS_DIRS)
-	$(verbose) set -e; for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done
+	$(verbose) set -e; for dep in $(ALL_TEST_DEPS_DIRS) ; do \
+		if [ -z "$(strip $(FULL))" ] && [ ! -L $$dep ] && [ -f $$dep/ebin/dep_built ]; then \
+			:; \
+		else \
+			$(MAKE) -C $$dep IS_DEP=1; \
+			if [ ! -L $$dep ] && [ -d $$dep/ebin ]; then touch $$dep/ebin/dep_built; fi; \
+		fi \
+	done
 endif
 
 ifneq ($(wildcard $(TEST_DIR)),)
-test-dir:
-	$(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -I include/ -o $(TEST_DIR) \
-		$(call core_find,$(TEST_DIR)/,*.erl) -pa ebin/
+test-dir: $(ERLANG_MK_TMP)/$(PROJECT).last-testdir-build
+	@:
+
+test_erlc_verbose_0 = @echo " ERLC  " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\
+	$(filter %.erl %.core,$(notdir $(FILES_TO_COMPILE))));
+test_erlc_verbose_2 = set -x;
+test_erlc_verbose = $(test_erlc_verbose_$(V))
+
+define compile_test_erl
+	$(test_erlc_verbose) erlc -v $(TEST_ERLC_OPTS) -o $(TEST_DIR) \
+		-pa ebin/ -I include/ $(1)
+endef
+
+ERL_TEST_FILES = $(call core_find,$(TEST_DIR)/,*.erl)
+$(ERLANG_MK_TMP)/$(PROJECT).last-testdir-build: $(ERL_TEST_FILES) $(MAKEFILE_LIST)
+	$(eval FILES_TO_COMPILE := $(if $(filter $(MAKEFILE_LIST),$?),$(filter $(ERL_TEST_FILES),$^),$?))
+	$(if $(strip $(FILES_TO_COMPILE)),$(call compile_test_erl,$(FILES_TO_COMPILE)) && touch $@)
 endif
 
-ifeq ($(wildcard src),)
+test-build:: IS_TEST=1
 test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS)
-test-build:: clean deps test-deps
+test-build:: $(if $(wildcard src),$(if $(wildcard ebin/test),,clean)) $(if $(IS_APP),,deps test-deps)
+# We already compiled everything when IS_APP=1.
+ifndef IS_APP
+ifneq ($(wildcard src),)
+	$(verbose) $(MAKE) --no-print-directory $(PROJECT).d ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
+	$(verbose) $(MAKE) --no-print-directory app-build ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
+	$(gen_verbose) touch ebin/test
+endif
+ifneq ($(wildcard $(TEST_DIR)),)
 	$(verbose) $(MAKE) --no-print-directory test-dir ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
-else
-ifeq ($(wildcard ebin/test),)
-test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS)
-test-build:: clean deps test-deps $(PROJECT).d
-	$(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
+endif
+endif
+
+# Roughly the same as test-build, but when IS_APP=1.
+# We only care about compiling the current application.
+ifdef IS_APP
+test-build-app:: ERLC_OPTS=$(TEST_ERLC_OPTS)
+test-build-app:: deps test-deps
+ifneq ($(wildcard src),)
+	$(verbose) $(MAKE) --no-print-directory $(PROJECT).d ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
+	$(verbose) $(MAKE) --no-print-directory app-build ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
 	$(gen_verbose) touch ebin/test
-else
-test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS)
-test-build:: deps test-deps $(PROJECT).d
-	$(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
+endif
+ifneq ($(wildcard $(TEST_DIR)),)
+	$(verbose) $(MAKE) --no-print-directory test-dir ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
+endif
 endif
 
 clean:: clean-test-dir
 
 clean-test-dir:
 ifneq ($(wildcard $(TEST_DIR)/*.beam),)
-	$(gen_verbose) rm -f $(TEST_DIR)/*.beam
-endif
+	$(gen_verbose) rm -f $(TEST_DIR)/*.beam $(ERLANG_MK_TMP)/$(PROJECT).last-testdir-build
 endif
 
 # Copyright (c) 2015-2016, Loïc Hoguin <essen@ninenines.eu>
@@ -5380,11 +5648,8 @@ $(call comma_list,$(foreach d,$(DEPS),\
 {erl_opts, $(call compat_erlc_opts_to_list,$(ERLC_OPTS))}.
 endef
 
-$(eval _compat_rebar_config = $$(compat_rebar_config))
-$(eval export _compat_rebar_config)
-
 rebar.config:
-	$(gen_verbose) echo "$${_compat_rebar_config}" > rebar.config
+	$(gen_verbose) $(call core_render,compat_rebar_config,rebar.config)
 
 # Copyright (c) 2015-2016, Loïc Hoguin <essen@ninenines.eu>
 # This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -5454,8 +5719,8 @@ endef
 asciidoc-manual:: doc-deps
 
 asciidoc-manual:: $(ASCIIDOC_MANUAL_FILES)
-	$(call erlang,$(call asciidoc2man.erl,$?))
-	$(foreach s,$(MAN_SECTIONS),mkdir -p doc/man$s/ && mv doc/src/manual/*.$s.gz doc/man$s/;)
+	$(gen_verbose) $(call erlang,$(call asciidoc2man.erl,$?))
+	$(verbose) $(foreach s,$(MAN_SECTIONS),mkdir -p doc/man$s/ && mv doc/src/manual/*.$s.gz doc/man$s/;)
 
 install-docs:: install-asciidoc
 
@@ -5522,31 +5787,30 @@ endef
 
 # To prevent autocompletion issues with ZSH, we add "include erlang.mk"
 # separately during the actual bootstrap.
-ifdef SP
 define bs_Makefile
 PROJECT = $p
 PROJECT_DESCRIPTION = New project
 PROJECT_VERSION = 0.1.0
-
+$(if $(SP),
 # Whitespace to be used when creating files from templates.
 SP = $(SP)
-
-endef
-else
-define bs_Makefile
-PROJECT = $p
-PROJECT_DESCRIPTION = New project
-PROJECT_VERSION = 0.1.0
-
+)
 endef
-endif
 
 define bs_apps_Makefile
 PROJECT = $p
 PROJECT_DESCRIPTION = New project
 PROJECT_VERSION = 0.1.0
+$(if $(SP),
+# Whitespace to be used when creating files from templates.
+SP = $(SP)
+)
+# Make sure we know where the applications are located.
+ROOT_DIR ?= $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(APPS_DIR)/app)
+APPS_DIR ?= ..
+DEPS_DIR ?= $(call core_relpath,$(DEPS_DIR),$(APPS_DIR)/app)
 
-include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(APPS_DIR)/app)/erlang.mk
+include $$(ROOT_DIR)/erlang.mk
 endef
 
 define bs_app
@@ -5566,8 +5830,8 @@ endef
 define bs_relx_config
 {release, {$p_release, "1"}, [$p, sasl, runtime_tools]}.
 {extended_start_script, true}.
-{sys_config, "rel/sys.config"}.
-{vm_args, "rel/vm.args"}.
+{sys_config, "config/sys.config"}.
+{vm_args, "config/vm.args"}.
 endef
 
 define bs_sys_config
@@ -5870,10 +6134,6 @@ endef
 
 # Plugin-specific targets.
 
-define render_template
-	$(verbose) printf -- '$(subst $(newline),\n,$(subst %,%%,$(subst ','\'',$(subst $(tab),$(WS),$(call $(1))))))\n' > $(2)
-endef
-
 ifndef WS
 ifdef SP
 WS = $(subst a,,a $(wordlist 1,$(SP),a a a a a a a a a a a a a a a a a a a a))
@@ -5887,40 +6147,44 @@ ifneq ($(wildcard src/),)
 	$(error Error: src/ directory already exists)
 endif
 	$(eval p := $(PROJECT))
+	$(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\
+		$(error Error: Invalid characters in the application name))
 	$(eval n := $(PROJECT)_sup)
-	$(call render_template,bs_Makefile,Makefile)
+	$(verbose) $(call core_render,bs_Makefile,Makefile)
 	$(verbose) echo "include erlang.mk" >> Makefile
 	$(verbose) mkdir src/
 ifdef LEGACY
-	$(call render_template,bs_appsrc,src/$(PROJECT).app.src)
+	$(verbose) $(call core_render,bs_appsrc,src/$(PROJECT).app.src)
 endif
-	$(call render_template,bs_app,src/$(PROJECT)_app.erl)
-	$(call render_template,tpl_supervisor,src/$(PROJECT)_sup.erl)
+	$(verbose) $(call core_render,bs_app,src/$(PROJECT)_app.erl)
+	$(verbose) $(call core_render,tpl_supervisor,src/$(PROJECT)_sup.erl)
 
 bootstrap-lib:
 ifneq ($(wildcard src/),)
 	$(error Error: src/ directory already exists)
 endif
 	$(eval p := $(PROJECT))
-	$(call render_template,bs_Makefile,Makefile)
+	$(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\
+		$(error Error: Invalid characters in the application name))
+	$(verbose) $(call core_render,bs_Makefile,Makefile)
 	$(verbose) echo "include erlang.mk" >> Makefile
 	$(verbose) mkdir src/
 ifdef LEGACY
-	$(call render_template,bs_appsrc_lib,src/$(PROJECT).app.src)
+	$(verbose) $(call core_render,bs_appsrc_lib,src/$(PROJECT).app.src)
 endif
 
 bootstrap-rel:
 ifneq ($(wildcard relx.config),)
 	$(error Error: relx.config already exists)
 endif
-ifneq ($(wildcard rel/),)
-	$(error Error: rel/ directory already exists)
+ifneq ($(wildcard config/),)
+	$(error Error: config/ directory already exists)
 endif
 	$(eval p := $(PROJECT))
-	$(call render_template,bs_relx_config,relx.config)
-	$(verbose) mkdir rel/
-	$(call render_template,bs_sys_config,rel/sys.config)
-	$(call render_template,bs_vm_args,rel/vm.args)
+	$(verbose) $(call core_render,bs_relx_config,relx.config)
+	$(verbose) mkdir config/
+	$(verbose) $(call core_render,bs_sys_config,config/sys.config)
+	$(verbose) $(call core_render,bs_vm_args,config/vm.args)
 
 new-app:
 ifndef in
@@ -5930,14 +6194,16 @@ ifneq ($(wildcard $(APPS_DIR)/$in),)
 	$(error Error: Application $in already exists)
 endif
 	$(eval p := $(in))
+	$(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\
+		$(error Error: Invalid characters in the application name))
 	$(eval n := $(in)_sup)
 	$(verbose) mkdir -p $(APPS_DIR)/$p/src/
-	$(call render_template,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile)
+	$(verbose) $(call core_render,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile)
 ifdef LEGACY
-	$(call render_template,bs_appsrc,$(APPS_DIR)/$p/src/$p.app.src)
+	$(verbose) $(call core_render,bs_appsrc,$(APPS_DIR)/$p/src/$p.app.src)
 endif
-	$(call render_template,bs_app,$(APPS_DIR)/$p/src/$p_app.erl)
-	$(call render_template,tpl_supervisor,$(APPS_DIR)/$p/src/$p_sup.erl)
+	$(verbose) $(call core_render,bs_app,$(APPS_DIR)/$p/src/$p_app.erl)
+	$(verbose) $(call core_render,tpl_supervisor,$(APPS_DIR)/$p/src/$p_sup.erl)
 
 new-lib:
 ifndef in
@@ -5947,10 +6213,12 @@ ifneq ($(wildcard $(APPS_DIR)/$in),)
 	$(error Error: Application $in already exists)
 endif
 	$(eval p := $(in))
+	$(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\
+		$(error Error: Invalid characters in the application name))
 	$(verbose) mkdir -p $(APPS_DIR)/$p/src/
-	$(call render_template,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile)
+	$(verbose) $(call core_render,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile)
 ifdef LEGACY
-	$(call render_template,bs_appsrc_lib,$(APPS_DIR)/$p/src/$p.app.src)
+	$(verbose) $(call core_render,bs_appsrc_lib,$(APPS_DIR)/$p/src/$p.app.src)
 endif
 
 new:
@@ -5964,9 +6232,9 @@ ifndef n
 	$(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP])
 endif
 ifdef in
-	$(call render_template,tpl_$(t),$(APPS_DIR)/$(in)/src/$(n).erl)
+	$(verbose) $(call core_render,tpl_$(t),$(APPS_DIR)/$(in)/src/$(n).erl)
 else
-	$(call render_template,tpl_$(t),src/$(n).erl)
+	$(verbose) $(call core_render,tpl_$(t),src/$(n).erl)
 endif
 
 list-templates:
@@ -6010,8 +6278,8 @@ ifeq ($(PLATFORM),msys2)
 	CXXFLAGS ?= -O3 -finline-functions -Wall
 else ifeq ($(PLATFORM),darwin)
 	CC ?= cc
-	CFLAGS ?= -O3 -std=c99 -arch x86_64 -finline-functions -Wall -Wmissing-prototypes
-	CXXFLAGS ?= -O3 -arch x86_64 -finline-functions -Wall
+	CFLAGS ?= -O3 -std=c99 -arch x86_64 -Wall -Wmissing-prototypes
+	CXXFLAGS ?= -O3 -arch x86_64 -Wall
 	LDFLAGS ?= -arch x86_64 -flat_namespace -undefined suppress
 else ifeq ($(PLATFORM),freebsd)
 	CC ?= cc
@@ -6031,14 +6299,14 @@ endif
 CFLAGS += -I"$(ERTS_INCLUDE_DIR)" -I"$(ERL_INTERFACE_INCLUDE_DIR)"
 CXXFLAGS += -I"$(ERTS_INCLUDE_DIR)" -I"$(ERL_INTERFACE_INCLUDE_DIR)"
 
-LDLIBS += -L"$(ERL_INTERFACE_LIB_DIR)" -lerl_interface -lei
+LDLIBS += -L"$(ERL_INTERFACE_LIB_DIR)" -lei
 
 # Verbosity.
 
-c_verbose_0 = @echo " C     " $(?F);
+c_verbose_0 = @echo " C     " $(filter-out $(notdir $(MAKEFILE_LIST) $(C_SRC_ENV)),$(^F));
 c_verbose = $(c_verbose_$(V))
 
-cpp_verbose_0 = @echo " CPP   " $(?F);
+cpp_verbose_0 = @echo " CPP   " $(filter-out $(notdir $(MAKEFILE_LIST) $(C_SRC_ENV)),$(^F));
 cpp_verbose = $(cpp_verbose_$(V))
 
 link_verbose_0 = @echo " LD    " $(@F);
@@ -6073,11 +6341,13 @@ app:: $(C_SRC_ENV) $(C_SRC_OUTPUT_FILE)
 test-build:: $(C_SRC_ENV) $(C_SRC_OUTPUT_FILE)
 
 $(C_SRC_OUTPUT_FILE): $(OBJECTS)
-	$(verbose) mkdir -p priv/
+	$(verbose) mkdir -p $(dir $@)
 	$(link_verbose) $(CC) $(OBJECTS) \
 		$(LDFLAGS) $(if $(filter $(C_SRC_TYPE),shared),-shared) $(LDLIBS) \
 		-o $(C_SRC_OUTPUT_FILE)
 
+$(OBJECTS): $(MAKEFILE_LIST) $(C_SRC_ENV)
+
 %.o: %.c
 	$(COMPILE_C) $(OUTPUT_OPTION) $<
 
@@ -6098,12 +6368,16 @@ clean-c_src:
 endif
 
 ifneq ($(wildcard $(C_SRC_DIR)),)
+ERL_ERTS_DIR = $(shell $(ERL) -eval 'io:format("~s~n", [code:lib_dir(erts)]), halt().')
+
 $(C_SRC_ENV):
 	$(verbose) $(ERL) -eval "file:write_file(\"$(call core_native_path,$(C_SRC_ENV))\", \
 		io_lib:format( \
+			\"# Generated by Erlang.mk. Edit at your own risk!~n~n\" \
 			\"ERTS_INCLUDE_DIR ?= ~s/erts-~s/include/~n\" \
 			\"ERL_INTERFACE_INCLUDE_DIR ?= ~s~n\" \
-			\"ERL_INTERFACE_LIB_DIR ?= ~s~n\", \
+			\"ERL_INTERFACE_LIB_DIR ?= ~s~n\" \
+			\"ERTS_DIR ?= $(ERL_ERTS_DIR)~n\", \
 			[code:root_dir(), erlang:system_info(version), \
 			code:lib_dir(erl_interface, include), \
 			code:lib_dir(erl_interface, lib)])), \
@@ -6115,6 +6389,10 @@ distclean-c_src-env:
 	$(gen_verbose) rm -f $(C_SRC_ENV)
 
 -include $(C_SRC_ENV)
+
+ifneq ($(ERL_ERTS_DIR),$(ERTS_DIR))
+$(shell rm -f $(C_SRC_ENV))
+endif
 endif
 
 # Templates.
@@ -6200,12 +6478,15 @@ endif
 ifneq ($(wildcard src/$n.erl),)
 	$(error Error: src/$n.erl already exists)
 endif
+ifndef n
+	$(error Usage: $(MAKE) new-nif n=NAME [in=APP])
+endif
 ifdef in
 	$(verbose) $(MAKE) -C $(APPS_DIR)/$(in)/ new-nif n=$n in=
 else
 	$(verbose) mkdir -p $(C_SRC_DIR) src/
-	$(call render_template,bs_c_nif,$(C_SRC_DIR)/$n.c)
-	$(call render_template,bs_erl_nif,src/$n.erl)
+	$(verbose) $(call core_render,bs_c_nif,$(C_SRC_DIR)/$n.c)
+	$(verbose) $(call core_render,bs_erl_nif,src/$n.erl)
 endif
 
 # Copyright (c) 2015-2017, Loïc Hoguin <essen@ninenines.eu>
@@ -6234,15 +6515,17 @@ ci:: $(addprefix ci-,$(CI_OTP) $(addsuffix -native,$(CI_HIPE)) $(addsuffix -erll
 ci-prepare: $(addprefix $(KERL_INSTALL_DIR)/,$(CI_OTP) $(addsuffix -native,$(CI_HIPE)))
 
 ci-setup::
+	$(verbose) :
 
 ci-extra::
+	$(verbose) :
 
 ci_verbose_0 = @echo " CI    " $(1);
 ci_verbose = $(ci_verbose_$(V))
 
 define ci_target
 ci-$1: $(KERL_INSTALL_DIR)/$2
-	$(verbose) $(MAKE) --no-print-directory clean distclean-c_src-env
+	$(verbose) $(MAKE) --no-print-directory clean
 	$(ci_verbose) \
 		PATH="$(KERL_INSTALL_DIR)/$2/bin:$(PATH)" \
 		CI_OTP_RELEASE="$1" \
@@ -6256,8 +6539,8 @@ $(foreach otp,$(CI_OTP),$(eval $(call ci_target,$(otp),$(otp),otp)))
 $(foreach otp,$(CI_HIPE),$(eval $(call ci_target,$(otp)-native,$(otp)-native,native)))
 $(foreach otp,$(CI_ERLLVM),$(eval $(call ci_target,$(otp)-erllvm,$(otp)-native,erllvm)))
 
-$(foreach otp,$(CI_OTP),$(eval $(call kerl_otp_target,$(otp))))
-$(foreach otp,$(sort $(CI_HIPE) $(CI_ERLLLVM)),$(eval $(call kerl_hipe_target,$(otp))))
+$(foreach otp,$(filter-out $(ERLANG_OTP),$(CI_OTP)),$(eval $(call kerl_otp_target,$(otp))))
+$(foreach otp,$(filter-out $(ERLANG_HIPE),$(sort $(CI_HIPE) $(CI_ERLLLVM))),$(eval $(call kerl_hipe_target,$(otp))))
 
 help::
 	$(verbose) printf "%s\n" "" \
@@ -6269,6 +6552,74 @@ help::
 
 endif
 
+# Copyright (c) 2020, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+ifdef CONCUERROR_TESTS
+
+.PHONY: concuerror distclean-concuerror
+
+# Configuration
+
+CONCUERROR_LOGS_DIR ?= $(CURDIR)/logs
+CONCUERROR_OPTS ?=
+
+# Core targets.
+
+check:: concuerror
+
+ifndef KEEP_LOGS
+distclean:: distclean-concuerror
+endif
+
+# Plugin-specific targets.
+
+$(ERLANG_MK_TMP)/Concuerror/bin/concuerror: | $(ERLANG_MK_TMP)
+	$(verbose) git clone https://github.com/parapluu/Concuerror $(ERLANG_MK_TMP)/Concuerror
+	$(verbose) $(MAKE) -C $(ERLANG_MK_TMP)/Concuerror
+
+$(CONCUERROR_LOGS_DIR):
+	$(verbose) mkdir -p $(CONCUERROR_LOGS_DIR)
+
+define concuerror_html_report
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Concuerror HTML report</title>
+</head>
+<body>
+<h1>Concuerror HTML report</h1>
+<p>Generated on $(concuerror_date)</p>
+<ul>
+$(foreach t,$(concuerror_targets),<li><a href="$(t).txt">$(t)</a></li>)
+</ul>
+</body>
+</html>
+endef
+
+concuerror: $(addprefix concuerror-,$(subst :,-,$(CONCUERROR_TESTS)))
+	$(eval concuerror_date := $(shell date))
+	$(eval concuerror_targets := $^)
+	$(verbose) $(call core_render,concuerror_html_report,$(CONCUERROR_LOGS_DIR)/concuerror.html)
+
+define concuerror_target
+.PHONY: concuerror-$1-$2
+
+concuerror-$1-$2: test-build | $(ERLANG_MK_TMP)/Concuerror/bin/concuerror $(CONCUERROR_LOGS_DIR)
+	$(ERLANG_MK_TMP)/Concuerror/bin/concuerror \
+		--pa $(CURDIR)/ebin --pa $(TEST_DIR) \
+		-o $(CONCUERROR_LOGS_DIR)/concuerror-$1-$2.txt \
+		$$(CONCUERROR_OPTS) -m $1 -t $2
+endef
+
+$(foreach test,$(CONCUERROR_TESTS),$(eval $(call concuerror_target,$(firstword $(subst :, ,$(test))),$(lastword $(subst :, ,$(test))))))
+
+distclean-concuerror:
+	$(gen_verbose) rm -rf $(CONCUERROR_LOGS_DIR)
+
+endif
+
 # Copyright (c) 2013-2016, Loïc Hoguin <essen@ninenines.eu>
 # This file is part of erlang.mk and subject to the terms of the ISC License.
 
@@ -6290,7 +6641,9 @@ CT_LOGS_DIR ?= $(CURDIR)/logs
 
 tests:: ct
 
+ifndef KEEP_LOGS
 distclean:: distclean-ct
+endif
 
 help::
 	$(verbose) printf "%s\n" "" \
@@ -6305,16 +6658,16 @@ help::
 CT_RUN = ct_run \
 	-no_auto_compile \
 	-noinput \
-	-pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \
+	-pa $(CURDIR)/ebin $(TEST_DIR) \
 	-dir $(TEST_DIR) \
 	-logdir $(CT_LOGS_DIR)
 
 ifeq ($(CT_SUITES),)
-ct: $(if $(IS_APP),,apps-ct)
+ct: $(if $(IS_APP)$(ROOT_DIR),,apps-ct)
 else
 # We do not run tests if we are in an apps/* with no test directory.
 ifneq ($(IS_APP)$(wildcard $(TEST_DIR)),1)
-ct: test-build $(if $(IS_APP),,apps-ct)
+ct: test-build $(if $(IS_APP)$(ROOT_DIR),,apps-ct)
 	$(verbose) mkdir -p $(CT_LOGS_DIR)
 	$(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS)
 endif
@@ -6322,30 +6675,34 @@ endif
 
 ifneq ($(ALL_APPS_DIRS),)
 define ct_app_target
-apps-ct-$1:
-	$(MAKE) -C $1 ct IS_APP=1
+apps-ct-$1: test-build
+	$$(MAKE) -C $1 ct IS_APP=1
 endef
 
 $(foreach app,$(ALL_APPS_DIRS),$(eval $(call ct_app_target,$(app))))
 
-apps-ct: test-build $(addprefix apps-ct-,$(ALL_APPS_DIRS))
+apps-ct: $(addprefix apps-ct-,$(ALL_APPS_DIRS))
 endif
 
-ifndef t
-CT_EXTRA =
-else
+ifdef t
 ifeq (,$(findstring :,$t))
 CT_EXTRA = -group $t
 else
 t_words = $(subst :, ,$t)
 CT_EXTRA = -group $(firstword $(t_words)) -case $(lastword $(t_words))
 endif
+else
+ifdef c
+CT_EXTRA = -case $c
+else
+CT_EXTRA =
+endif
 endif
 
 define ct_suite_target
 ct-$(1): test-build
 	$(verbose) mkdir -p $(CT_LOGS_DIR)
-	$(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(1)) $(CT_EXTRA) $(CT_OPTS)
+	$(gen_verbose_esc) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(1)) $(CT_EXTRA) $(CT_OPTS)
 endef
 
 $(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test))))
@@ -6397,11 +6754,17 @@ define filter_opts.erl
 	halt().
 endef
 
+# DIALYZER_PLT is a variable understood directly by Dialyzer.
+#
+# We append the path to erts at the end of the PLT. This works
+# because the PLT file is in the external term format and the
+# function binary_to_term/1 ignores any trailing data.
 $(DIALYZER_PLT): deps app
 	$(eval DEPS_LOG := $(shell test -f $(ERLANG_MK_TMP)/deps.log && \
 		while read p; do test -d $$p/ebin && echo $$p/ebin; done <$(ERLANG_MK_TMP)/deps.log))
 	$(verbose) dialyzer --build_plt $(DIALYZER_PLT_OPTS) --apps \
 		erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS_LOG) || test $$? -eq 2
+	$(verbose) $(ERL) -eval 'io:format("~n~s~n", [code:lib_dir(erts)]), halt().' >> $@
 
 plt: $(DIALYZER_PLT)
 
@@ -6409,11 +6772,18 @@ distclean-plt:
 	$(gen_verbose) rm -f $(DIALYZER_PLT)
 
 ifneq ($(wildcard $(DIALYZER_PLT)),)
-dialyze:
+dialyze: $(if $(filter --src,$(DIALYZER_DIRS)),,deps app)
+	$(verbose) if ! tail -n1 $(DIALYZER_PLT) | \
+		grep -q "^`$(ERL) -eval 'io:format("~s", [code:lib_dir(erts)]), halt().'`$$"; then \
+		rm $(DIALYZER_PLT); \
+		$(MAKE) plt; \
+	fi
 else
 dialyze: $(DIALYZER_PLT)
 endif
-	$(verbose) dialyzer --no_native `$(ERL) -eval "$(subst $(newline),,$(call escape_dquotes,$(call filter_opts.erl)))" -extra $(ERLC_OPTS)` $(DIALYZER_DIRS) $(DIALYZER_OPTS)
+	$(verbose) dialyzer --no_native `$(ERL) \
+		-eval "$(subst $(newline),,$(call escape_dquotes,$(call filter_opts.erl)))" \
+		-extra $(ERLC_OPTS)` $(DIALYZER_DIRS) $(DIALYZER_OPTS) $(if $(wildcard ebin/),-pa ebin/)
 
 # Copyright (c) 2013-2016, Loïc Hoguin <essen@ninenines.eu>
 # This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -6429,7 +6799,7 @@ EDOC_OUTPUT ?= doc
 define edoc.erl
 	SrcPaths = lists:foldl(fun(P, Acc) ->
 		filelib:wildcard(atom_to_list(P) ++ "/{src,c_src}") ++ Acc
-	end, [], [$(call comma_list,$(patsubst %,'%',$(EDOC_SRC_DIRS)))]),
+	end, [], [$(call comma_list,$(patsubst %,'%',$(call core_native_path,$(EDOC_SRC_DIRS))))]),
 	DefaultOpts = [{dir, "$(EDOC_OUTPUT)"}, {source_path, SrcPaths}, {subpackages, false}],
 	edoc:application($(1), ".", [$(2)] ++ DefaultOpts),
 	halt(0).
@@ -6458,6 +6828,7 @@ distclean-edoc:
 
 DTL_FULL_PATH ?=
 DTL_PATH ?= templates/
+DTL_PREFIX ?=
 DTL_SUFFIX ?= _dtl
 DTL_OPTS ?=
 
@@ -6473,18 +6844,17 @@ DTL_FILES := $(sort $(call core_find,$(DTL_PATH),*.dtl))
 
 ifneq ($(DTL_FILES),)
 
-DTL_NAMES   = $(addsuffix $(DTL_SUFFIX),$(DTL_FILES:$(DTL_PATH)/%.dtl=%))
+DTL_NAMES   = $(addprefix $(DTL_PREFIX),$(addsuffix $(DTL_SUFFIX),$(DTL_FILES:$(DTL_PATH)/%.dtl=%)))
 DTL_MODULES = $(if $(DTL_FULL_PATH),$(subst /,_,$(DTL_NAMES)),$(notdir $(DTL_NAMES)))
 BEAM_FILES += $(addsuffix .beam,$(addprefix ebin/,$(DTL_MODULES)))
 
 ifneq ($(words $(DTL_FILES)),0)
 # Rebuild templates when the Makefile changes.
-$(ERLANG_MK_TMP)/last-makefile-change-erlydtl: $(MAKEFILE_LIST)
-	@mkdir -p $(ERLANG_MK_TMP)
-	@if test -f $@; then \
+$(ERLANG_MK_TMP)/last-makefile-change-erlydtl: $(MAKEFILE_LIST) | $(ERLANG_MK_TMP)
+	$(verbose) if test -f $@; then \
 		touch $(DTL_FILES); \
 	fi
-	@touch $@
+	$(verbose) touch $@
 
 ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change-erlydtl
 endif
@@ -6495,10 +6865,10 @@ define erlydtl_compile.erl
 			"" ->
 				filename:basename(F, ".dtl");
 			_ ->
-				"$(DTL_PATH)/" ++ F2 = filename:rootname(F, ".dtl"),
+				"$(call core_native_path,$(DTL_PATH))/" ++ F2 = filename:rootname(F, ".dtl"),
 				re:replace(F2, "/",  "_",  [{return, list}, global])
 		end,
-		Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"),
+		Module = list_to_atom("$(DTL_PREFIX)" ++ string:to_lower(Module0) ++ "$(DTL_SUFFIX)"),
 		case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors]) of
 			ok -> ok;
 			{ok, _} -> ok
@@ -6510,7 +6880,7 @@ endef
 ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/
 	$(if $(strip $?),\
 		$(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$(call core_native_path,$?)),\
-			-pa ebin/ $(DEPS_DIR)/erlydtl/ebin/))
+			-pa ebin/))
 
 endif
 
@@ -6543,13 +6913,15 @@ help::
 
 # Plugin-specific targets.
 
+escript-zip:: FULL=1
 escript-zip:: deps app
 	$(verbose) mkdir -p $(dir $(ESCRIPT_ZIP))
 	$(verbose) rm -f $(ESCRIPT_ZIP_FILE)
 	$(gen_verbose) cd .. && $(ESCRIPT_ZIP) $(ESCRIPT_ZIP_FILE) $(PROJECT)/ebin/*
 ifneq ($(DEPS),)
 	$(verbose) cd $(DEPS_DIR) && $(ESCRIPT_ZIP) $(ESCRIPT_ZIP_FILE) \
-		`cat $(ERLANG_MK_TMP)/deps.log | sed 's/^$(subst /,\/,$(DEPS_DIR))\///' | sed 's/$$/\/ebin\/\*/'`
+		$(subst $(DEPS_DIR)/,,$(addsuffix /*,$(wildcard \
+			$(addsuffix /ebin,$(shell cat $(ERLANG_MK_TMP)/deps.log)))))
 endif
 
 escript:: escript-zip
@@ -6586,32 +6958,17 @@ help::
 # Plugin-specific targets.
 
 define eunit.erl
-	Enabled = case "$(COVER)" of
-		"" -> false;
-		_ ->
-			case filelib:is_dir("ebin") of
-				false -> false;
-				true ->
-					case cover:compile_beam_directory("ebin") of
-						{error, _} -> halt(1);
-						_ -> true
-					end
-			end
-	end,
+	$(call cover.erl)
+	CoverSetup(),
 	case eunit:test($1, [$(EUNIT_OPTS)]) of
 		ok -> ok;
 		error -> halt(2)
 	end,
-	case {Enabled, "$(COVER)"} of
-		{false, _} -> ok;
-		{_, ""} -> ok;
-		_ ->
-			cover:export("$(COVER_DATA_DIR)/eunit.coverdata")
-	end,
+	CoverExport("$(call core_native_path,$(COVER_DATA_DIR))/eunit.coverdata"),
 	halt()
 endef
 
-EUNIT_ERL_OPTS += -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(CURDIR)/ebin
+EUNIT_ERL_OPTS += -pa $(TEST_DIR) $(CURDIR)/ebin
 
 ifdef t
 ifeq (,$(findstring :,$(t)))
@@ -6628,17 +6985,356 @@ EUNIT_TEST_MODS = $(notdir $(basename $(call core_find,$(TEST_DIR)/,*.erl)))
 EUNIT_MODS = $(foreach mod,$(EUNIT_EBIN_MODS) $(filter-out \
 	$(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(EUNIT_TEST_MODS)),'$(mod)')
 
-eunit: test-build $(if $(IS_APP),,apps-eunit) cover-data-dir
+eunit: test-build $(if $(IS_APP)$(ROOT_DIR),,apps-eunit) cover-data-dir
+ifneq ($(wildcard src/ $(TEST_DIR)),)
 	$(gen_verbose) $(call erlang,$(call eunit.erl,[$(call comma_list,$(EUNIT_MODS))]),$(EUNIT_ERL_OPTS))
+endif
 
 ifneq ($(ALL_APPS_DIRS),)
-apps-eunit:
+apps-eunit: test-build
 	$(verbose) eunit_retcode=0 ; for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; \
 		[ $$? -ne 0 ] && eunit_retcode=1 ; done ; \
 		exit $$eunit_retcode
 endif
 endif
 
+# Copyright (c) 2020, Loïc Hoguin <essen@ninenines.eu>
+# 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))))
+
 # Copyright (c) 2015-2017, Loïc Hoguin <essen@ninenines.eu>
 # This file is part of erlang.mk and subject to the terms of the ISC License.
 
@@ -6650,6 +7346,7 @@ ifeq ($(filter proper,$(DEPS) $(TEST_DEPS)),proper)
 tests:: proper
 
 define proper_check.erl
+	$(call cover.erl)
 	code:add_pathsa([
 		"$(call core_native_path,$(CURDIR)/ebin)",
 		"$(call core_native_path,$(DEPS_DIR)/*/ebin)",
@@ -6665,13 +7362,16 @@ define proper_check.erl
 			end
 		|| {F, 0} <- M:module_info(exports)])
 	end,
-	try
-		case $(1) of
+	try begin
+		CoverSetup(),
+		Res = case $(1) of
 			all -> [true] =:= lists:usort([Module(M) || M <- [$(call comma_list,$(3))]]);
 			module -> Module($(2));
 			function -> proper:quickcheck($(2), nocolors)
-		end
-	of
+		end,
+		CoverExport("$(COVER_DATA_DIR)/proper.coverdata"),
+		Res
+	end of
 		true -> halt(0);
 		_ -> halt(1)
 	catch error:undef ->
@@ -6682,21 +7382,81 @@ endef
 
 ifdef t
 ifeq (,$(findstring :,$(t)))
-proper: test-build
+proper: test-build cover-data-dir
 	$(verbose) $(call erlang,$(call proper_check.erl,module,$(t)))
 else
-proper: test-build
+proper: test-build cover-data-dir
 	$(verbose) echo Testing $(t)/0
 	$(verbose) $(call erlang,$(call proper_check.erl,function,$(t)()))
 endif
 else
-proper: test-build
+proper: test-build cover-data-dir
 	$(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \
 		$(wildcard ebin/*.beam) $(call core_find,$(TEST_DIR)/,*.beam))))))
 	$(gen_verbose) $(call erlang,$(call proper_check.erl,all,undefined,$(MODULES)))
 endif
 endif
 
+# Copyright (c) 2015-2016, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+# Verbosity.
+
+proto_verbose_0 = @echo " PROTO " $(filter %.proto,$(?F));
+proto_verbose = $(proto_verbose_$(V))
+
+# Core targets.
+
+ifneq ($(wildcard src/),)
+ifneq ($(filter gpb protobuffs,$(BUILD_DEPS) $(DEPS)),)
+PROTO_FILES := $(filter %.proto,$(ALL_SRC_FILES))
+ERL_FILES += $(addprefix src/,$(patsubst %.proto,%_pb.erl,$(notdir $(PROTO_FILES))))
+
+ifeq ($(PROTO_FILES),)
+$(ERLANG_MK_TMP)/last-makefile-change-protobuffs:
+	$(verbose) :
+else
+# Rebuild proto files when the Makefile changes.
+# We exclude $(PROJECT).d to avoid a circular dependency.
+$(ERLANG_MK_TMP)/last-makefile-change-protobuffs: $(filter-out $(PROJECT).d,$(MAKEFILE_LIST)) | $(ERLANG_MK_TMP)
+	$(verbose) if test -f $@; then \
+		touch $(PROTO_FILES); \
+	fi
+	$(verbose) touch $@
+
+$(PROJECT).d:: $(ERLANG_MK_TMP)/last-makefile-change-protobuffs
+endif
+
+ifeq ($(filter gpb,$(BUILD_DEPS) $(DEPS)),)
+define compile_proto.erl
+	[begin
+		protobuffs_compile:generate_source(F, [
+			{output_include_dir, "./include"},
+			{output_src_dir, "./src"}])
+	end || F <- string:tokens("$1", " ")],
+	halt().
+endef
+else
+define compile_proto.erl
+	[begin
+		gpb_compile:file(F, [
+			{include_as_lib, true},
+			{module_name_suffix, "_pb"},
+			{o_hrl, "./include"},
+			{o_erl, "./src"}])
+	end || F <- string:tokens("$1", " ")],
+	halt().
+endef
+endif
+
+ifneq ($(PROTO_FILES),)
+$(PROJECT).d:: $(PROTO_FILES)
+	$(verbose) mkdir -p ebin/ include/
+	$(if $(strip $?),$(proto_verbose) $(call erlang,$(call compile_proto.erl,$?)))
+endif
+endif
+endif
+
 # Copyright (c) 2013-2016, Loïc Hoguin <essen@ninenines.eu>
 # This file is part of erlang.mk and subject to the terms of the ISC License.
 
@@ -6707,7 +7467,7 @@ endif
 RELX ?= $(ERLANG_MK_TMP)/relx
 RELX_CONFIG ?= $(CURDIR)/relx.config
 
-RELX_URL ?= https://erlang.mk/res/relx-v3.26.0
+RELX_URL ?= https://erlang.mk/res/relx-v3.27.0
 RELX_OPTS ?=
 RELX_OUTPUT_DIR ?= _rel
 RELX_REL_EXT ?=
@@ -6737,20 +7497,29 @@ distclean:: distclean-relx-rel
 
 # Plugin-specific targets.
 
-$(RELX):
-	$(verbose) mkdir -p $(ERLANG_MK_TMP)
+$(RELX): | $(ERLANG_MK_TMP)
 	$(gen_verbose) $(call core_http_get,$(RELX),$(RELX_URL))
 	$(verbose) chmod +x $(RELX)
 
 relx-rel: $(RELX) rel-deps app
-	$(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) release $(if $(filter 1,$(RELX_TAR)),tar)
+	$(verbose) $(RELX) $(if $(filter 1,$V),-V 3) -c $(RELX_CONFIG) $(RELX_OPTS) release
+	$(verbose) $(MAKE) relx-post-rel
+ifeq ($(RELX_TAR),1)
+	$(verbose) $(RELX) $(if $(filter 1,$V),-V 3) -c $(RELX_CONFIG) $(RELX_OPTS) tar
+endif
 
 relx-relup: $(RELX) rel-deps app
-	$(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) release relup $(if $(filter 1,$(RELX_TAR)),tar)
+	$(verbose) $(RELX) $(if $(filter 1,$V),-V 3) -c $(RELX_CONFIG) $(RELX_OPTS) release
+	$(MAKE) relx-post-rel
+	$(verbose) $(RELX) $(if $(filter 1,$V),-V 3) -c $(RELX_CONFIG) $(RELX_OPTS) relup $(if $(filter 1,$(RELX_TAR)),tar)
 
 distclean-relx-rel:
 	$(gen_verbose) rm -rf $(RELX_OUTPUT_DIR)
 
+# Default hooks.
+relx-post-rel::
+	$(verbose) :
+
 # Run target.
 
 ifeq ($(wildcard $(RELX_CONFIG)),)
@@ -6766,20 +7535,25 @@ define get_relx_release.erl
 		{semver, _} -> "";
 		VsnStr -> Vsn0
 	end,
-	io:format("~s ~s", [Name, Vsn]),
+	Extended = case lists:keyfind(extended_start_script, 1, Config) of
+		{_, true} -> "1";
+		_ -> ""
+	end,
+	io:format("~s ~s ~s", [Name, Vsn, Extended]),
 	halt(0).
 endef
 
 RELX_REL := $(shell $(call erlang,$(get_relx_release.erl)))
 RELX_REL_NAME := $(word 1,$(RELX_REL))
 RELX_REL_VSN := $(word 2,$(RELX_REL))
+RELX_REL_CMD := $(if $(word 3,$(RELX_REL)),console)
 
 ifeq ($(PLATFORM),msys2)
 RELX_REL_EXT := .cmd
 endif
 
 run:: all
-	$(verbose) $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/bin/$(RELX_REL_NAME)$(RELX_REL_EXT) console
+	$(verbose) $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/bin/$(RELX_REL_NAME)$(RELX_REL_EXT) $(RELX_REL_CMD)
 
 ifdef RELOAD
 rel::
@@ -6804,7 +7578,7 @@ endif
 # Configuration.
 
 SHELL_ERL ?= erl
-SHELL_PATHS ?= $(CURDIR)/ebin $(APPS_DIR)/*/ebin $(DEPS_DIR)/*/ebin $(TEST_DIR)
+SHELL_PATHS ?= $(CURDIR)/ebin $(TEST_DIR)
 SHELL_OPTS ?=
 
 ALL_SHELL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(SHELL_DEPS))
@@ -6820,10 +7594,21 @@ help::
 
 $(foreach dep,$(SHELL_DEPS),$(eval $(call dep_target,$(dep))))
 
+ifneq ($(SKIP_DEPS),)
+build-shell-deps:
+else
 build-shell-deps: $(ALL_SHELL_DEPS_DIRS)
-	$(verbose) set -e; for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done
+	$(verbose) set -e; for dep in $(ALL_SHELL_DEPS_DIRS) ; do \
+		if [ -z "$(strip $(FULL))" ] && [ ! -L $$dep ] && [ -f $$dep/ebin/dep_built ]; then \
+			:; \
+		else \
+			$(MAKE) -C $$dep IS_DEP=1; \
+			if [ ! -L $$dep ] && [ -d $$dep/ebin ]; then touch $$dep/ebin/dep_built; fi; \
+		fi \
+	done
+endif
 
-shell: build-shell-deps
+shell:: build-shell-deps
 	$(gen_verbose) $(SHELL_ERL) -pa $(SHELL_PATHS) $(SHELL_OPTS)
 
 # Copyright 2017, Stanislaw Klekot <dozzie@jarowit.net>
@@ -6914,17 +7699,21 @@ ifeq ($(filter triq,$(DEPS) $(TEST_DEPS)),triq)
 tests:: triq
 
 define triq_check.erl
+	$(call cover.erl)
 	code:add_pathsa([
 		"$(call core_native_path,$(CURDIR)/ebin)",
 		"$(call core_native_path,$(DEPS_DIR)/*/ebin)",
 		"$(call core_native_path,$(TEST_DIR))"]),
-	try
-		case $(1) of
+	try begin
+		CoverSetup(),
+		Res = case $(1) of
 			all -> [true] =:= lists:usort([triq:check(M) || M <- [$(call comma_list,$(3))]]);
 			module -> triq:check($(2));
 			function -> triq:check($(2))
-		end
-	of
+		end,
+		CoverExport("$(COVER_DATA_DIR)/triq.coverdata"),
+		Res
+	end of
 		true -> halt(0);
 		_ -> halt(1)
 	catch error:undef ->
@@ -6935,15 +7724,15 @@ endef
 
 ifdef t
 ifeq (,$(findstring :,$(t)))
-triq: test-build
+triq: test-build cover-data-dir
 	$(verbose) $(call erlang,$(call triq_check.erl,module,$(t)))
 else
-triq: test-build
+triq: test-build cover-data-dir
 	$(verbose) echo Testing $(t)/0
 	$(verbose) $(call erlang,$(call triq_check.erl,function,$(t)()))
 endif
 else
-triq: test-build
+triq: test-build cover-data-dir
 	$(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \
 		$(wildcard ebin/*.beam) $(call core_find,$(TEST_DIR)/,*.beam))))))
 	$(gen_verbose) $(call erlang,$(call triq_check.erl,all,undefined,$(MODULES)))
@@ -6995,9 +7784,14 @@ distclean-xref:
 # This file is part of erlang.mk and subject to the terms of the ISC License.
 
 COVER_REPORT_DIR ?= cover
-COVER_DATA_DIR ?= $(CURDIR)
+COVER_DATA_DIR ?= $(COVER_REPORT_DIR)
 
-# Hook in coverage to ct
+ifdef COVER
+COVER_APPS ?= $(notdir $(ALL_APPS_DIRS))
+COVER_DEPS ?=
+endif
+
+# Code coverage for Common Test.
 
 ifdef COVER
 ifdef CT_RUN
@@ -7007,13 +7801,44 @@ test-build:: $(TEST_DIR)/ct.cover.spec
 $(TEST_DIR)/ct.cover.spec: cover-data-dir
 	$(gen_verbose) printf "%s\n" \
 		"{incl_app, '$(PROJECT)', details}." \
-		'{export,"$(abspath $(COVER_DATA_DIR))/ct.coverdata"}.' > $@
+		"{incl_dirs, '$(PROJECT)', [\"$(call core_native_path,$(CURDIR)/ebin)\" \
+			$(foreach a,$(COVER_APPS),$(comma) \"$(call core_native_path,$(APPS_DIR)/$a/ebin)\") \
+			$(foreach d,$(COVER_DEPS),$(comma) \"$(call core_native_path,$(DEPS_DIR)/$d/ebin)\")]}." \
+		'{export,"$(call core_native_path,$(abspath $(COVER_DATA_DIR))/ct.coverdata)"}.' > $@
 
 CT_RUN += -cover $(TEST_DIR)/ct.cover.spec
 endif
 endif
 endif
 
+# Code coverage for other tools.
+
+ifdef COVER
+define cover.erl
+	CoverSetup = fun() ->
+		Dirs = ["$(call core_native_path,$(CURDIR)/ebin)"
+			$(foreach a,$(COVER_APPS),$(comma) "$(call core_native_path,$(APPS_DIR)/$a/ebin)")
+			$(foreach d,$(COVER_DEPS),$(comma) "$(call core_native_path,$(DEPS_DIR)/$d/ebin)")],
+		[begin
+			case filelib:is_dir(Dir) of
+				false -> false;
+				true ->
+					case cover:compile_beam_directory(Dir) of
+						{error, _} -> halt(1);
+						_ -> true
+					end
+			end
+		end || Dir <- Dirs]
+	end,
+	CoverExport = fun(Filename) -> cover:export(Filename) end,
+endef
+else
+define cover.erl
+	CoverSetup = fun() -> ok end,
+	CoverExport = fun(_) -> ok end,
+endef
+endif
+
 # Core targets
 
 ifdef COVER
@@ -7073,7 +7898,9 @@ ifneq ($(COVER_REPORT_DIR),)
 
 cover-report-clean:
 	$(gen_verbose) rm -rf $(COVER_REPORT_DIR)
+ifneq ($(COVER_REPORT_DIR),$(COVER_DATA_DIR))
 	$(if $(shell ls -A $(COVER_DATA_DIR)/),,$(verbose) rmdir $(COVER_DATA_DIR))
+endif
 
 ifeq ($(COVERDATA),)
 cover-report:
@@ -7163,7 +7990,7 @@ __ARCHIVE_BELOW__
 endef
 
 sfx:
-	$(call render_template,sfx_stub,$(SFX_OUTPUT_FILE))
+	$(verbose) $(call core_render,sfx_stub,$(SFX_OUTPUT_FILE))
 	$(gen_verbose) cat $(SFX_ARCHIVE) >> $(SFX_OUTPUT_FILE)
 	$(verbose) chmod +x $(SFX_OUTPUT_FILE)
 
@@ -7182,6 +8009,11 @@ $(foreach p,$(DEP_PLUGINS),\
 		$(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\
 		$(call core_dep_plugin,$p/plugins.mk,$p))))
 
+help:: help-plugins
+
+help-plugins::
+	$(verbose) :
+
 # Copyright (c) 2013-2015, Loïc Hoguin <essen@ninenines.eu>
 # Copyright (c) 2015-2016, Jean-Sébastien Pédron <jean-sebastien@rabbitmq.com>
 # This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -7216,11 +8048,11 @@ else
 #
 # $(ALL_DEPS_DIRS) includes $(BUILD_DEPS).
 
-$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_DEPS_DIRS)
-$(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_DOC_DEPS_DIRS)
-$(ERLANG_MK_RECURSIVE_REL_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_REL_DEPS_DIRS)
-$(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_TEST_DEPS_DIRS)
-$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_SHELL_DEPS_DIRS)
+$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(LOCAL_DEPS_DIRS) $(ALL_DEPS_DIRS)
+$(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST): $(LOCAL_DEPS_DIRS) $(ALL_DEPS_DIRS) $(ALL_DOC_DEPS_DIRS)
+$(ERLANG_MK_RECURSIVE_REL_DEPS_LIST): $(LOCAL_DEPS_DIRS) $(ALL_DEPS_DIRS) $(ALL_REL_DEPS_DIRS)
+$(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST): $(LOCAL_DEPS_DIRS) $(ALL_DEPS_DIRS) $(ALL_TEST_DEPS_DIRS)
+$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): $(LOCAL_DEPS_DIRS) $(ALL_DEPS_DIRS) $(ALL_SHELL_DEPS_DIRS)
 
 # Allow to use fetch-deps and $(DEP_TYPES) to fetch multiple types of
 # dependencies with a single target.
@@ -7237,24 +8069,17 @@ ifneq ($(filter shell,$(DEP_TYPES)),)
 $(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_SHELL_DEPS_DIRS)
 endif
 
-ERLANG_MK_RECURSIVE_TMP_LIST := $(abspath $(ERLANG_MK_TMP)/recursive-tmp-deps.log)
+ERLANG_MK_RECURSIVE_TMP_LIST := $(abspath $(ERLANG_MK_TMP)/recursive-tmp-deps-$(shell echo $$PPID).log)
 
 $(ERLANG_MK_RECURSIVE_DEPS_LIST) \
 $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) \
 $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) \
 $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) \
-$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST):
+$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): | $(ERLANG_MK_TMP)
 ifeq ($(IS_APP)$(IS_DEP),)
-	$(verbose) mkdir -p $(ERLANG_MK_TMP)
 	$(verbose) rm -f $(ERLANG_MK_RECURSIVE_TMP_LIST)
 endif
-ifndef IS_APP
-	$(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \
-		$(MAKE) -C $$dep $@ \
-		 IS_APP=1 \
-		 ERLANG_MK_RECURSIVE_TMP_LIST=$(ERLANG_MK_RECURSIVE_TMP_LIST); \
-	done
-endif
+	$(verbose) touch $(ERLANG_MK_RECURSIVE_TMP_LIST)
 	$(verbose) set -e; for dep in $^ ; do \
 		if ! grep -qs ^$$dep$$ $(ERLANG_MK_RECURSIVE_TMP_LIST); then \
 			echo $$dep >> $(ERLANG_MK_RECURSIVE_TMP_LIST); \
@@ -7267,7 +8092,11 @@ endif
 		fi \
 	done
 ifeq ($(IS_APP)$(IS_DEP),)
-	$(verbose) sort < $(ERLANG_MK_RECURSIVE_TMP_LIST) | uniq > $@
+	$(verbose) sort < $(ERLANG_MK_RECURSIVE_TMP_LIST) | \
+		uniq > $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted
+	$(verbose) cmp -s $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted $@ \
+		|| mv $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted $@
+	$(verbose) rm -f $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted
 	$(verbose) rm $(ERLANG_MK_RECURSIVE_TMP_LIST)
 endif
 endif # ifneq ($(SKIP_DEPS),)
@@ -7285,3 +8114,43 @@ list-shell-deps: $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST)
 
 list-deps list-doc-deps list-rel-deps list-test-deps list-shell-deps:
 	$(verbose) cat $^
+
+# Query dependencies recursively.
+
+.PHONY: query-deps query-doc-deps query-rel-deps query-test-deps \
+	query-shell-deps
+
+QUERY ?= name fetch_method repo version
+
+define query_target
+$(1): $(2) clean-tmp-query.log
+ifeq ($(IS_APP)$(IS_DEP),)
+	$(verbose) rm -f $(4)
+endif
+	$(verbose) $(foreach dep,$(3),\
+		echo $(PROJECT): $(foreach q,$(QUERY),$(call query_$(q),$(dep))) >> $(4) ;)
+	$(if $(filter-out query-deps,$(1)),,\
+		$(verbose) set -e; for dep in $(3) ; do \
+			if grep -qs ^$$$$dep$$$$ $(ERLANG_MK_TMP)/query.log; then \
+				:; \
+			else \
+				echo $$$$dep >> $(ERLANG_MK_TMP)/query.log; \
+				$(MAKE) -C $(DEPS_DIR)/$$$$dep $$@ QUERY="$(QUERY)" IS_DEP=1 || true; \
+			fi \
+		done)
+ifeq ($(IS_APP)$(IS_DEP),)
+	$(verbose) touch $(4)
+	$(verbose) cat $(4)
+endif
+endef
+
+clean-tmp-query.log:
+ifeq ($(IS_DEP),)
+	$(verbose) rm -f $(ERLANG_MK_TMP)/query.log
+endif
+
+$(eval $(call query_target,query-deps,$(ERLANG_MK_RECURSIVE_DEPS_LIST),$(BUILD_DEPS) $(DEPS),$(ERLANG_MK_QUERY_DEPS_FILE)))
+$(eval $(call query_target,query-doc-deps,$(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST),$(DOC_DEPS),$(ERLANG_MK_QUERY_DOC_DEPS_FILE)))
+$(eval $(call query_target,query-rel-deps,$(ERLANG_MK_RECURSIVE_REL_DEPS_LIST),$(REL_DEPS),$(ERLANG_MK_QUERY_REL_DEPS_FILE)))
+$(eval $(call query_target,query-test-deps,$(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST),$(TEST_DEPS),$(ERLANG_MK_QUERY_TEST_DEPS_FILE)))
+$(eval $(call query_target,query-shell-deps,$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST),$(SHELL_DEPS),$(ERLANG_MK_QUERY_SHELL_DEPS_FILE)))