Makefiles for PHP Projects
GNU Make is a proven and well known build tool. For PHP projects though, it is not often used. First, there's no "compile" task which requires a build tool. Maybe it's also considered to be a Linux only tool, and most PHP projects strive to be platform independent. If you're running your applications on Unix systems only, then consider using it for your next project -- you might be surprised how useful this tool may be! Here's what I like the most about it:
- It uses simple shell commands (and you can do a lot with Unix tools!)
- It lets you define a proper dependency graph, and only "builds" what needs to be rebuilt. Thus it can save a lot of time.
The goal of this article is to show valid use cases where Makefiles could be used in PHP projects. It's not a good tutorial to learn the syntax and to get started. For this, see the manual or let google help you :-)
If you need an example of a Makefile which could be used in PHP projects, see this simple project skeleton which I recently published on github.
The Use Case for a build tool
Developing applications with PHP has changed a lot in the last years. There's much more "tooling" involved. You may want to
- run syntax checkers
- do static code analysis
- run different kinds of tests
- build assets
- build documentation
- start a development server
Many of those tasks are performed by invoking a command line script
with its specific arguments. That's a lot of typing, and often
you may need to look up the
--help page to remind you what
those arguments are. If you do it often enough, you feel
the need for a "wrapper" around all this single tools which
knows how to invoke them for your specific project.
About dependencies in build processes
Compiling source code of large code bases to machine code
takes some time. That's why one goal of build tools was
to only rebuild what has changed. To figure this out, it
may need to know the dependencies of each component.
That is, if
a has been modified, and
b depends on
then both of them need to be recompiled. This is not
the case for
c, which does not depend on
a (and not
b). With make and other more traditional build tools,
tracking this dependencies are at the very core of the tool.
Dependencies in PHP projects
Here are some obvious examples, where you could save time using a proper build tool, which respects dependencies:
- lint with
php -l: You only need to check files that have changed since the last run
- run a particular test-suite, instead of all of the tests (without remembering how exactly you named the suite for the component you're currently working on!)
Another advantage is that your tasks can be simpler, because you don't need
to check prerequisites yourself. Just specify which file needs to exists to
run your task. The build tool takes care, and creates the file if it does
not yet exist. Because of this, there's often no need to remember many
different tasks, but to just type the "top level command" (or "target", more
precisely). An example is
make test. You don't need to remember to first
composer install, if you have defined
vendor/autoload.php as a
dependency of your
test target. And for the integration test, you may
just need to say that you need a running server as a dependency.
So, how does make know about your task's dependencies?
You have to declare them. For each single target. An example:
styles.css: styles.scss sass styles.scss styles.css
First, some terminology:
- The whole thing is called a rule.
- The left part (before the colon) of the first line is called a target. It's the file which should be built by the rule.
- The right hand side of the first line is a list of prerequisites (the target depends on those things).
- All additional lines (indented by a TAB), are commands.
styles.css is the target. This is the file which should be created
by the task.
styles.scss is the prerequisite, the dependency of
styles.css. The next line describes how to create the target.
If you now want to minify the css file, you have to define
styles.min.css: styles.css some-minifier-tool styles.css > styles.min.css
This declares a dependency graph
styles.min.css > styles.css > styles.scss.
If you run
make styles.min.css, make checks for that target (the
minified file), and sees that it is missing. So it decides to build it. How?
It analyses the rule. Before looking at the actual command(s), it checks the
prerequisites, and -- in this case -- it sees that we need
Let's assume this file exists. But we recently modified
styles.scss, the actual
source file. Would it be OK to build the minified css file directly from
the css file? No, our modification on the source file (
would not make it into our final minified file!
And that's why make looks further and detects that
the timestamp of
styles.scss is newer than the timestamp of
exists and has no rule with further dependencies, make takes it
as a source file (i.e., it does not try to build it somehow).
Then it builds the other two targets, first the css file, and then
the minified version from that css file.
To summarize, before running the commands of a rule, make compares the timestamp of each prerequisite with the timestamp of the target (if it exists), and only (re)builds the target if it either does not exist, or is older than the newest of the prerequisites.
So it only works with files as dependencies?
Yes. That's how make works, and it's what makes it possible to work without an additional database. But for our PHP projects, it would often be useful to define tasks which do not generate a file, but also have dependencies. As an example, take the linter:
.PHONY: phplint phplint: $(srcs) $(foreach f,$?,php -l $(f);)
First, there are some new things here:
PHONY: This is to tell make not to consider the name phplint to be a file. That is, if a file named phplint would exist, it should run the task anyway.
$(srcs): This is a variable. We assume that it contains a list of all of our php source files (this variable needs to be defined somewhere else).
$?This is a special variable which holds all of the modified files in the list of prerequisites.
foreach: It expands each of the modified files to
php -l $f(where
$fis the name of a single modified file).
As you probably see, the goal here is to run
php -l on every modified file.
...does that work? Actually, no! make has no way to know what "modified" means here, as we have no target file to compare against. This is exactly the limitation we have due to the file system approach I explained above. So what's the solution then? We can create a "dummy" file to store the timestamp of the last time we ran the linter:
# $(TMP_DIR)/phplint is our "dummy file". .PHONY: phplint phplint: $(TMP_DIR)/phplint $(TMP_DIR)/phplint: $(src) @echo lint source files... @$(foreach f,$?,php -l $(f);) touch $@
We have defined two rules here:
phplint is the main target, which we want to
invoke using the command
make phplint on the command line. It is just a
$(TMP_DIR)/phplint, which is our "dummy file".
The actual work is done by the
second rule, which runs the linter, and then generates (or updates) this dummy file.
(The reason we defined the main target
phplint is to have a
simpler way to invoke it on the command line).
The second rule does the same as before, but additionally
touch-es the dummy file.
Now make has a file -- with a timestamp -- to compare our sources against!
If we run
make phplint again, make will see that the target (
is newer than all of the prerequisites (our source files) and do nothing.
While this seems complicated at the first, it's not too complicated after you
saw (and maybe even wrote) that pattern a few times.
And then: simple shell commands
After you've read a lot about dependencies, here's another thing I love
about make: It uses the shell to perform tasks. Of course, there's
also the risk to encounter problems when the Makefile is used on a
different platform. The big advantage is in my opinion,
that many tasks can be expressed concisely and
simple. While there's a plugin for gulp just to let you rename a file,
with make you can just use
mv src dest as normal.
You can also use pipes to combine
all this single Unix commands.
After getting used to the syntax and the few special variables used in GNU make, I really fell in love with this tool (as far as that's possible :-) ).
However, I would probably not use it in larger projects, which have to be cross-platform. The problem is, that even if you think it would work on another platform, you have probably not tested it on every single platform. In this case I would look for another tool, which also works strictly with dependencies. One tool which looks interesting is doit, a python tool. However, I have not used it yet, so this is just a guess...
As a reminder, if you want to try it out yourself, have a look at my example Makefile on github.
So, what do you think, is it a good idea to use Makefiles for PHP projects? What tools do you use, and why?