Browse Source

Switch to the new erlang.mk

Loïc Hoguin 11 years ago
parent
commit
9d6293e411
2 changed files with 822 additions and 309 deletions
  1. 230 137
      README.md
  2. 592 172
      erlang.mk

+ 230 - 137
README.md

@@ -11,8 +11,7 @@ Requirements
 `erlang.mk` requires GNU Make and expects to be ran in a standard
 `erlang.mk` requires GNU Make and expects to be ran in a standard
 unix environment with Erlang installed and in the `$PATH`.
 unix environment with Erlang installed and in the `$PATH`.
 
 
-`erlang.mk` uses `wget` for downloading the package index file
-when the `pkg://` scheme is used.
+`erlang.mk` uses `wget` for downloading the package index file.
 
 
 `erlang.mk` will NOT work if the path contains spaces. This is a
 `erlang.mk` will NOT work if the path contains spaces. This is a
 limitation of POSIX compatible make build tools.
 limitation of POSIX compatible make build tools.
@@ -25,24 +24,71 @@ Makefile:
 
 
 ``` Makefile
 ``` Makefile
 PROJECT = my_project
 PROJECT = my_project
-
 include erlang.mk
 include erlang.mk
 ```
 ```
 
 
-Dependencies
+Alternatively you can use the following command to generate a skeleton
+of an OTP application:
+
+``` bash
+$ make -f erlang.mk bootstrap
+```
+
+To generate a skeleton of an OTP library:
+
+``` bash
+$ make -f erlang.mk bootstrap-lib
+```
+
+Finally if you are going to create a release of this project you may
+want to also use the `bootstrap-rel` target.
+
+You can combine targets to perform many operations. For example, the
+shell command `make clean app` will have the effect of recompiling
+the application fully, without touching the dependencies.
+
+A common workflow when editing a file would be to run `make` regularly
+to see if it compiles (or less often `make clean app` if you want to
+recompile everything), followed by `make dialyze` to see if there are
+any type errors and then `make tests` to run the test suites. The
+result of the test runs can be browsed from the `logs/index.html` file.
+
+Getting help
 ------------
 ------------
 
 
-Erlang projects often depend on other projects to run. Adding dependencies
-to the Makefile is easy. You need to create the variable `DEPS` listing
-the names of all the dependencies, along with one `dep_$(NAME)` variable
-per dependency giving the git repository and commit to retrieve.
+You can use `make help` to get help about erlang.mk or its plugins.
 
 
-These variables should be defined before the include line.
+Packages
+--------
+
+A package index functionality is included with erlang.mk.
+
+To use a package, you simply have to add it to the `DEPS` variable
+in your Makefile. For example this depends on Cowboy:
 
 
 ``` Makefile
 ``` Makefile
-DEPS = cowboy bullet
-dep_cowboy = https://github.com/extend/cowboy.git 0.8.4
-dep_bullet = https://github.com/extend/bullet.git 0.4.1
+PROJECT = my_project
+DEPS = cowboy
+include erlang.mk
+```
+
+If the project you want is not included in the package index, or if
+you want a different version, a few options are available. You can
+edit the package file and contribute to it by opening a pull request.
+You can use a custom package file, in which case you will probably
+want to set the `PKG_FILE2` variable to its location. Or you can
+put the project information directly in the Makefile.
+
+In the latter case you need to create a variable `dep_*` with the
+asterisk replaced by the project name, for example `cowboy`. This
+variable must contain three things: the fetching method used, the
+URL and the version requested.
+
+The following snippet overrides the Cowboy version required:
+
+``` Makefile
+DEPS = cowboy
+dep_cowboy = git https://github.com/extend/cowboy 1.0.0
 ```
 ```
 
 
 They will always be compiled using the command `make`. If the dependency
 They will always be compiled using the command `make`. If the dependency
@@ -60,173 +106,220 @@ dep_ct_helper = https://github.com/extend/ct_helper.git master
 Please note that the test dependencies will only be compiled once
 Please note that the test dependencies will only be compiled once
 when they are fetched, unlike the normal dependencies.
 when they are fetched, unlike the normal dependencies.
 
 
-Package index
+Releases
+--------
+
+If a `relx.config` file is present, erlang.mk will download `relx`
+automatically and build the release into the `_rel` folder. This
+is the default command when the file exists.
+
+No special configuration is required for this to work.
+
+Customization
 -------------
 -------------
 
 
-A very basic package index is included with erlang.mk. You can list
-all known packages with the `make pkg-list` command. You can search
-a package with the `make pkg-search q=STRING` command, replacing
-`STRING` with what you want to search. Use quotes around the string
-if needed.
+A custom erlang.mk may be created by editing the `build.config`
+file and then running `make`. Only the core package handling
+and erlc support are required.
+
+If you need more functionality out of your Makefile, you can add extra
+targets after the include line, or create an erlang.mk plugin.
+
+Defining a target before the include line will override the default
+target `all`.
+
+The rest of this README starts by listing the core functionality
+and then details each plugin individually.
+
+Core functionality
+------------------
+
+The following targets are standard:
+
+`all` is equivalent to `deps app rel`.
 
 
-In addition, it is possible to specify dependencies in a simplified
-manner if they exist in the package index. The above example could
-instead read as:
+`deps` fetches and compiles the dependencies.
+
+`app` compiles the application.
+
+`rel` builds the release.
+
+`docs` generates the documentation.
+
+`tests` runs the test suites.
+
+`clean` deletes the output files.
+
+`distclean` deletes the output files but also any intermediate
+files that are usually worth keeping around to save time,
+and any other files needed by plugins (for example the Dialyzer
+PLT file).
+
+`help` gives some help about using erlang.mk.
+
+You may add additional operations to them by using the double
+colons. Make will run all targets sharing the same name when
+invoked.
 
 
 ``` Makefile
 ``` Makefile
-DEPS = cowboy bullet
-dep_cowboy = pkg://cowboy 0.8.4
-dep_bullet = pkg://bullet 0.4.1
+clean::
+	@rm anotherfile
 ```
 ```
 
 
