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.

While there's a trend to automate this kind of things, the most tools used for it are task runners, written in PHP or JavaScript (Grunt, Gulp, ..). I think that's a good thing, but the "traditional" build tools have an additional concept, that is missing in many (not all) of this tools: Dependency management.

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 a, then both of them need to be recompiled. This is not the case for c, which does not depend on a (and not on 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 call 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.

Here, 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.css as a prerequisite:

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 styles.css. 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 (styles.scss) 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 styles.css. As styles.scss 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 $f is the name of a single modified file).

As you probably see, the goal here is to run php -l on every modified file.

But wait...

...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 wrapper for $(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 ($(TMP_DIR)/phplint) 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.

Conclusion

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?

Author: Claudio Kressibucher
Tags: PHP, Tools