A Shallow Dive into GNU Make - JohnHau/mis GitHub Wiki

https://interrupt.memfault.com/blog/gnu-make-guidelines

03 Mar 2020 by Noah Pendleton GNU Make is a popular and commonly used program for building C language software. It is used when building the Linux kernel and other frequently used GNU/Linux programs and software libraries.

Most embedded software developers will work with GNU Make at some point in their career, either using it to compile small libraries or building an entire project. Though there are many, many alternatives to Make, it’s still commonly chosen as the build system for new software given its feature set and wide support.

This article explains general concepts and features of GNU Make and includes recommendations for getting the most out of a Make build! Consider it a brief guided tour through some of my favorite/most used Make concepts and features 🤗.

If you feel like you already know Make pretty well, feel free to skip the tutorial portion and jump to my personal recommendations.

Table of Contents What is GNU Make? When to choose Make Invoking Make Parallel Invocation Anatomy of a Makefile Variables Targets (Goals) Prerequisites Recipe Advanced Topics Functions Conditionals include Directive Sub-make Metaprogramming with eval VPATH touch Debugging Makefiles Profiling Using a Verbose Flag Full Example Recommendations Outro References What is GNU Make? GNU Make is a program that automates the running of shell commands and helps with repetitive tasks. It is typically used to transform files into some other form, e.g. compiling source code files into programs or libraries.

It does this by tracking prerequisites and executing a hierarchy of commands to produce targets.

Although the GNU Make manual is lengthy, I suggest giving it a read as it is the best reference I’ve found: https://www.gnu.org/software/make/manual/html_node/index.html

Let’s dive in!

When to choose Make Make is suitable for building small C/C++ projects or libraries that would be included in another project’s build system. Most build systems will have a way to integrate Make-based sub-projects.

For larger projects, you will find a more modern build system easier to work with.

I would suggest a build system other than Make in the following situations:

When the number of targets (or files) being built is (or will eventually be) in the hundreds. A “configure” step is desired, which sets up and persists variables, target definitions, and environment configurations. The project is going to remain internal or private and will not need to be built by end users. You find debugging a frustrating exercise. You need the build to be cross platform that can build on macOS, Linux, and Windows. In these situations, you might find using CMake, Bazel, Meson, or another modern build system a more pleasurable experience.

Invoking Make Running make will load a file named Makefile from the current directory and attempt to update the default goal (more on goals later).

Make will search for files named GNUmakefile, makefile, and Makefile, in that order

You can specify a particular makefile with the -f/--file argument:

$ make -f foo.mk You can specify any number of goals by listing them as positional arguments:

typical goals

$ make clean all You can pass Make a directory with the -C argument, and this will run Make as if it first cd‘d into that directory.

$ make -C some/sub/directory Fun fact: git also can be run with -C for the same effect!

Parallel Invocation Make can run jobs in parallel if you provide the -j or -l options. A guideline I’ve been told is to set the job limit to 1.5 times the number of processor cores you have:

a machine with 4 cores:

$ make -j 6 Anecdotally, I’ve seen slightly better CPU utilization with the -l “load limit” option, vs. the -j “jobs” option. YMMV though!

There are a few ways to programmatically find the CPU count for the current machine. One easy option is to use the python multiprocessing.cpu_count() function to get the number of threads supported by the system (note on a system with hyper-threading, this will use up a lot of your machine’s resources, but is probably preferable to letting Make spawn an unlimited number of jobs).

call the python cpu_count() function in a subshell

$ make -l $(python -c "import multiprocessing; print(multiprocessing.cpu_count())") Output During Parallel Invocation If you have a lot of output from the commands Make is executing in parallel, you might see output interleaved on stdout. To handle this, Make has the option --ouput-sync.

I recommend using --output-sync=recurse, which will print the entire output of each target’s recipe when it completes, without interspersing other recipe output.

It also will output an entire recursive Make’s output together if your recipe is using recursive make.

Anatomy of a Makefile A Makefile contains rules used to produce targets. Some basic components of a Makefile are shown below:

Comments are prefixed with the '#' symbol

A variable assignment

FOO = "hello there!"

A rule creating target "test", with "test.c" as a prerequisite

test: test.c # The contents of a rule is called the "recipe", and is # typically composed of one or more shell commands. # It must be indented from the target name (historically with # tabs, spaces are permitted)

# Using the variable "FOO"
echo $(FOO)