-erlang.mk will look inside the index for the proper URL and use it
-for fetching the dependency.
+You can enable verbose mode by calling Make with the variable
+`V` set to 1.
 
 
-All packages featured in the index are compatible with erlang.mk
-with no extra work required.
+``` bash
+$ V=1 make
 
 
-Releases
---------
+Core package functionality
+--------------------------
 
 
-If a `relx.config` file is present, erlang.mk will download `relx`
-automatically and build the release into the `_rel` folder. This
-is the default command when the file exists.
+The following targets are specific to packages:
 
 
-No special configuration is required for this to work.
+`pkg-list` lists all packages in the index.
 
 
-Compiled files
---------------
+`pkg-search n=STRING` searches the index for STRING.
 
 
-The following files will be automatically compiled:
+Packages are downloaded into `DEPS_DIR` (`./deps/` by default).
 
 
-| Wildcard                 | Description                   |
-| ------------------------ | ----------------------------- |
-| `src/$(PROJECT).app.src` | OTP application resource file |
-| `src/*.erl`              | Erlang source files           |
-| `src/*.core`             | Core Erlang source files      |
-| `src/*.xrl`              | Leex source files             |
-| `src/*.yrl`              | Yecc source files             |
-| `templates/*.dtl`        | ErlyDTL template files        |
+The package index file is downloaded from `PKG_FILE_URL`
+and saved in `PKG_FILE2`.
+```
+Core compiler functionality
+---------------------------
 
 
-Only the `.app.src` file and at least one `.erl` file are required.
+erlang.mk will automatically compile the OTP application
+resource file found in `src/$(PROJECT).app.src` (do note it
+requires an empty `modules` line); Erlang source files found
+in `src/*.erl` or any subdirectory; Core Erlang source files
+found in `src/*.core` or any subdirectory; Leex source files
+found in `src/*.xrl` or any subdirectory; and Yecc source
+files found in `src/*.yrl` or any subdirectory.
 
 
-Commands
---------
+You can change compilation options by setting the `ERLC_OPTS`
+variable. It takes the arguments that will then be passed to
+`erlc`. For more information, please see `erl -man erlc`.
 
 
-The following targets are defined:
-
-| Targets      | Description                                  |
-| ------------ | -------------------------------------------- |
-| `all`        | Compile the application and all dependencies |
-| `clean-all`  | Clean the application and all dependencies   |
-| `app`        | Compile the application                      |
-| `clean`      | Clean the application                        |
-| `deps`       | Compile the dependencies                     |
-| `clean-deps` | Clean the dependencies                       |
-| `docs`       | Generate the Edoc documentation              |
-| `clean-docs` | Clean the Edoc documentation                 |
-| `test_*`     | Run the common_test suite `*`                |
-| `tests`      | Run all the common_test suites               |
-| `build-plt`  | Generate the PLT needed by Dialyzer          |
-| `dialyze`    | Run Dialyzer on the application              |
-| `pkg-list`   | List packages in the index                   |
-| `pkg-search` | Search for packages in the index             |
-| `rel`        | Build a release                              |
-| `clean-rel`  | Delete the previously built release          |
-
-Cleaning means removing all generated and temporary files.
-
-Dependencies are fetched as soon as a command involving them is
-invoked. This means that most of the targets will trigger a
-dependency fetch. It is only done once per dependency.
-
-You can run an individual test suite by using the special `test_*`
-targets. For example if you have a common_test suite named `spdy`
-and you want to run only this suite and not the others, you can
-use the `make test_spdy` command.
+You can specify a list of modules to be compiled first using
+the `COMPILE_FIRST` variable.
 
 
-The default target when calling `make` is `all` when no `relx.config`
-exists, and `rel` when it does exist.
+Bootstrap plugin
+----------------
 
 
-You can combine targets to perform many operations. For example, the
-shell command `make clean app` will have the effect of recompiling
-the application fully, without touching the dependencies.
+This plugin is available by default. It adds the following
+targets:
 
 
-A common workflow when editing a file would be to run `make` regularly
-to see if it compiles (or less often `make clean app` if you want to
-recompile everything), followed by `make dialyze` to see if there are
-any type errors and then `make tests` to run the test suites. The
-result of the test runs can be browsed from the `logs/index.html` file.
+`bootstrap` generates a skeleton of an OTP application.
 
 
-Options
--------
+`bootstrap-lib` generates a skeleton of an OTP library.
+
+`bootstrap-rel` generates the files needed to build a release.
+
+`new` generate a skeleton module based on one of the available
+templates.
 
 
-The following variables can be overriden:
+`list-templates` lists the available templates.
 
 
-`V` defines the verbosity of the commands. You can set it
-to an empty value to make commands verbose.
+C compiler plugin
+-----------------
 
 
-`ERLC_OPTS` allows you to change the `erlc` compilation
-options. You should always compile with at least the `+debug_info` set.
+This plugin is not included by default. It is meant to
+simplify the management of projects that include C source
+code, like NIFs.
 
 
-`COMPILE_FIRST` is a list of modules (not filenames) that should be
-compiled before all others.
+If the file `$(C_SRC_DIR)/Makefile` exists, then the plugin
+simply calls it when needed. Otherwise it tries to compile
+it directly.
 
 
-`DEPS_DIR` is the path to the directory where the dependencies are
-downloaded to. It defaults to `deps`. It will be propagated into
-all the subsequent make calls, allowing all dependencies to use
-the same folder as expected.
+You can use a different directory than `./c_src` by setting
+the `C_SRC_DIR` variable.
 
 
-`EDOC_OPTS` allows you to specify
-[options](http://www.erlang.org/doc/man/edoc.html#run-3) to pass to
-`edoc` when building the documentation. Notice: not all options are
-documented in one place; follow the links to get to the options for
-the various operations of the documentation generation.
+You can override the output file by setting the `C_SRC_OUTPUT`
+variable.
 
 
-`TEST_ERLC_OPTS` allows you to change the `erlc` compilation
-options that are used for building the project for testing, but
-also the tests themselves. Unlike `ERLC_OPTS` it doesn't consider
-warnings to be errors and does not warn when `export_all` is used.
+You can override the temporary file containing information
+about Erlang's environment by setting the `C_SRC_ENV` variable.
+This file is automatically generated on first run.
 
 
-`CT_SUITES` is the list of common_test suites to run when you use
-the `make tests` command. The default behaviour is to autodetect your
-common_test suites. If you only want to run the tests in `ponies_SUITE`
-you should set this variable to `ponies`.
+Finally you can add extra compiler options using the
+`C_SRC_OPTS` variable. You can also override the defaults
+`CC` and `CFLAGS` if required.
 
 
-`CT_OPTS` allows you to specify extra common_test options.
+Common_test plugin
+------------------
 
 
-`PLT_APPS` is the list of applications to include when building the
-`.plt` file for Dialyzer. You do not need to put `erts`, `kernel` or
-`stdlib` in there because they will always be included. The applications
-the project depends on will also be included.
+This plugin is available by default.
 
 
-`DIALYZER_PLT` allows you to change the PLT file used by dialyzer.
+There is nothing to configure to use it, simply create your
+test suites in the `./test/` directory and erlang.mk will
+figure everything out automatically.
 
 
-`DIALYZER_OPTS` allows you to change the `dialyzer` options.
+You can override the list of suites that will run when using
+`make tests` by setting the `CT_SUITES` variable.
 
 
-`PKG_FILE` allows you to change the location of the package index file
-on your system.
+You can add extra `ct_run` options by defining the `CT_OPTS`
+variable. For more information please see `erl -man ct_run`.
 
 
-`PKG_FILE_URL` allows you to change the URL from which the package index
-file is fetched.
+You can run an individual test suite by using the special `ct-*`
+targets. For example if you have a common_test suite named `spdy`
+and you want to run only this suite and not the others, you can
+use the `make ct-spdy` command.
 
 
-`RELX_CONFIG` is the location of the `relx.config` file, if any.
+Dialyzer plugin
+---------------
 
 
-`RELX` is the location of the `relx` executable for building releases.
+This plugin is available by default. It adds the following
+targets:
 
 
-`RELX_URL` is the location where `relx` can be downloaded if it is
-not found locally.
+`plt` builds the PLT file for this application.
 
 
-`RELX_OPTS` is for setting relx in-line options, if any.
+`dialyze` runs Dialyzer.
 
 
-Extra targets
--------------
+The PLT file is built in `./$(PROJECT).plt` by default.
+You can override this location by setting the `DIALYZER_PLT`
+variable.
 
 
-If you need more functionality out of your Makefile, you can add extra
-targets after the include line.
+The `PLT_APPS` variable lists the applications that will be
+included in the PLT file. There is no need to specify `erts`,
+`kernel`, `stdlib` or the project's dependencies here, as they
+are automatically added.
 
 
-Defining a target before the include line will override the default
-target `all`.
+Dialyzer options can be modified by defining the `DIALYZER_OPTS`
+variable. For more information please see `erl -man dialyzer`.
+
+EDoc plugin
+-----------
+
+This plugin is available by default.
+
+EDoc options can be specified in Erlang format by defining
+the `EDOC_OPTS` variable. For more information please see
+`erl -man edoc`.
+
+ErlyDTL plugin
+--------------
+
+This plugin is available by default. It adds automatic
+compilation of ErlyDTL templates found in `templates/*.dtl`
+or any subdirectory.
+
+Relx plugin
+-----------
+
+This plugin is available by default.
+
+You can change the location of the `relx` executable
+(downloaded automatically) by defining the `RELX` variable,
+and the location of the configuration file by defining
+the `RELX_CONFIG` variable.
+
+The URL used to download `relx` can be overriden by setting
+the `RELX_URL` variable.
+
+You can change the generated releases location by setting
+the `RELX_OUTPUT_DIR` variable. Any other option should go
+in the `RELX_OPTS` variable.
 
 
 Support
 Support
 -------
 -------

+ 592 - 172
erlang.mk

@@ -12,109 +12,195 @@
 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 
-# Project.
+.PHONY: all deps app rel docs tests clean distclean help
+
+ERLANG_MK_VERSION = 1
+
+# Core configuration.
 
 
 PROJECT ?= $(notdir $(CURDIR))
 PROJECT ?= $(notdir $(CURDIR))
+PROJECT := $(strip $(PROJECT))
 
 
-# Packages database file.
+# Verbosity.
 
 
-PKG_FILE ?= $(CURDIR)/.erlang.mk.packages.v1
-export PKG_FILE
+V ?= 0
 
 
-PKG_FILE_URL ?= https://raw.githubusercontent.com/extend/erlang.mk/master/packages.v1.tsv
+gen_verbose_0 = @echo " GEN   " $@;
+gen_verbose = $(gen_verbose_$(V))
 
 
-define get_pkg_file
-	wget --no-check-certificate -O $(PKG_FILE) $(PKG_FILE_URL) || rm $(PKG_FILE)
+# Core targets.
+
+all:: deps app rel
+
+clean::
+	$(gen_verbose) rm -f erl_crash.dump
+
+distclean:: clean
+
+help::
+	@printf "%s\n" \
+		"erlang.mk (version $(ERLANG_MK_VERSION)) is distributed under the terms of the ISC License." \
+		"Copyright (c) 2013-2014 Loïc Hoguin <essen@ninenines.eu>" \
+		"" \
+		"Usage: [V=1] make [target]" \
+		"" \
+		"Core targets:" \
+		"  all         Run deps, app and rel targets in that order" \
+		"  deps        Fetch dependencies (if needed) and compile them" \
+		"  app         Compile the project" \
+		"  rel         Build a release for this project, if applicable" \
+		"  docs        Build the documentation for this project" \
+		"  tests       Run the tests for this project" \
+		"  clean       Delete temporary and output files from most targets" \
+		"  distclean   Delete all temporary and output files" \
+		"  help        Display this help and exit" \
+		"" \
+		"The target clean only removes files that are commonly removed." \
+		"Dependencies and releases are left untouched." \
+		"" \
+		"Setting V=1 when calling make enables verbose mode."
+
+# Core functions.
+
+define core_http_get
+	wget --no-check-certificate -O $(1) $(2)|| rm $(1)
 endef
 endef
 
 
-# Verbosity and tweaks.
+# Copyright (c) 2013-2014, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
 
 
-V ?= 0
+.PHONY: distclean-deps distclean-pkg pkg-list pkg-search
 
 
-appsrc_verbose_0 = @echo " APP   " $(PROJECT).app.src;
-appsrc_verbose = $(appsrc_verbose_$(V))
+# Configuration.
 
 
-erlc_verbose_0 = @echo " ERLC  " $(filter %.erl %.core,$(?F));
-erlc_verbose = $(erlc_verbose_$(V))
+DEPS_DIR ?= $(CURDIR)/deps
+export DEPS_DIR
 
 
-xyrl_verbose_0 = @echo " XYRL  " $(filter %.xrl %.yrl,$(?F));
-xyrl_verbose = $(xyrl_verbose_$(V))
+REBAR_DEPS_DIR = $(DEPS_DIR)
+export REBAR_DEPS_DIR
 
 
-dtl_verbose_0 = @echo " DTL   " $(filter %.dtl,$(?F));
-dtl_verbose = $(dtl_verbose_$(V))
+ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DEPS))
 
 
-gen_verbose_0 = @echo " GEN   " $@;
-gen_verbose = $(gen_verbose_$(V))
+ifeq ($(filter $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),)
+ifeq ($(ERL_LIBS),)
+	ERL_LIBS = $(DEPS_DIR)
+else
+	ERL_LIBS := $(ERL_LIBS):$(DEPS_DIR)
+endif
+endif
+export ERL_LIBS
 
 
-.PHONY: rel clean-rel all clean-all app clean deps clean-deps \
-	docs clean-docs build-tests tests build-plt dialyze
+PKG_FILE2 ?= $(CURDIR)/.erlang.mk.packages.v2
+export PKG_FILE2
 
 
-# Release.
+PKG_FILE_URL ?= https://raw.githubusercontent.com/extend/erlang.mk/master/packages.v2.tsv
 
 
-RELX_CONFIG ?= $(CURDIR)/relx.config
+# Core targets.
 
 
-ifneq ($(wildcard $(RELX_CONFIG)),)
+deps:: $(ALL_DEPS_DIRS)
+	@for dep in $(ALL_DEPS_DIRS) ; do \
+		if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ] ; then \
+			$(MAKE) -C $$dep ; \
+		else \
+			echo "include $(CURDIR)/erlang.mk" | $(MAKE) -f - -C $$dep ; \
+		fi ; \
+	done
 
 
-RELX ?= $(CURDIR)/relx
-export RELX
+distclean:: distclean-deps distclean-pkg
 
 
-RELX_URL ?= https://github.com/erlware/relx/releases/download/v1.0.2/relx
-RELX_OPTS ?=
-RELX_OUTPUT_DIR ?= _rel
+# Deps related targets.
 
 
-ifneq ($(firstword $(subst -o,,$(RELX_OPTS))),)
-	RELX_OUTPUT_DIR = $(firstword $(subst -o,,$(RELX_OPTS)))
-endif
+define dep_fetch
+	if [ "$$$$VS" = "git" ]; then \
+		git clone -n -- $$$$REPO $(DEPS_DIR)/$(1); \
+		cd $(DEPS_DIR)/$(1) && git checkout -q $$$$COMMIT; \
+	else \
+		exit 78; \
+	fi
+endef
 
 
-define get_relx
-	wget -O $(RELX) $(RELX_URL) || rm $(RELX)
-	chmod +x $(RELX)
+define dep_target
+$(DEPS_DIR)/$(1):
+	@mkdir -p $(DEPS_DIR)
+	@if [ ! -f $(PKG_FILE2) ]; then $(call core_http_get,$(PKG_FILE2),$(PKG_FILE_URL)); fi
+ifeq (,$(dep_$(1)))
+	DEPPKG=$$$$(awk 'BEGIN { FS = "\t" }; $$$$1 == "$(1)" { print $$$$2 " " $$$$3 " " $$$$4 }' $(PKG_FILE2);) \
+	VS=$$$$(echo $$$$DEPPKG | cut -d " " -f1); \
+	REPO=$$$$(echo $$$$DEPPKG | cut -d " " -f2); \
+	COMMIT=$$$$(echo $$$$DEPPKG | cut -d " " -f3); \
+	$(call dep_fetch,$(1))
+else
+	VS=$(word 1,$(dep_$(1))); \
+	REPO=$(word 1,$(dep_$(2))); \
+	COMMIT=$(word 1,$(dep_$(3))); \
+	$(call dep_fetch,$(1))
+endif
 endef
 endef
 
 
-rel: clean-rel all $(RELX)
-	@$(RELX) -c $(RELX_CONFIG) $(RELX_OPTS)
+$(foreach dep,$(DEPS),$(eval $(call dep_target,$(dep))))
 
 
-$(RELX):
-	@$(call get_relx)
+distclean-deps:
+	$(gen_verbose) rm -rf $(DEPS_DIR)
 
 
-clean-rel:
-	$(gen_verbose) rm -rf $(RELX_OUTPUT_DIR)
+# Packages related targets.
 
 
-endif
+$(PKG_FILE2):
+	$(call core_http_get,$(PKG_FILE2),$(PKG_FILE_URL))
 
 
-# Deps directory.
+pkg-list: $(PKG_FILE2)
+	@cat $(PKG_FILE2) | awk 'BEGIN { FS = "\t" }; { print \
+		"Name:\t\t" $$1 "\n" \
+		"Repository:\t" $$3 "\n" \
+		"Website:\t" $$5 "\n" \
+		"Description:\t" $$6 "\n" }'
 
 
-DEPS_DIR ?= $(CURDIR)/deps
-export DEPS_DIR
+ifdef q
+pkg-search: $(PKG_FILE2)
+	@cat $(PKG_FILE2) | grep -i ${q} | awk 'BEGIN { FS = "\t" }; { print \
+		"Name:\t\t" $$1 "\n" \
+		"Repository:\t" $$3 "\n" \
+		"Website:\t" $$5 "\n" \
+		"Description:\t" $$6 "\n" }'
+else
+pkg-search:
+	$(error Usage: make pkg-search q=STRING)
+endif
 
 
-REBAR_DEPS_DIR = $(DEPS_DIR)
-export REBAR_DEPS_DIR
+distclean-pkg:
+	$(gen_verbose) rm -f $(PKG_FILE2)
 
 
-ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DEPS))
-ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS))
+help::
+	@printf "%s\n" "" \
+		"Package-related targets:" \
+		"  pkg-list              List all known packages" \
+		"  pkg-search q=STRING   Search for STRING in the package index"
 
 
-# Application.
+# Copyright (c) 2013-2014, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
 
 
-ifeq ($(filter $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),)
-ifeq ($(ERL_LIBS),)
-	ERL_LIBS = $(DEPS_DIR)
-else
-	ERL_LIBS := $(ERL_LIBS):$(DEPS_DIR)
-endif
-endif
-export ERL_LIBS
+.PHONY: clean-app
+
+# Configuration.
 
 
 ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \
 ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \
 	+warn_shadow_vars +warn_obsolete_guard # +bin_opt_info +warn_missing_spec
 	+warn_shadow_vars +warn_obsolete_guard # +bin_opt_info +warn_missing_spec
 COMPILE_FIRST ?=
 COMPILE_FIRST ?=
 COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST)))
 COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST)))
 
 
-all: deps app
+# Verbosity.
 
 
-clean-all: clean clean-deps clean-docs
-	$(gen_verbose) rm -rf .$(PROJECT).plt $(DEPS_DIR) logs
+appsrc_verbose_0 = @echo " APP   " $(PROJECT).app.src;
+appsrc_verbose = $(appsrc_verbose_$(V))
 
 
-app: ebin/$(PROJECT).app
+erlc_verbose_0 = @echo " ERLC  " $(filter %.erl %.core,$(?F));
+erlc_verbose = $(erlc_verbose_$(V))
+
+xyrl_verbose_0 = @echo " XYRL  " $(filter %.xrl %.yrl,$(?F));
+xyrl_verbose = $(xyrl_verbose_$(V))
+
+# Core targets.
+
+app:: ebin/$(PROJECT).app
 	$(eval MODULES := $(shell find ebin -type f -name \*.beam \
 	$(eval MODULES := $(shell find ebin -type f -name \*.beam \
 		| sed "s/ebin\//'/;s/\.beam/',/" | sed '$$s/.$$//'))
 		| sed "s/ebin\//'/;s/\.beam/',/" | sed '$$s/.$$//'))
 	$(appsrc_verbose) cat src/$(PROJECT).app.src \
 	$(appsrc_verbose) cat src/$(PROJECT).app.src \
@@ -132,99 +218,336 @@ define compile_xyrl
 	@rm ebin/*.erl
 	@rm ebin/*.erl
 endef
 endef
 
 
-define compile_dtl
-	$(dtl_verbose) erl -noshell -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/ -eval ' \
-		Compile = fun(F) -> \
-			Module = list_to_atom( \
-				string:to_lower(filename:basename(F, ".dtl")) ++ "_dtl"), \
-			erlydtl:compile(F, Module, [{out_dir, "ebin/"}]) \
-		end, \
-		_ = [Compile(F) || F <- string:tokens("$(1)", " ")], \
-		init:stop()'
-endef
-
-ebin/$(PROJECT).app: $(shell find src -type f -name \*.erl) \
-		$(shell find src -type f -name \*.core) \
-		$(shell find src -type f -name \*.xrl) \
-		$(shell find src -type f -name \*.yrl) \
-		$(shell find templates -type f -name \*.dtl 2>/dev/null)
+ifneq ($(wildcard src/),)
+ebin/$(PROJECT).app::
 	@mkdir -p ebin/
 	@mkdir -p ebin/
-	$(if $(strip $(filter %.erl %.core,$?)), \
-		$(call compile_erl,$(filter %.erl %.core,$?)))
-	$(if $(strip $(filter %.xrl %.yrl,$?)), \
-		$(call compile_xyrl,$(filter %.xrl %.yrl,$?)))
-	$(if $(strip $(filter %.dtl,$?)), \
-		$(call compile_dtl,$(filter %.dtl,$?)))
 
 
-clean:
-	$(gen_verbose) rm -rf ebin/ test/*.beam erl_crash.dump
+ebin/$(PROJECT).app:: $(shell find src -type f -name \*.erl) \
+		$(shell find src -type f -name \*.core)
+	$(if $(strip $?),$(call compile_erl,$?))
 
 
-# Dependencies.
+ebin/$(PROJECT).app:: $(shell find src -type f -name \*.xrl) \
+		$(shell find src -type f -name \*.yrl)
+	$(if $(strip $?),$(call compile_xyrl,$?))
+endif
 
 
-define get_dep
-	@mkdir -p $(DEPS_DIR)
-ifeq (,$(findstring pkg://,$(word 1,$(dep_$(1)))))
-	git clone -n -- $(word 1,$(dep_$(1))) $(DEPS_DIR)/$(1)
-else
-	@if [ ! -f $(PKG_FILE) ]; then $(call get_pkg_file); fi
-	git clone -n -- `awk 'BEGIN { FS = "\t" }; \
-		$$$$1 == "$(subst pkg://,,$(word 1,$(dep_$(1))))" { print $$$$2 }' \
-		$(PKG_FILE)` $(DEPS_DIR)/$(1)
+clean:: clean-app
+
+# Extra targets.
+
+clean-app:
+	$(gen_verbose) rm -rf ebin/
+
+# Copyright (c) 2014, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates
+
+# Core targets.
+
+help::
+	@printf "%s\n" "" \
+		"Bootstrap targets:" \
+		"  bootstrap          Generate a skeleton of an OTP application" \
+		"  bootstrap-lib      Generate a skeleton of an OTP library" \
+		"  bootstrap-rel      Generate the files needed to build a release" \
+		"  new t=TPL n=NAME   Generate a module NAME based on the template TPL" \
+		"  bootstrap-lib      List available templates"
+
+# Bootstrap templates.
+
+bs_appsrc = "{application, $(PROJECT), [" \
+	"	{description, \"\"}," \
+	"	{vsn, \"0.1.0\"}," \
+	"	{modules, []}," \
+	"	{registered, []}," \
+	"	{applications, [" \
+	"		kernel," \
+	"		stdlib" \
+	"	]}," \
+	"	{mod, {$(PROJECT)_app, []}}," \
+	"	{env, []}" \
+	"]}."
+bs_appsrc_lib = "{application, $(PROJECT), [" \
+	"	{description, \"\"}," \
+	"	{vsn, \"0.1.0\"}," \
+	"	{modules, []}," \
+	"	{registered, []}," \
+	"	{applications, [" \
+	"		kernel," \
+	"		stdlib" \
+	"	]}" \
+	"]}."
+bs_Makefile = "PROJECT = $(PROJECT)" \
+	"include erlang.mk"
+bs_app = "-module($(PROJECT)_app)." \
+	"-behaviour(application)." \
+	"" \
+	"-export([start/2])." \
+	"-export([stop/1])." \
+	"" \
+	"start(_Type, _Args) ->" \
+	"	$(PROJECT)_sup:start_link()." \
+	"" \
+	"stop(_State) ->" \
+	"	ok."
+bs_relx_config = "{release, {$(PROJECT)_release, \"1\"}, [$(PROJECT)]}." \
+	"{extended_start_script, true}." \
+	"{sys_config, \"rel/sys.config\"}." \
+	"{vm_args, \"rel/vm.args\"}."
+bs_sys_config = "[" \
+	"]."
+bs_vm_args = "-name $(PROJECT)@127.0.0.1" \
+	"-setcookie $(PROJECT)" \
+	"-heart"
+# Normal templates.
+tpl_supervisor = "-module($(n))." \
+	"-behaviour(supervisor)." \
+	"" \
+	"-export([start_link/0])." \
+	"-export([init/1])." \
+	"" \
+	"start_link() ->" \
+	"	supervisor:start_link({local, ?MODULE}, ?MODULE, [])." \
+	"" \
+	"init([]) ->" \
+	"	Procs = []," \
+	"	{ok, {{one_for_one, 1, 5}, Procs}}."
+tpl_gen_server = "-module($(n))." \
+	"-behaviour(gen_server)." \
+	"" \
+	"%% API." \
+	"-export([start_link/0])." \
+	"" \
+	"%% gen_server." \
+	"-export([init/1])." \
+	"-export([handle_call/3])." \
+	"-export([handle_cast/2])." \
+	"-export([handle_info/2])." \
+	"-export([terminate/2])." \
+	"-export([code_change/3])." \
+	"" \
+	"-record(state, {" \
+	"})." \
+	"" \
+	"%% API." \
+	"" \
+	"-spec start_link() -> {ok, pid()}." \
+	"start_link() ->" \
+	"	gen_server:start_link(?MODULE, [], [])." \
+	"" \
+	"%% gen_server." \
+	"" \
+	"init([]) ->" \
+	"	{ok, \#state{}}." \
+	"" \
+	"handle_call(_Request, _From, State) ->" \
+	"	{reply, ignored, State}." \
+	"" \
+	"handle_cast(_Msg, State) ->" \
+	"	{noreply, State}." \
+	"" \
+	"handle_info(_Info, State) ->" \
+	"	{noreply, State}." \
+	"" \
+	"terminate(_Reason, _State) ->" \
+	"	ok." \
+	"" \
+	"code_change(_OldVsn, State, _Extra) ->" \
+	"	{ok, State}."
+tpl_cowboy_http = "-module($(n))." \
+	"-behaviour(cowboy_http_handler)." \
+	"" \
+	"-export([init/3])." \
+	"-export([handle/2])." \
+	"-export([terminate/3])." \
+	"" \
+	"-record(state, {" \
+	"})." \
+	"" \
+	"init(_, Req, _Opts) ->" \
+	"	{ok, Req, \#state{}}." \
+	"" \
+	"handle(Req, State=\#state{}) ->" \
+	"	{ok, Req2} = cowboy_req:reply(200, Req)," \
+	"	{ok, Req2, State}." \
+	"" \
+	"terminate(_Reason, _Req, _State) ->" \
+	"	ok."
+tpl_cowboy_loop = "-module($(n))." \
+	"-behaviour(cowboy_loop_handler)." \
+	"" \
+	"-export([init/3])." \
+	"-export([info/3])." \
+	"-export([terminate/3])." \
+	"" \
+	"-record(state, {" \
+	"})." \
+	"" \
+	"init(_, Req, _Opts) ->" \
+	"	{loop, Req, \#state{}, 5000, hibernate}." \
+	"" \
+	"info(_Info, Req, State) ->" \
+	"	{loop, Req, State, hibernate}." \
+	"" \
+	"terminate(_Reason, _Req, _State) ->" \
+	"	ok."
+tpl_cowboy_rest = "-module($(n))." \
+	"" \
+	"-export([init/3])." \
+	"-export([content_types_provided/2])." \
+	"-export([get_html/2])." \
+	"" \
+	"init(_, _Req, _Opts) ->" \
+	"	{upgrade, protocol, cowboy_rest}." \
+	"" \
+	"content_types_provided(Req, State) ->" \
+	"	{[{{<<\"text\">>, <<\"html\">>, '_'}, get_html}], Req, State}." \
+	"" \
+	"get_html(Req, State) ->" \
+	"	{<<\"<html><body>This is REST!</body></html>\">>, Req, State}."
+tpl_cowboy_ws = "-module($(n))." \
+	"-behaviour(cowboy_websocket_handler)." \
+	"" \
+	"-export([init/3])." \
+	"-export([websocket_init/3])." \
+	"-export([websocket_handle/3])." \
+	"-export([websocket_info/3])." \
+	"-export([websocket_terminate/3])." \
+	"" \
+	"-record(state, {" \
+	"})." \
+	"" \
+	"init(_, _, _) ->" \
+	"	{upgrade, protocol, cowboy_websocket}." \
+	"" \
+	"websocket_init(_, Req, _Opts) ->" \
+	"	Req2 = cowboy_req:compact(Req)," \
+	"	{ok, Req2, \#state{}}." \
+	"" \
+	"websocket_handle({text, Data}, Req, State) ->" \
+	"	{reply, {text, Data}, Req, State};" \
+	"websocket_handle({binary, Data}, Req, State) ->" \
+	"	{reply, {binary, Data}, Req, State};" \
+	"websocket_handle(_Frame, Req, State) ->" \
+	"	{ok, Req, State}." \
+	"" \
+	"websocket_info(_Info, Req, State) ->" \
+	"	{ok, Req, State}." \
+	"" \
+	"websocket_terminate(_Reason, _Req, _State) ->" \
+	"	ok."
+tpl_ranch_protocol = "-module($(n))." \
+	"-behaviour(ranch_protocol)." \
+	"" \
+	"-export([start_link/4])." \
+	"-export([init/4])." \
+	"" \
+	"-type opts() :: []." \
+	"-export_type([opts/0])." \
+	"" \
+	"-record(state, {" \
+	"	socket :: inet:socket()," \
+	"	transport :: module()" \
+	"})." \
+	"" \
+	"start_link(Ref, Socket, Transport, Opts) ->" \
+	"	Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts])," \
+	"	{ok, Pid}." \
+	"" \
+	"-spec init(ranch:ref(), inet:socket(), module(), opts()) -> ok." \
+	"init(Ref, Socket, Transport, _Opts) ->" \
+	"	ok = ranch:accept_ack(Ref)," \
+	"	loop(\#state{socket=Socket, transport=Transport})." \
+	"" \
+	"loop(State) ->" \
+	"	loop(State)."
+
+# Plugin-specific targets.
+
+bootstrap:
+ifneq ($(wildcard src/),)
+	$(error Error: src/ directory already exists)
 endif
 endif
-	cd $(DEPS_DIR)/$(1) ; git checkout -q $(word 2,$(dep_$(1)))
-endef
+	@printf "%s\n" $(bs_Makefile) > Makefile
+	@mkdir src/
+	@printf "%s\n" $(bs_appsrc) > src/$(PROJECT).app.src
+	@printf "%s\n" $(bs_app) > src/$(PROJECT)_app.erl
+	$(eval n := $(PROJECT)_sup)
+	@printf "%s\n" $(tpl_supervisor) > src/$(PROJECT)_sup.erl
+
+bootstrap-lib:
+ifneq ($(wildcard src/),)
+	$(error Error: src/ directory already exists)
+endif
+	@printf "%s\n" $(bs_Makefile) > Makefile
+	@mkdir src/
+	@printf "%s\n" $(bs_appsrc_lib) > src/$(PROJECT).app.src
 
 
-define dep_target
-$(DEPS_DIR)/$(1):
-	$(call get_dep,$(1))
-endef
+bootstrap-rel:
+ifneq ($(wildcard relx.config),)
+	$(error Error: relx.config already exists)
+endif
+ifneq ($(wildcard rel/),)
+	$(error Error: rel/ directory already exists)
+endif
+	@printf "%s\n" $(bs_relx_config) > relx.config
+	@mkdir rel/
+	@printf "%s\n" $(bs_sys_config) > rel/sys.config
+	@printf "%s\n" $(bs_vm_args) > rel/vm.args
+
+new:
+ifeq ($(wildcard src/),)
+	$(error Error: src/ directory does not exist)
+endif
+ifndef t
+	$(error Usage: make new t=TEMPLATE n=NAME)
+endif
+ifndef tpl_$(t)
+	$(error Unknown template)
+endif
+ifndef n
+	$(error Usage: make new t=TEMPLATE n=NAME)
+endif
+	@printf "%s\n" $(tpl_$(t)) > src/$(n).erl
 
 
-$(foreach dep,$(DEPS),$(eval $(call dep_target,$(dep))))
+list-templates:
+	@echo Available templates: $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES))))
 
 
-deps: $(ALL_DEPS_DIRS)
-	@for dep in $(ALL_DEPS_DIRS) ; do \
-		if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ] ; then \
-			$(MAKE) -C $$dep ; \
-		else \
-			echo "include $(CURDIR)/erlang.mk" | $(MAKE) -f - -C $$dep ; \
-		fi ; \
-	done
+# Copyright (c) 2013-2014, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
 
 
-clean-deps:
-	@for dep in $(ALL_DEPS_DIRS) ; do \
-		if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ] ; then \
-			$(MAKE) -C $$dep clean ; \
-		else \
-			echo "include $(CURDIR)/erlang.mk" | $(MAKE) -f - -C $$dep clean ; \
-		fi ; \
-	done
+.PHONY: build-ct-deps build-ct-suites tests-ct clean-ct distclean-ct
 
 
-# Documentation.
+# Configuration.
 
 
-EDOC_OPTS ?=
+CT_OPTS ?=
+ifneq ($(wildcard test/),)
+	CT_SUITES ?= $(sort $(subst _SUITE.erl,,$(shell find test -type f -name \*_SUITE.erl -exec basename {} \;)))
+else
+	CT_SUITES ?=
+endif
 
 
-docs: clean-docs
-	$(gen_verbose) erl -noshell \
-		-eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), init:stop().'
+TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard
+TEST_ERLC_OPTS += -DTEST=1 -DEXTRA=1 +'{parse_transform, eunit_autoexport}'
 
 
-clean-docs:
-	$(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info
+# Core targets.
 
 
-# Tests.
+tests:: tests-ct
 
 
-$(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep))))
+clean:: clean-ct
 
 
-TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard
-TEST_ERLC_OPTS += -DTEST=1 -DEXTRA=1 +'{parse_transform, eunit_autoexport}'
+distclean:: distclean-ct
 
 
-build-test-deps: $(ALL_TEST_DEPS_DIRS)
-	@for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep; done
+help::
+	@printf "%s\n" "" \
+		"All your common_test suites have their associated targets." \
+		"A suite named http_SUITE can be ran using the ct-http target."
 
 
-build-tests: build-test-deps
-	$(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -o test/ \
-		$(wildcard test/*.erl test/*/*.erl) -pa ebin/
+# Plugin-specific targets.
+
+ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS))
 
 
-CT_OPTS ?=
 CT_RUN = ct_run \
 CT_RUN = ct_run \
 	-no_auto_compile \
 	-no_auto_compile \
 	-noshell \
 	-noshell \
