GNU Makefiles - best practices

This is a summary of best practices when using Makefiles. This post is primarily intended for my students building tools in our lab. Much of this post is based on the Make bestpractices that I learned during my stint at Sun Microsystems, updated to reflect GNU Make since it is the most common make flavour at this point by a significant margin.

Contents

1. (GNU) Makefiles best practices 1. Automatic variables are your friends.

2. Be aware of partial targets

3. Use sentinels

4. Explicitly thread any files used in later stages in dependencies.

5. Thread dependencies, with entry targets treated as leaves

6. Specify the targets as Make variables, which are translated to Makefile pattern rules

7. Avoid phony target names that are also common directories

8. Timestamp dependency rules

9. Make it a practice to explicitly use .PHONY

10. Remove implicit rules

11. Disable automatic removal of intermediate files

12. Idempotent on repeated invocations

13. Clean up after yourself

14. Never save an artifact in a repository that can be generated by make

15. Avoid in-place edits of any files.

16. Silent makes

17. Recommendation from here

18. Ensure that a failure in a pipe stage kills the build

19. Exporting a variable to the environment for specific targets

2. Artifacts

Important: Pyodide takes time to initialize. Initialization completion is indicated by a red border around Run all button.

Run all

(GNU) Makefiles best practices

Automatic variables are your friends.

• $@ is the name of the target. Make it a habit to touch $@ at the end of the make rule for timestamps, or, if it is the name of a useful target, generate .$@ or $@~ if $@ includes a path, and finally mv .$@ $@ as the last step.

• $* is the matched portion represented by % in Makefile targets. Confusingly, $% is used for another purpose.

lt; is the name of the first prerequisite.

• $^ is the list of all prerequisites.

• $? is the list of all prerequisites that are newer than the target.

• Attaching parentheses and D and F respectively gives you the directory and file parts of these variables. For example, $(@D) is the directory part of the target, while $(@F) is the file part.

Be aware of partial targets

Do not rely on files that get created with content as the target name.

Essentially, if you rely on files that are produced by a process and the process can be interrupted while writing to the file, the file will be corrupt, and make will not know about it in the next run.

For example, do not do this.

Instead, make it a two-step process.

Note: the use of @ at the beginning of the command line to hide command invocation echo here.

Or even better:

See also: .DELETE_ON_ERROR

Use sentinels

Use sentinels when your recipe creates multiple files that are required elsewhere in the build

Here, the extract target generates multiple files from the tar.gz file. If you depend on one of the files generated as the target, you run the risk of extract being interrupted after the specified file is created but before others are created. Similarly, this rule would never run if some other file generated by extract is older than project.tar.gz. Hence, always rely on sentinel files when there are multiple files created by one rule.

Explicitly thread any files used in later stages in dependencies.

That is, rather than having project/%.jpg depend on project/.extracted, thread the name of the extracted file in the dependency chain.

Thread dependencies, with entry targets treated as leaves

This lets you use shorter names when invoking intermediate stages and ensures that you don’t do more work than necessary.

Specify the targets as Make variables, which are translated to Makefile pattern rules

This will be invoked as:

Avoid phony target names that are also common directories

Namely, avoid target names such as build, all, include, src, lib, etc. These are often created as directories by common open-source programs, and your Makefile can get confused, which can be hard to debug. This can be avoided using .PHONY targets, but I have found it better to avoid using such directory names at all. The nice thing is that you can define a timestamp dependency rule to create that directory if needed (see below).

Timestamp dependency rules

Another best practice is to use the “make sure it exists, but don’t check the timestamp” dependency rule for directories and similar targets.

Note: the use of ; to indicate ending of target and beginning of recipe

Here, OBJDIR will be created if it does not exist. If you forget the |, the dependents of OBJDIR will be recreated each time a file inside OBJDIR changes. That is, ideally,

subtle: directories are listed as changed when entries are created, leading to spurious rebuilds.

should be

Do not rely on the ordering of recipes in the Makefile to ensure that the targets are built in a particular order. For example, if you have

Do not assume that the targets will be built in the order a, b, c. If you want that order, explicitly specify it using dependencies:

If not, you will come to grief when some poor programmer decides to use make -j or uses one of the parallel makes. Make use of tools such as gvmake to ensure that the dependencies are correctly ordered. Use -o make.dot to create a dotfile and inspect it directly — it has a human-readable syntax.

Use --debug=basic -n to view which files need to be remade, and GNU remake for debugging. Usegmake debugger or gmd

Make it a practice to explicitly use .PHONY

Phony targets are those targets that provide entry points to make invocations and are not file-based.

Remove implicit rules

Disable automatic removal of intermediate files

Idempotent on repeated invocations

make invoked on the same target multiple times without changes should not differ in the artifacts produced compared to a single invocation.

Clean up after yourself

make clean should remove all files generated by the build. The target make clobber can be used as a super make clean if the build is not self-contained.

Never save an artifact in a repository that can be generated by make

Avoid the temptation to save generated artifacts if they can be regenerated by another run of make.

Avoid in-place edits of any files.

These are susceptible to race conditions, corruption, and incomplete builds, and can make your life difficult.

Silent makes

Thanks to mad-scientist for introducing me to the .SILENT target. There are various ways to accomplish this. The most fine-grained approach is to use @ in front of a recipe line:

Another is to use the .SILENT target to silence specific targets:

The .SILENT target, if defined without dependencies, will silence all targets:

You can, of course, make it dependent on a variable V:

Or achieve the same effect more easily:

make itself accepts the command-line option --silent:

Recommendation from here

Change the .RECIPEPREFIX if you are starting a new project where you do not have to interact with other projects. I recommend ; over > as the former lets you join multiple command lines together or split a command line into multiple parts with little effort.

Ensure that a failure in a pipe stage kills the build

Other best practices:

• Always include all and clean targets

• Use variables for compiler settings

Exporting a variable to the environment for specific targets

Define the variable at the top of the Makefile:

For each target that needs it exported into the shell environment, add a target-specific export line before the rule:

This exports LD_LIBRARY_PATH into every shell command in those recipes without affecting any other target. Note: A top-level VAR := value is visible to Make expressions ($(VAR)) but is not passed to child shells unless explicitly exported. A bare export VAR would export it globally to all targets; the target-specific form shown here limits the export to the targets you choose.

Run all

Artifacts

The runnable Python source for this notebook is available here.

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论