Browse Source

Add dependency tracking (makedep)

This is a very large change of a central part of Erlang.mk.
I will admit that I am not quite confident on that one. If
you do have issues following this change, please open a
ticket and I will look at it immediately.

At this point, it works for me, but I wouldn't be surprised
to hear about a few minor issues.

This commit introduces a dependency file $(PROJECT).d which
contains Makefile rules between Erlang source files and
headers, behaviors and parse_transforms. This allows us
to rebuild only the files that are needed.

The $(PROJECT).d is generated automatically when missing,
and when any of the files change.

It is possible to hook before and after this generation,
by defining a $(PROJECT).d:: rule. This allows users to
generate Erlang files which are then compiled by Erlang.mk
automatically (and to track their dependencies, of course).

Here goes nothing...
Loïc Hoguin 10 years ago
parent
commit
bdfcb324f7
2 changed files with 380 additions and 42 deletions
  1. 113 42
      core/erlc.mk
  2. 267 0
      doc/src/guide/app.asciidoc

+ 113 - 42
core/erlc.mk

@@ -24,6 +24,9 @@ app_verbose = $(app_verbose_$(V))
 appsrc_verbose_0 = @echo " APP   " $(PROJECT).app.src;
 appsrc_verbose_0 = @echo " APP   " $(PROJECT).app.src;
 appsrc_verbose = $(appsrc_verbose_$(V))
 appsrc_verbose = $(appsrc_verbose_$(V))
 
 
+makedep_verbose_0 = @echo " DEPEND" $(PROJECT).d;
+makedep_verbose = $(makedep_verbose_$(V))
+
 erlc_verbose_0 = @echo " ERLC  " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\
 erlc_verbose_0 = @echo " ERLC  " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\
 	$(filter %.erl %.core,$(?F)));
 	$(filter %.erl %.core,$(?F)));
 erlc_verbose = $(erlc_verbose_$(V))
 erlc_verbose = $(erlc_verbose_$(V))
@@ -40,9 +43,11 @@ mib_verbose = $(mib_verbose_$(V))
 # Targets.
 # Targets.
 
 
 ifeq ($(wildcard ebin/test),)
 ifeq ($(wildcard ebin/test),)
-app:: app-build
+app::
+	$(verbose) $(MAKE) --no-print-directory app-build
 else
 else
-app:: clean app-build
+app:: clean
+	$(verbose) $(MAKE) --no-print-directory app-build
 endif
 endif
 
 
 ifeq ($(wildcard src/$(PROJECT)_app.erl),)
 ifeq ($(wildcard src/$(PROJECT)_app.erl),)
@@ -70,7 +75,7 @@ define app_file
 endef
 endef
 endif
 endif
 
 
-app-build: erlc-include ebin/$(PROJECT).app
+app-build: ebin/$(PROJECT).app
 	$(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null || true))
 	$(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null || true))
 	$(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename $(shell find ebin -type f -name *.beam))))))
 	$(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename $(shell find ebin -type f -name *.beam))))))
 ifeq ($(wildcard src/$(PROJECT).app.src),)
 ifeq ($(wildcard src/$(PROJECT).app.src),)
@@ -87,61 +92,127 @@ else
 		> ebin/$(PROJECT).app
 		> ebin/$(PROJECT).app
 endif
 endif
 
 