@@ -232,35 +555,49 @@ CT_RUN = ct_run \
 	-dir test \
 	-dir test \
 	-logdir logs
 	-logdir logs
 
 
-ifneq ($(wildcard test/),)
-	CT_SUITES ?= $(sort $(subst _SUITE.erl,,$(shell find test -type f -name \*_SUITE.erl -exec basename {} \;)))
-else
-	CT_SUITES ?=
-endif
+$(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep))))
+
+build-ct-deps: $(ALL_TEST_DEPS_DIRS)
+	@for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep; done
+
+build-ct-suites: build-ct-deps
+	$(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -o test/ \
+		$(wildcard test/*.erl test/*/*.erl) -pa ebin/
 
 
-define test_target
-test_$(1): ERLC_OPTS = $(TEST_ERLC_OPTS)
-test_$(1): clean deps app build-tests
+tests-ct: ERLC_OPTS = $(TEST_ERLC_OPTS)
+tests-ct: clean deps app build-ct-suites
 	@if [ -d "test" ] ; \
 	@if [ -d "test" ] ; \
 	then \
 	then \
 		mkdir -p logs/ ; \
 		mkdir -p logs/ ; \
-		$(CT_RUN) -suite $(addsuffix _SUITE,$(1)) $(CT_OPTS) ; \
+		$(CT_RUN) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) ; \
 	fi
 	fi
 	$(gen_verbose) rm -f test/*.beam
 	$(gen_verbose) rm -f test/*.beam
-endef
-
-$(foreach test,$(CT_SUITES),$(eval $(call test_target,$(test))))
 
 
-tests: ERLC_OPTS = $(TEST_ERLC_OPTS)
-tests: clean deps app build-tests
+define ct_suite_target
+ct-$(1): ERLC_OPTS = $(TEST_ERLC_OPTS)
+ct-$(1): clean deps app build-ct-suites
 	@if [ -d "test" ] ; \
 	@if [ -d "test" ] ; \
 	then \
 	then \
 		mkdir -p logs/ ; \
 		mkdir -p logs/ ; \
-		$(CT_RUN) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) ; \
+		$(CT_RUN) -suite $(addsuffix _SUITE,$(1)) $(CT_OPTS) ; \
 	fi
 	fi
 	$(gen_verbose) rm -f test/*.beam
 	$(gen_verbose) rm -f test/*.beam
+endef
 
 
-# Dialyzer.
+$(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test))))
+
+clean-ct:
+	$(gen_verbose) rm -rf test/*.beam
+
+distclean-ct:
+	$(gen_verbose) rm -rf logs/
+
+# Copyright (c) 2013-2014, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: plt distclean-plt dialyze
+
+# Configuration.
 
 
 DIALYZER_PLT ?= $(CURDIR)/.$(PROJECT).plt
 DIALYZER_PLT ?= $(CURDIR)/.$(PROJECT).plt
 export DIALYZER_PLT
 export DIALYZER_PLT
@@ -269,32 +606,115 @@ PLT_APPS ?=
 DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions \
 DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions \
 	-Wunmatched_returns # -Wunderspecs
 	-Wunmatched_returns # -Wunderspecs
 
 
-build-plt: deps app
+# Core targets.
+
+distclean:: distclean-plt
+
+help::
+	@printf "%s\n" "" \
+		"Dialyzer targets:" \
+		"  plt         Build a PLT file for this project" \
+		"  dialyze     Analyze the project using Dialyzer"
+
+# Plugin-specific targets.
+
+plt: deps app
 	@dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(ALL_DEPS_DIRS)
 	@dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(ALL_DEPS_DIRS)
 
 
+distclean-plt:
+	$(gen_verbose) rm -f $(DIALYZER_PLT)
+
 dialyze:
 dialyze:
 	@dialyzer --no_native --src -r src $(DIALYZER_OPTS)
 	@dialyzer --no_native --src -r src $(DIALYZER_OPTS)
 
 
-# Packages.
+# Copyright (c) 2013-2014, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
 
 
-$(PKG_FILE):
-	@$(call get_pkg_file)
+# Verbosity.
 
 
-pkg-list: $(PKG_FILE)
-	@cat $(PKG_FILE) | awk 'BEGIN { FS = "\t" }; { print \
-		"Name:\t\t" $$1 "\n" \
-		"Repository:\t" $$2 "\n" \
-		"Website:\t" $$3 "\n" \
-		"Description:\t" $$4 "\n" }'
+dtl_verbose_0 = @echo " DTL   " $(filter %.dtl,$(?F));
+dtl_verbose = $(dtl_verbose_$(V))
+
+# Core targets.
+
+define compile_erlydtl
+	$(dtl_verbose) erl -noshell -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/ -eval ' \
+		Compile = fun(F) -> \
+			Module = list_to_atom( \
+				string:to_lower(filename:basename(F, ".dtl")) ++ "_dtl"), \
+			erlydtl:compile(F, Module, [{out_dir, "ebin/"}]) \
+		end, \
+		_ = [Compile(F) || F <- string:tokens("$(1)", " ")], \
+		init:stop()'
+endef
+
+ifneq ($(wildcard src/),)
+ebin/$(PROJECT).app:: $(shell find templates -type f -name \*.dtl 2>/dev/null)
+	$(if $(strip $?),$(call compile_erlydtl,$?))
+endif
+
+# Copyright (c) 2013-2014, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: distclean-edoc
+
+# Configuration.
+
+EDOC_OPTS ?=
+
+# Core targets.
+
+docs:: distclean-edoc
+	$(gen_verbose) erl -noshell \
+		-eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), init:stop().'
+
+distclean:: distclean-edoc
+
+# Plugin-specific targets.
+
+distclean-edoc:
+	$(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info
+
+# Copyright (c) 2013-2014, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: distclean-rel
+
+# Configuration.
+
+RELX_CONFIG ?= $(CURDIR)/relx.config
+
+ifneq ($(wildcard $(RELX_CONFIG)),)
+
+RELX ?= $(CURDIR)/relx
+export RELX
+
+RELX_URL ?= https://github.com/erlware/relx/releases/download/v1.0.2/relx
+RELX_OPTS ?=
+RELX_OUTPUT_DIR ?= _rel
+
+ifeq ($(firstword $(RELX_OPTS)),-o)
+	RELX_OUTPUT_DIR = $(word 2,$(RELX_OPTS))
+endif
+
+# Core targets.
+
+rel:: distclean-rel $(RELX)
+	@$(RELX) -c $(RELX_CONFIG) $(RELX_OPTS)
+
+distclean:: distclean-rel
+
+# Plugin-specific targets.
+
+define relx_fetch
+	$(call core_http_get,$(RELX),$(RELX_URL))
+	chmod +x $(RELX)
+endef
+
+$(RELX):
+	@$(call relx_fetch)
+
+distclean-rel:
+	$(gen_verbose) rm -rf $(RELX_OUTPUT_DIR)
 
 
-ifdef q
-pkg-search: $(PKG_FILE)
-	@cat $(PKG_FILE) | grep -i ${q} | awk 'BEGIN { FS = "\t" }; { print \
-		"Name:\t\t" $$1 "\n" \
-		"Repository:\t" $$2 "\n" \
-		"Website:\t" $$3 "\n" \
-		"Description:\t" $$4 "\n" }'
-else
-pkg-search:
-	@echo "Usage: make pkg-search q=STRING"
 endif
 endif