# Calling the C compiler using a predefined variable naming
# the default C compiler, '$(CC)'
$(CC) test.c -o test

Let’s take a look at each part of the example above.

Variables Variables are used with the syntax $(FOO), where FOO is the variable name.

Variables contain purely strings as Make does not have other data types. Appending to a variable will add a space and the new content:

FOO = one FOO += two

FOO is now "one two"

FOO = one FOO = $(FOO)two

FOO is now "onetwo"

Variable Assignment In GNU Make syntax, variables are assigned with two “flavors”:

recursive expansion: variable = expression The expression on the right hand side is assigned verbatim to the variable- this behaves much like a macro in C/C++, where the expression is evaluated when the variable is used: FOO = 1 BAR = $(FOO) FOO = 2

prints BAR=2

$(info BAR=$(BAR)) simple expansion: variable := expression This assigns the result of an expression to a variable; the expression is expanded at the time of assignment: FOO = 1 BAR := $(FOO) FOO = 2

prints BAR=1

$(info BAR=$(BAR)) Note: the $(info ...) function is being used above to print expressions and can be handy when debugging makefiles!*`

Variables which are not explicitly, implicitly, nor automatically set will evaluate to an empty string.

Environment Variables Environment variables are carried into the Make execution environment. Consider the following makefile for example:

$(info YOLO variable = $(YOLO)) If we set the variable YOLO in the shell command when running make, we’ll set the value:

$ YOLO="hello there!" make YOLO variable = hello there! make: *** No targets. Stop. Note: Make prints the “No targets” error because our makefile had no targets listed!

If you use the ?= assignment syntax, Make will only assign that value if the variable doesn’t already have a value:

Makefile:

default CC to gcc

CC ?= gcc We can then override $(CC) in that makefile:

$ CC=clang make Another common pattern is to allow inserting additional flags. In the makefile, we would append to the variable instead of directly assigning to it.

CFLAGS += -Wall This permits passing extra flags in from the environment:

$ CFLAGS='-Werror=conversion -Werror=double-promotion' make This can be very useful!

Overriding Variables A special category of variable usage is called overriding variables. Using this command-line option will override the value set ANYWHERE ELSE in the environment or Makefile!

Makefile:

the value passed in the make command will override

any value set elsewhere

YOLO = "not overridden" $(info $(YOLO)) Command:

setting "YOLO" to different values in the environment + makefile + overriding

variable, yields the overriding value

$ YOLO="environment set" make YOLO='overridden!!' overridden!! make: *** No targets. Stop. Overriding variables can be confusing, and should be used with caution!

Target-Specific Variables These variables are only available in the recipe context. They also apply to any prerequisite recipe!

set the -g value to CFLAGS

applies to the prog.o/foo.o/bar.o recipes too!

prog : CFLAGS = -g prog : prog.o foo.o bar.o echo $(CFLAGS) # will print '-g' Implicit Variables These are pre-defined by Make (unless overridden with any other variable type of the same name). Some common examples:

$(CC) - the C compiler (gcc) $(AR) - archive program (ar) $(CFLAGS) - flags for the C compiler Full list here:

https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html

Automatic Variables These are special variables always set by Make and available in recipe context. They can be useful to prevent duplicated names (Don’t Repeat Yourself).

A few common automatic variables:

$@ : the target name, here it would be "test.txt"

test.txt: echo HEYO > $@

$^ : name of all the prerequisites

all.zip: foo.txt test.txt # run the gzip command with all the prerequisites "$^", outputting to the # name of the target, "$@" gzip -c $^ > $@ See more at: https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html

Targets (Goals) Targets are the left hand side in the rule syntax:

target: prerequisite recipe Targets almost always name files. This is because Make uses last-modified time to track if a target is newer or older than its prerequisites and whether it needs to be rebuilt!

When invoking Make, you can specify which target(s) you want to build as the goals by specifying it as a positional argument:

make the 'test.txt' and 'all.zip' targets

make test.txt all.zip If you don’t specify a goal in the command, Make uses the first target specified in the makefile, called the “default goal” (you can also override the default goal if you need to).

Phony Targets Sometimes it’s useful to have meta-targets like all, clean, test, etc. In these cases, you don’t want Make to check for a file named all/clean etc.

Make provides the .PHONY target syntax to mark a target as not pointing to a file:

Say our project builds a program and a library 'foo' and 'foo.a'; if we want

to build both by default we might make an 'all' rule that builds both

.PHONY: all

all: foo foo.a If you have multiple phony targets, a good pattern might be to append each to .PHONY where it’s defined:

the 'all' rule that builds and tests. Note that it's listed first to make it

the default rule

.PHONY: all all: build test

compile foo.c into a program 'foo'

foo: foo.c $(CC) foo.c -o foo

compile foo-lib.c into a library 'foo.a'

foo.a: foo-lib.c # compile the object file $(CC) foo-lib.c -c foo-lib.o # use ar to create a static library containing our object file. using the # '$@' variable here to specify the rule target 'foo.a' $(AR) rcs $@ foo-lib.o

a phony rule that builds our project; just contains a prerequisite of the

library + program

.PHONY: build build: foo foo.a

a phony rule that runs our test harness. has the 'build' target as a

prerequisite! Make will make sure (pardon the pun) the build rule executes

first

.PHONY: test test: build ./run-tests.sh NOTE!!! .PHONY targets are ALWAYS considered out-of-date, so Make will ALWAYS run the recipe for those targets (and therfore any target that has a .PHONY prerequisite!). Use with caution!!

Implicit Rules Implicit rules are provided by Make. I find using them to be confusing since there’s so much behavior happening behind the scenes. You will occasionally encounter them in the wild, so be aware.

Here’s a quick example:

this will compile 'test.c' with the default $(CC), $(CFLAGS), into the program

'test'. it will handle prerequisite tracking on test.c

test: test.o Full list of implicit rules here:

https://www.gnu.org/software/make/manual/html_node/Catalogue-of-Rules.html

Pattern Rules Pattern rules let you write a generic rule that applies to multiple targets via pattern-matching:

Note the use of the '$<' automatic variable, specifying the first

prerequisite, which is the .c file

%.o: %.c $(CC) -c $< -o $@ The rule will then be used to make any target matching the pattern, which above would be any file matching %.o, e.g. foo.o, bar.o.

If you use those .o files mentioned above to build a program:

OBJ_FILES = foo.o bar.o

Use CC to link foo.o + bar.o into 'program'. Note the use of the '$^'

automatic variable, specifying ALL the prerequisites (all the OBJ_FILES)

should be part of the link command

program: $(OBJ_FILES) $(CC) -o $@ $^ Prerequisites As seen above, these are targets that Make will check before running a rule. They can be files or other targets.

If any prerequisite is newer (modified-time) than the target, Make will run the target rule.

In C projects, you might have a rule that converts a C file to an object file, and you want the object file to rebuild if the C file changes:

foo.o: foo.c # use automatic variables for the input and output file names $(CC) $^ -c $@ Automatic Prerequisites A very important consideration for C language projects is to trigger recompilation if an #include header files change for a C file. This is done with the -M compiler flag for gcc/clang, which will output a .d file you will then import with the Make include directive.

The .d file will contain the necessary prerequisites for the .c file so any header change causes a rebuild. See more details here:

https://www.gnu.org/software/make/manual/html_node/Automatic-Prerequisites.html http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/

The basic form might be:

these are the compiler flags for emitting the dependency tracking file. Note

the usage of the '$<' automatic variable

DEPFLAGS = -MMD -MP -MF $<.d

test.o: test.c $(CC) $(DEPFLAGS) $< -c $@

bring in the prerequisites by including all the .d files. prefix the line with

'-' to prevent an error if any of the files do not exist

-include $(wildcard *.d) Order-Only Prerequisites These prerequisites will only be built if they don’t exist; if they are newer than the target, they will not trigger a target re-build.

A typical use is to create a directory for output files; emitting files to a directory will update its mtime attribute, but we don’t want that to trigger a rebuild.

OUTPUT_DIR = build

output the .o to the build directory, which we add as an order-only

prerequisite- anything right of the | pipe is considered order-only

$(OUTPUT_DIR)/test.o: test.c | $(OUTPUT_DIR) $(CC) -c $^ -o $@

rule to make the directory

$(OUTPUT_DIR): mkdir -p $@ Recipe The “recipe” is the list of shell commands to be executed to create the target. They are passed into a sub-shell (/bin/sh by default). The rule is considered successful if the target is updated after the recipe runs (but is not an error if this doesn’t happen).

foo.txt: # a simple recipe echo HEYO > $@ If any line of the recipe returns a non-zero exit code, Make will terminate and print an error message. You can tell Make to ignore non-zero exit codes by prefixing with the - character:

.PHONY: clean clean: # we don't care if rm fails -rm -r ./build Prefixing a recipe line with @ will disable echoing that line before executing:

clean: @# this recipe will just print 'About to clean everything!' @# prefixing the shell comment lines '#' here also prevents them from @# appearing during execution @echo About to clean everything! Make will expand variable/function expressions in the recipe context before running them, but will otherwise not process it. If you want to access shell variables, escape them with $:

USER = linus

print-user: # print out the shell variable $USER echo $$USER

# print out the make variable USER
echo $(USER)

Advanced Topics These features are less frequently encountered, but provide some powerful functionality that can enable sophisticated behavior in your build.

Functions Make functions are called with the syntax:

$(function-name arguments) where arguments is a comma-delimited list of arguments.

Built-in Functions There are several functions provided by Make. The most common ones I use are for text manipulation: https://www.gnu.org/software/make/manual/html_node/Text-Functions.html https://www.gnu.org/software/make/manual/html_node/File-Name-Functions.html

For example:

FILES=$(wildcard *.c)

you can combine function calls; here we strip the suffix off of $(FILES) with

the $(basename) function, then add the .o suffix

O_FILES=$(addsuffix .o,$(basename $(FILES)))

note that the GNU Make Manual suggests an alternate form for this particular

operation:

O_FILES=$(FILES:.c=.o) User-Defined Functions You can define your own functions as well:

reverse = $(2) $(1)

foo = $(call reverse,a,b) A more complicated but quite useful example:

recursive wildcard (use it instead of $(shell find . -name '*.c'))

taken from https://stackoverflow.com/a/18258352

rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))

C_FILES = $(call rwildcard,.,*.c) Shell Function You can have Make call a shell expression and capture the result:

TODAYS_DATE=$(shell date --iso-8601) I’m cautious when using this feature, though; it adds a dependency on whatever programs you use, so if you’re calling more exotic programs, make sure your build environment is controlled (e.g. in a container or with Conda).

Conditionals Make has syntax for conditional expressions:

FOO=yolo ifeq ($(FOO),yolo) $(info foo is yolo!) else $(info foo is not yolo :( ) endif

testing if a variable is set; unset variables are empty

ifneq ($(FOO),) # checking if FOO is blank $(info FOO is unset) endif The “complex conditional” syntax is just the if-elseif-else combination:

"complex conditional"

ifeq ($(FOO),yolo) $(info foo is yolo) else ifeq ($(FOO), heyo) $(info foo is heyo) else $(info foo is not yolo or heyo :( ) endif include Directive You can import other Makefile contents using the include directive:

sources.mk:

SOURCE_FILES :=
bar.c
foo.c \

Makefile:

include sources.mk

OBJECT_FILES = $(SOURCE_FILES:.c=.o)

%.o: %.c $(CC) -c $^ -o $@ Sub-make Invoking Make from a Makefile should be done with the $(MAKE) variable:

somelib.a: $(MAKE) -C path/to/somelib/directory This is often used when building external libraries. It’s also used heavily in Kconfig builds (e.g. when building the Linux kernel).

Note that this approach has some pitfalls:

Recursive invocation can result in slow builds. Tracking prerequisites can be tricky; often you will see .PHONY used. More details on the disadvantages here:

http://aegis.sourceforge.net/auug97.pdf

Metaprogramming with eval Make’s eval directive allows us to generate Make syntax at runtime:

generate rules for xml->json in some weird world

FILES = $(wildcard inputfile/*.xml)

create a user-defined function that generates rules

define GENERATE_RULE = $(eval

prereq rule for creating output directory

$(1)_OUT_DIR = $(dir $(1))/$(1)_out $(1)_OUT_DIR: mkdir -p $@

rule that calls a script on the input file and produces $@ target

$(1)_OUT_DIR/$(1).json: $(1) | $(1)_OUT_DIR ./convert-xml-to-json.sh $(1) $@ )

add the target to the all rule

all: $(1)_OUT_DIR/$(1).json endef

produce the rules

.PHONY: all all:

$(foreach file,$(FILES),$(call GENERATE_RULE,$(file))) Note that approaches using this feature of Make can be quite confusing, adding helpful comments explaining what the intent is can be useful for your future self!

VPATH VPATH is a special Make variable that contains a list of directories Make should search when looking for prerequisites and targets.

It can be used to emit object files or other derived files into a ./build directory, instead of cluttering up the src directory:

This makefile should be invoked from the temporary build directory, eg:

$ mkdir -p build && cd ./build && make -f ../Makefile

Derive the directory containing this Makefile

MAKEFILE_DIR = $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))

now inform Make we should look for prerequisites from the root directory as

well as the cwd

VPATH += $(MAKEFILE_DIR)

SRC_FILES = $(wildcard $(MAKEFILE_DIR)/src/*.c)

Set the obj file paths to be relative to the cwd

OBJ_FILES = $(subst $(MAKEFILE_DIR)/,,$(SRC_FILES:.c=.o))

now we can continue as if Make was running from the root directory, and not a

subdirectory

$(OBJ_FILES) will be built by the pattern rule below

foo.a: $(OBJ_FILES) $(AR) rcs $@ $(OBJ_FILES)

pattern rule; since we added ROOT_DIR to VPATH, Make can find prerequisites

like src/test.c when running from the build directory!

%.o: %.c # create the directory tree for the output file 👍 echo $@ mkdir -p $(dir $@) # compile $(CC) -c $^ -o $@ I recommend avoiding use of VPATH. It’s usually simpler to achieve the same out-of-tree behavior by outputting the generated files in a build directory without needing VPATH.

touch You may see the touch command used to track rules that seem difficult to otherwise track; for example, when unpacking a toolchain:

our tools are stored in tools.tar.gz, and downloaded from a server

TOOLS_ARCHIVE = tools.tar.gz TOOLS_URL = https://httpbin.org/get

the rule to download the tools using wget

$(TOOLS_ARCHIVE): wget $(TOOLS_URL) -O $(TOOLS_ARCHIVE)

rule to unpack them

tools-unpacked.dummy: $(TOOLS_ARCHIVE) # running this command results in a directory.. but how do we know it # completed, without a file to track? tar xzvf $^ # use the touch command to record completion in a dummy file touch $@ I recommend avoiding the use of touch. However there are some cases where it might be unavoidable.

Debugging Makefiles I typically use the Make equivalent of printf, the $(info/warning/error) functions, for small problems, for example when checking conditional paths that aren’t working:

ifeq ($(CC),clang) $(error whoops, clang not supported!) endif For debugging why a rule is running when it shouldn’t (or vice versa), you can use the --debug options: https://www.gnu.org/software/make/manual/html_node/Options-Summary.html

I recommend redirecting stdout to a file when using this option, it can produce a lot of output.

Profiling For profiling a make invocation (e.g. for attempting to improve compilation times), this tool can be useful:

https://github.com/rocky/remake

Check out the tips here for compilation-related performance improvements:

https://interrupt.memfault.com/blog/improving-compilation-times-c-cpp-projects

Using a Verbose Flag If your project includes a lot of compiler flags (search paths, lots of warning flags, etc.), then you may want to simplify the output of Make rules. It can be useful to have a toggle to easily see the full output, for example:

ifeq ($(V),1) Q := else Q := @ endif

%.o: %.c # prefix the compilation command with the $(Q) variable # use echo to print a simple "Compiling x.c" to show progress @echo Compiling $(notdir @^) $(Q) $(CC) -c $^ -o $@ To enable printing out the full compilation commands, set the V environment variable like so:

$ V=1 make Full Example Here’s an annotated example of a complete build process for an example C project. You can see this example and the source tree here.

Makefile for building the 'example' binary from C sources

Verbose flag

ifeq ($(V),1) Q := else Q := @ endif

The build folder, for all generated output. This should normally be included

in a .gitignore rule

BUILD_FOLDER := build

Default all rule will build the 'example' target, which here is an executable

.PHONY: all: $(BUILD_FOLDER)/example

List of C source files. Putting this in a separate variable, with a file on

each line, makes it easy to add files later (and makes it easier to see

additions in pull requests). Larger projects might use a wildcard to locate

source files automatically.

SRC_FILES =
src/example.c
src/main.c

Generate a list of .o files from the .c files. Prefix them with the build

folder to output the files there

OBJ_FILES = $(addprefix $(BUILD_FOLDER)/,$(SRC_FILES:.c=.o))

Generate a list of depfiles, used to track includes. The file name is the same

as the object files with the .d extension added

DEP_FILES = $(addsuffix .d,$(OBJ_FILES))

Flags to generate the .d dependency-tracking files when we compile. It's

named the same as the target file with the .d extension

DEPFLAGS = -MMD -MP -MF [email protected]

Include the dependency tracking files

-include $(DEP_FILES)

List of include dirs. These are put into CFLAGS.

INCLUDE_DIRS =
src/

Prefix the include dirs with '-I' when passing them to the compiler

CFLAGS += $(addprefix -I,$(INCLUDE_DIRS))

Set some compiler flags we need. Note that we're appending to the CFLAGS

variable

CFLAGS +=
-std=c11
-Wall
-Werror
-ffunction-sections -fdata-sections
-Og
-g3

Our project requires some linker flags: garbage collect sections, output a

.map file

LDFLAGS +=
-Wl,--gc-sections,-Map,[email protected]

Set LDLIBS to specify linking with libm, the math library

LDLIBS +=
-lm

The rule for compiling the SRC_FILES into OBJ_FILES

$(BUILD_FOLDER)/%.o: %.c @echo Compiling $(notdir $<) @# Create the folder structure for the output file @mkdir -p $(dir $@) $(Q) $(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@

The rule for building the executable "example", using OBJ_FILES as

prerequisites. Since we're not relying on an implicit rule, we need to

explicity list CFLAGS, LDFLAGS, LDLIBS

$(BUILD_FOLDER)/example: $(OBJ_FILES) @echo Linking $(notdir $@) $(Q) $(CC) $(CFLAGS) $(LDFLAGS) $^ $(LDLIBS) -o $@

Remove debug information for a smaller executable. An embedded project might

instead using [arm-none-eabi-]objcopy to convert the ELF file to a raw binary

suitable to be written to an embedded device

STRIPPED_OUTPUT = $(BUILD_FOLDER)/example-stripped

$(STRIPPED_OUTPUT): $(BUILD_FOLDER)/example @echo Stripping $(notdir $@) $(Q)objcopy --strip-debug $^ $@

Since all our generated output is placed into the build folder, our clean rule

is simple. Prefix the recipe line with '-' to not error if the build folder

doesn't exist (the -f flag for rm also has this effect)

.PHONY: clean clean: - rm -rf $(BUILD_FOLDER) Recommendations A list of recommendations for getting the most of Make:

Targets should usually be real files. Always use $(MAKE) when issuing sub-make commands. Try to avoid using .PHONY targets. If the rule generates any file artifact, consider using that as the target instead of a phony name! Try to avoid using implicit rules. For C files, make sure to use .d automatic include tracking! Use metaprogramming with caution. Use automatic variables in rules. Always try to use $@ for a recipe output path, so your rule and Make have the exact same path. Use comments liberally in Makefiles, especially if there is complicated behavior or subtle syntax used. Your co-workers (and future self) will thank you. Use the -j or -l options to run Make in parallel! Try to avoid using the touch command to track rule completion Outro I hope this article has provided a few useful pointers around GNU Make!

Make remains common in C language projects, most likely due to its usage in the Linux kernel. Many recently developed statically compiled programming languages, such as Rust or Go, provide their own build infrastructure. However, when integrating Make-based software into those languages, for example when building a C library to be called from Rust, it can be surprisingly helpful to understand some Make concepts!

You may also encounter automake in open source projects (look for a ./configure script). This is a related tool that generates Makefiles, and is worth a look (especially if you are writing C software that needs to be very widely portable).

There are many competitors to GNU Make available today, I encourage everyone to look into them. Some examples:

CMake is pretty popular (the Zephyr project uses this) and worth a look. It makes out-of-tree builds pretty easy Bazel uses a declarative syntax (vs. Make’s imperative approach) Meson is a meta-builder like cmake, but by default uses Ninja as the backend, and can be very fast References Good detailed dive into less common topics (shout out on remake): https://blog.jgc.org/2013/02/updated-list-of-my-gnu-make-articles.html

Mix of very exotic and simpler material: https://tech.davis-hansson.com/p/make/

Useful tutorial: http://maemo.org/maemo_training_material/…

Nice pictures: https://www.jfranken.de/homepages/johannes/vortraege/make.en.html

Very nice summary: https://www.alexeyshmalko.com/2014/7-things-you-should-know-about-make/

Noah Pendleton is an embedded software engineer at Memfault. Noah previously worked on embedded software teams at Fitbit and Markforged