-erlc-include:
-	- $(verbose) if [ -d ebin/ ]; then \
-		find include/ src/ -type f -name \*.hrl -newer ebin -exec touch $(shell find src/ -type f -name "*.erl") \; 2>/dev/null || printf ''; \
-	fi
+# Source files.
 
 
-define compile_erl
-	$(erlc_verbose) erlc -v $(if $(IS_DEP),$(filter-out -Werror,$(ERLC_OPTS)),$(ERLC_OPTS)) -o ebin/ \
-		-pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),\
-		$(COMPILE_FIRST_PATHS) $(1))
-endef
+ifneq ($(wildcard src/),)
+ERL_FILES = $(sort $(call core_find,src/,*.erl))
+CORE_FILES = $(sort $(call core_find,src/,*.core))
 
 
-define compile_xyrl
-	$(xyrl_verbose) erlc -v -o ebin/ $(1)
-	$(xyrl_verbose) erlc $(ERLC_OPTS) -o ebin/ ebin/*.erl
-	$(verbose) rm ebin/*.erl
-endef
+# ASN.1 files.
 
 
-define compile_asn1
-	$(asn1_verbose) erlc -v -I include/ -o ebin/ $(1)
-	$(verbose) mv ebin/*.hrl include/
-	$(verbose) mv ebin/*.asn1db include/
-	$(verbose) rm ebin/*.erl
-endef
+ifneq ($(wildcard asn1/),)
+ASN1_FILES = $(sort $(call core_find,asn1/,*.asn1))
+ERL_FILES += $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES))))
 
 
-define compile_mib
-	$(mib_verbose) erlc -v $(ERLC_MIB_OPTS) -o priv/mibs/ \
-		-I priv/mibs/ $(COMPILE_MIB_FIRST_PATHS) $(1)
-	$(mib_verbose) erlc -o include/ -- priv/mibs/*.bin
+define compile_asn1
+	$(verbose) mkdir -p include/
+	$(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(1)
+	$(verbose) mv asn1/*.erl src/
+	$(verbose) mv asn1/*.hrl include/
+	$(verbose) mv asn1/*.asn1db include/
 endef
 endef
 
 
-ifneq ($(wildcard src/),)
-ebin/$(PROJECT).app::
-	$(verbose) mkdir -p ebin/
-
-ifneq ($(wildcard asn1/),)
-ebin/$(PROJECT).app:: $(sort $(call core_find,asn1/,*.asn1))
-	$(verbose) mkdir -p include
+$(PROJECT).d:: $(ASN1_FILES)
 	$(if $(strip $?),$(call compile_asn1,$?))
 	$(if $(strip $?),$(call compile_asn1,$?))
 endif
 endif
 
 
+# SNMP MIB files.
+
 ifneq ($(wildcard mibs/),)
 ifneq ($(wildcard mibs/),)
-ebin/$(PROJECT).app:: $(sort $(call core_find,mibs/,*.mib))
-	$(verbose) mkdir -p priv/mibs/ include
-	$(if $(strip $?),$(call compile_mib,$?))
+MIB_FILES = $(sort $(call core_find,mibs/,*.mib))
+
+$(PROJECT).d:: $(MIB_FILES)
+	$(verbose) mkdir -p include/ priv/mibs/
+	$(mib_verbose) erlc -v $(ERLC_MIB_OPTS) -o priv/mibs/ -I priv/mibs/ $(COMPILE_MIB_FIRST_PATHS) $(MIB_FILES)
+	$(mib_verbose) erlc -o include/ -- priv/mibs/*.bin
 endif
 endif
 
 
-ebin/$(PROJECT).app:: $(sort $(call core_find,src/,*.erl *.core))
+# Leex and Yecc files.
+
+XRL_FILES = $(sort $(call core_find,src/,*.xrl))
+XRL_ERL_FILES = $(addprefix src/,$(patsubst %.xrl,%.erl,$(notdir $(XRL_FILES))))
+ERL_FILES += $(XRL_ERL_FILES)
+
+YRL_FILES = $(sort $(call core_find,src/,*.yrl))
+YRL_ERL_FILES = $(addprefix src/,$(patsubst %.yrl,%.erl,$(notdir $(YRL_FILES))))
+ERL_FILES += $(YRL_ERL_FILES)
+
+$(PROJECT).d:: $(XRL_FILES) $(YRL_FILES)
+	$(if $(strip $?),$(xyrl_verbose) erlc -v -o src/ $?)
+
+# Erlang and Core Erlang files.
+
+define makedep.erl
+	ErlFiles = lists:usort(string:tokens("$(ERL_FILES)", " ")),
+	Modules = [{filename:basename(F, ".erl"), F} || F <- ErlFiles],
+	Add = fun (Dep, Acc) ->
+		case lists:keyfind(atom_to_list(Dep), 1, Modules) of
+			{_, DepFile} -> [DepFile|Acc];
+			false -> Acc
+		end
+	end,
+	AddHd = fun (Dep, Acc) ->
+		case {Dep, lists:keymember(Dep, 2, Modules)} of
+			{"src/" ++ _, false} -> [Dep|Acc];
+			{"include/" ++ _, false} -> [Dep|Acc];
+			_ -> Acc
+		end
+	end,
+	CompileFirst = fun (Deps) ->
+		First0 = [case filename:extension(D) of
+			".erl" -> filename:basename(D, ".erl");
+			_ -> []
+		end || D <- Deps],
+		case lists:usort(First0) of
+			[] -> [];
+			[[]] -> [];
+			First -> ["COMPILE_FIRST +=", [[" ", F] || F <- First], "\n"]
+		end
+	end,
+	Depend = [begin
+		case epp:parse_file(F, [{includes, ["include/"]}]) of
+			{ok, Forms} ->
+				Deps = lists:usort(lists:foldl(fun
+					({attribute, _, behavior, Dep}, Acc) -> Add(Dep, Acc);
+					({attribute, _, behaviour, Dep}, Acc) -> Add(Dep, Acc);
+					({attribute, _, compile, {parse_transform, Dep}}, Acc) -> Add(Dep, Acc);
+					({attribute, _, file, {Dep, _}}, Acc) -> AddHd(Dep, Acc);
+					(_, Acc) -> Acc
+				end, [], Forms)),
+				[F, ":", [[" ", D] || D <- Deps], "\n", CompileFirst(Deps)];
+			{error, enoent} ->
+				[]
+		end
+	end || F <- ErlFiles],
+	ok = file:write_file("$(1)", Depend),
+	halt()
+endef
+
+$(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl)
+	$(makedep_verbose) $(call erlang,$(call makedep.erl,$@))
+
+-include $(PROJECT).d
+
+ebin/$(PROJECT).app:: $(PROJECT).d
+	$(verbose) mkdir -p ebin/
+
+define compile_erl
+	$(erlc_verbose) erlc -v $(if $(IS_DEP),$(filter-out -Werror,$(ERLC_OPTS)),$(ERLC_OPTS)) -o ebin/ \
+		-pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),$(COMPILE_FIRST_PATHS) $(1))
+endef
+
+ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES)
 	$(if $(strip $?),$(call compile_erl,$?))
 	$(if $(strip $?),$(call compile_erl,$?))
 
 
-ebin/$(PROJECT).app:: $(sort $(call core_find,src/,*.xrl *.yrl))
-	$(if $(strip $?),$(call compile_xyrl,$?))
+$(sort $(ERL_FILES) $(CORE_FILES)):
+	@touch $@
 endif
 endif
 
 
 clean:: clean-app
 clean:: clean-app
 
 
 clean-app:
 clean-app:
-	$(gen_verbose) rm -rf ebin/ priv/mibs/ \
-		$(addprefix include/,$(addsuffix .hrl,$(notdir $(basename $(call core_find,mibs/,*.mib)))))
+	$(gen_verbose) rm -rf $(PROJECT).d ebin/ priv/mibs/ $(XRL_ERL_FILES) $(YRL_ERL_FILES) \
+		$(addprefix include/,$(patsubst %.mib,.hrl,$(notdir $(MIB_FILES)))) \
+		$(addprefix include/,$(patsubst %.asn1,.hrl,$(notdir $(ASN1_FILES)))) \
+		$(addprefix include/,$(patsubst %.asn1,.asn1db,$(notdir $(ASN1_FILES)))) \
+		$(addprefix src/,$(patsubst %.erl,.asn1db,$(notdir $(ASN1_FILES))))

+ 267 - 0
doc/src/guide/app.asciidoc

@@ -0,0 +1,267 @@
+== Building
+
+Erlang.mk can do a lot of things, but it is, first and
+foremost, a build tool. In this chapter we will cover
+the basics of building a project with Erlang.mk.
+
+For most of this chapter, we will assume that you are
+using a project link:getting_started.asciidoc[generated by Erlang.mk].
+
+=== How to build
+
+To build a project, all you have to do is type `make`:
+
+[source,bash]
+$ make
+
+It will work regardless of your project: OTP applications,
+library applications, NIFs, port drivers or even releases.
+Erlang.mk also automatically downloads and compiles the
+dependencies for your project.
+
+All this is possible thanks to a combination of configuration
+and conventions. Most of the conventions come from Erlang/OTP
+itself so any seasoned Erlang developers should feel right at
+home.
+
+=== What to build
+
+Erlang.mk gives you control over three steps of the build
+process, allowing you to do a partial build if needed.
+
+A build has three phases: first any dependency is fetched
+and built, then the project itself is built and finally a
+release may be generated when applicable. A release is only
+generated for projects specifically configured to do so.
+
+Erlang.mk handles those three phases automatically when you
+type `make`. But sometimes you just want to repeat one or
+two of them.
+
+The commands detailed in this section are most useful after
+you have a successful build as they allow you to quickly
+redo a step instead of going through everything. This is
+especially useful for large projects or projects that end
+up generating releases.
+
+==== Application
+
+You can build your application specifically, without
+looking at handling dependencies or generating a release,
+by running the following command:
+
+[source,bash]
+$ make app
+
+This command is very useful if you have a lot of dependencies
+and develop on a machine with slow file access, like the
+Raspberry Pi and many other embedded devices.
+
+Note that this command may fail if a required dependency
+is missing.
+
+==== Dependencies
+
+You can build all dependencies, and nothing else, by
+running the following command:
+
+[source,bash]
+$ make deps
+
+This will fetch and compile all dependencies and their
+dependencies, recursively.
+
+link:deps.asciidoc[Packages and dependencies] are covered
+in the next chapter.
+
+==== Release
+
+You can generate the release, skipping the steps for building
+the application and dependencies, by running the following
+command:
+
+[source,bash]
+$ make rel
+
+This command can be useful if nothing changed except the
+release configuration files.
+
+Consult the link:relx.asciidoc[Releases] chapter for more
+information about what releases are and how they are generated.
+
+Note that this command may fail if a required dependency
+is missing.
+
+=== Application resource file
+
+When building your application, Erlang.mk will generate the
+http://www.erlang.org/doc/man/app.html[application resource file].
+This file is mandatory for all Erlang applications and is
+found in 'ebin/$(PROJECT).app'.
+
+`PROJECT` is a variable defined in your Makefile and taken
+from the name of the directory when Erlang.mk bootstraps
+your project.
+
+Erlang.mk can build the 'ebin/$(PROJECT).app' in two different
+ways: from the configuration found in the Makefile, or from
+the 'src/$(PROJECT).app.src' file.
+
+==== Application configuration
+
+Erlang.mk automatically fills the `PROJECT` variable when
+bootstrapping a new project, but everything else is up to
+you. None of the values are required to build your project,
+although it is recommended to fill everything relevant to
+your situation.
+
+`PROJECT`::
+	The name of the OTP application or library.
+`PROJECT_DESCRIPTION`::
+	Short description of the project.
+`PROJECT_VERSION`::
+	Current version of the project.
+`PROJECT_REGISTERED`::
+	List of the names of all registered processes.
+`OTP_DEPS`::
+	List of Erlang/OTP applications this project depends on,
+	excluding `erts`, `kernel` and `stdlib`.
+`DEPS`::
+	List of applications this project depends on that need
+	to be fetched by Erlang.mk.
+
+There's no need for quotes or anything. The relevant part of
+the Cowboy Makefile follows, if you need an example:
+
+[source,make]
+----
+PROJECT = cowboy
+PROJECT_DESCRIPTION = Small, fast, modular HTTP server.
+PROJECT_VERSION = 2.0.0-pre.2
+PROJECT_REGISTERED = cowboy_clock
+
+OTP_DEPS = crypto
+DEPS = cowlib ranch
+----
+
+Any space before and after the value is dropped.
+
+link:deps.asciidoc[Dependencies] are covered in details in
+the next chapter.
+
+==== Legacy method
+
+The 'src/$(PROJECT).app.src' file is a legacy method of
+building Erlang applications. It was introduced by the original
+`rebar` build tool, of which Erlang.mk owes a great deal as it
+is its main inspiration.
+
+The '.app.src' file serves as a template to generate the '.app'
+file. Erlang.mk will take it, fill in the `modules` value
+dynamically, and save the result in 'ebin/$(PROJECT).app'.
+
+When using this method, Erlang.mk cannot fill the `applications`
+key from dependencies automatically, which means you need to
+add them to Erlang.mk and to the '.app.src' at the same time,
+duplicating the work.
+
+=== File formats
+
+Erlang.mk supports a variety of different source file formats.
+The following formats are supported natively:
+
+[cols="<,3*^",options="header"]
+|===
+| Extension | Location | Description        | Output
+| .erl      | src/     | Erlang source      | ebin/*.beam
+| .core     | src/     | Core Erlang source | ebin/*.beam
+| .xrl      | src/     | Leex source        | src/*.erl
+| .yrl      | src/     | Yecc source        | src/*.erl
+| .asn1     | asn1/    | ASN.1 files        | include/*.hrl include/*.asn1db src/*.erl
+| .mib      | mibs/    | SNMP MIB files     | include/*.hrl priv/mibs/*.bin
+|===
+
+Files are always searched recursively.
+
+The build is ordered, so that files that generate Erlang source
+files are run before, and the resulting Erlang source files are
+then built normally.
+
+In addition, Erlang.mk keeps track of header files (`.hrl`)
+as described at the end of this chapter. It can also compile
+C code, as described in the link:ports.asciidoc[NIFs and port drivers]
+chapter.
+
+Erlang.mk also comes with plugins for the following formats:
+
+[cols="<,3*^",options="header"]
+|===
+| Extension | Location   | Description      | Output
+| .dtl      | templates/ | Django templates | ebin/*.beam
+| .proto    | src/       | Protocol buffers | ebin/*.beam
+|===
+
+=== Cold and hot builds
+
+The first time you run `make`, Erlang.mk will build everything.
+
+The second time you run `make`, and all subsequent times, Erlang.mk
+will only rebuild what changed. Erlang.mk has been optimized for
+this use case, as it is the most common during development.
+
+Erlang.mk figures out what changed by using the dependency tracking
+feature of Make. Make automatically rebuilds a target if one of its
+dependency has changed (for example if a header file has changed,
+all the source files that include it will be rebuilt), and Erlang.mk
+leverages this feature to cut down on rebuild times.
+
+Note that this applies only to building; some other features of
+Erlang.mk will run every time they are called regardless of files
+changed.
+
+=== Dependency tracking
+
+NOTE: This section is about the dependency tracking between files
+inside your project, not application dependencies.
+
+Erlang.mk keeps track of the dependencies between the different
+files in your project. This information is kept in the '$(PROJECT).d'
+file in your directory. It is generated if missing, and will be
+generated again after every file change, by default.
+
+Dependency tracking is what allows Erlang.mk to know when to
+rebuild Erlang files when header files, behaviors or parse
+transforms have changed. Erlang.mk also automatically keeps
+track of which files should be compiled first, for example
+when you have behaviors used by other modules in your project.
+
+=== Cleaning
+
+Building typically involves creating a lot of new files. Some
+are reused in rebuilds, some are simply replaced. All can be
+removed safely.
+
+Erlang.mk provides two commands to remove them: `clean` and
+`distclean`. `clean` removes all the intermediate files that
+were created as a result of building, including the BEAM files,
+the dependency tracking file and the generated documentation.
+`distclean` removes these and more, including the downloaded
+dependencies, Dialyzer's PLT file and the generated release,
+putting your directory back to the state it was before you
+started working on it.
+
+To clean:
+
+[source,bash]
+$ make clean
+
+Or distclean:
+
+[source,bash]
+$ make distclean
+
+That is the question.
+
+Note that Erlang.mk will automatically clean some files as
+part of other targets, but it will never run `distclean` if
+you don't explicitly use it.