This article was first published in 11 Makefile combat tips – Taixiao Technology

For the past few months, I’ve been refactoring an open source project that’s been under development for years: Linux Lab.

During development, you need to work with Makefiles to improve features, speed, and experience.

Over the months, I’ve accumulated a lot of Makefile skills. In retrospect, my previous knowledge of Makefiles was kindergarten level; -)

This article is long, please read the outline first. It is recommended to read the section of the outline that you are interested in (see Easter eggs at the end of the article) :

1. Immediate assignment (:=) and delayed assignment (=) 2. Timing relationship between variable assignment and target execution 3. 4. Makefile debugging and tracing methods 5. Makefile and Shell file file handling differences 6. Use comma and space variables in Makefile expressions 7. Differentiate software version numbers in makefiles 8. Simple way to modify the default execution target 9. Two ways to check whether the file exists 10. How to use the 11. Makefile instance template as a variable like a normal programCopy the code

This article summarizes a number of advanced Makefile uses to make reading and writing makefiles more efficient.

Immediate assignment (:=) and delayed assignment (=)

  • : =: Forces execution in sequence and assigns values immediately.
  • =: The result of assignment is not determined until the entire path is executed, and the latter overrides the previous, deferred assignment.

According to common logic, “:=” is recommended by default.

Examples are as follows:

$ cat Makefile

a = foo
b1 := $(a) bar
b2 = $(a) bar
a = xyz

all:
	@echo b1=$(b1)
	@echo b2=$(b2)

$ make
b1=foo bar
b2=xyz bar
Copy the code

The temporal relationship between variable assignment and target execution

Here’s a look at the relationship between variable assignment and compilation target, as well as the different ways in which variables are passed and set.

So let’s take a look at the usual way you might pass parameters, which one do you think will work?

$ make a=b target
$ make target a=b
$ a=b make target
$ export a=b && make target
Copy the code

Also, in this case, does target1 print the same variable as target2?

a = aaa

test1:
	echo $a

a = bbb

test2:
	echo $a
Copy the code

Here is an example (note: the indent of the target command must be a TAB).

$ cat Makefile a ? = aaa b := $(a) c = $(a) a_origin = $(origin a) b_origin = $(origin b) c_origin = $(origin c) all: @echo all:$(a) @echo all:$(b) @echo all:$(c) @echo all:$(a_origin) @echo all:$(b_origin) @echo all:$(c_origin) a = bbb b  := $(a) c = $(a) test1: @echo test1:$(a) @echo test1:$(b) @echo test1:$(c) @echo test1:$(a_origin) @echo test1:$(b_origin) @echo test1:$(c_origin) a = ccc b := $(a) c = $(a) test2: @echo test2:$(a) @echo test2:$(b) @echo test2:$(c) @echo test2:$(a_origin) @echo test2:$(b_origin) @echo test2:$(c_origin) a = dddCopy the code

Look at the execution.

About the order of variable assignments and variable references in the target

First, execute the default target, which is the first target to appear, in this case “all” :

$ make
all:ddd
all:ccc
all:ddd
all:file
all:file
all:file
Copy the code

What’s weird? Why do “all” targets get DDD, CCC, and DDD just after these three?

a ? = aaa b := $(a) c = $(a)Copy the code

Why not aaa, AAA and AAA?

Next, execute test1, test2:

$ make test1
test1:ddd
test1:ccc
test1:ddd
test1:file
test1:file
test1:file

$ make test2
test2:ddd
test2:ccc
test2:ddd
test2:file
test2:file
test2:file
Copy the code

Test1, test2, all the same, right? Therefore, the conclusion is that all variable assignments in the Makefile are completed before all targets, regardless of the relative position of the variable assignments to the target.

In addition, we can see that B did not keep up with C’s rhythm. After getting CCC, it did not get the last set DDD as C did, reflecting the “immediate assignment” of “:=”, while C waited until the “A” at the end of the Makefile. In addition, the last values of the three variables are all internal file assignments, so origin is file.

Assign from the command line

$ make a=fff
all:fff
all:fff
all:fff
all:command line
all:file
all:file
Copy the code

Notice that the command line overwrites all variable assignments in the Makefile, and a has a high priority.

$ make b=fff
all:ddd
all:fff
all:ddd
all:file
all:command line
all:file
Copy the code

Since A and C didn’t reference B, only B changed here.

$ make c=fff
all:ddd
all:ccc
all:fff
all:file
all:file
all:command line
Copy the code

Again, A and B do not refer to C, only C has changed.

Assign by environment variable

$ a=xxx make
all:ddd
all:ccc
all:ddd
all:file
all:file
all:file
Copy the code

The make internal assignment statement is used again.

$ a=xxx make -e
all:xxx
all:xxx
all:xxx
all:environment override
all:file
all:file
Copy the code

They do, so for the environment variable to work, you have to pass -e to make.

$ b=xxx make -e
all:ddd
all:xxx
all:ddd
all:file
all:environment override
all:file
Copy the code

This has the same effect:

$ export b=fff
$ make -e
all:ddd
all:fff
all:ddd
all:file
all:environment override
all:file
Copy the code

However, it is recommended not to use -e casually, in case someone exported an environment variable in.bashrc or.profile in advance and did not set it yourself, you may doubt life, the program behavior may be unexpected and difficult to debug.

Which takes precedence, environment variables or command lines

$ b=xxx make -e b=yyy
all:ddd
all:yyy
all:ddd
all:file
all:command line
all:file
Copy the code

You can see that the command line takes precedence.

To summarize:

  • All variable statements are executed before statements under target (each target statement has a TAB indent).
  • Override priority:command line > environment override > file

And then a little homework assignment, okay? What is the result of this?

$ b=xxx make -e b=yyy all b=zzz test2 b=mmm
Copy the code

How do I get all the arguments and compile targets passed by make

Let’s start with this question:

$ make test1 test2 test3 a=123 b=456
Copy the code

How do I get all the parameters after the make command in the Makefile?

$1, $2, $3, $4… $@

Similarly, there are requirements in makefiles, such as to see if a parameter has been passed in and to do different things depending on the parameter.

There are two types of arguments after make: command-line variables and compile targets.

These are stored in the MAKEOVERRIDES and MAKECMDGOALS variables, respectively.

To determine if a compilation target is passed, do this:

ifeq ($(filter test1, $(MAKECMDGOALS)), test1)
    do something here
endif
Copy the code

The above code can actually be used to put variable assignments into target-specific code blocks. This can greatly improve the efficiency of large Makefiles by not executing extraneous blocks of code when executing a specific target.

To check whether an argument is passed, do the following:

ifeq ($(origin a), command line)
    do something here
endif
Copy the code

Of course, you can also do the findString check from the MAKEOVERRIDES, but not as easily as Origin.

Makefile debugging and tracing methods at a glance

Debugging

$ make --debug xxx
Copy the code

Expand the entire process of make parsing and executing XXX.

Tracing

$ make --trace xxx
Copy the code

Expand the execution of the XXX object code, a bit like set-x in the Shell. This feature is only supported in Make 4.1 and later.

Logging

$(info ...)
$(warning ...)
$(error ...)
Copy the code

Error Logs are printed and immediately exited. This is very suitable for reoccurring errors.

Environment dumping

$ make -p xxx > xxx.data.dump
Copy the code

Open xxx.data.dump and find the location of XXX to check whether relevant variables meet expectations.

File name handling differs between makefiles and shells

There are Shell – like commands dirname and basename in makefiles: dir, basename, notdir.

$ cat Makefile
makefile:
	@echo $(dir $a)
	@echo $(basename $a)
	@echo $(notdir $a)

shell:
	@echo $(shell dirname $a)
	@echo $(shell basename $a)

$ make makefile a=/path/to/abc.efg.tgz
/path/to/
/path/to/abc.efg
abc.efg.tgz
$ make shell a=/path/to/abc.efg.tgz
/path/to
abc.efg.tgz

$ make makefile a=/path/to/
/path/to/
/path/to/

$ make shell a=/path/to/
/path
to

$ make makefile a=/path/to
/path/
/path/to
to
Copy the code

By comparison, you can see that dir and basename in the Makefile are very subtly different from dirName and basename in the Shell. If you interpret it as equivalent, then it’s very troublesome, because you don’t get what you expect.

For files, there is the following equivalence relation:

parameter action Makefile Shell
/path/to/abc.efg.tgz Take a directory dir dirname
Same as above Take the file name notdir basename

Also note that the Makefile dir takes a directory with a/suffix, while the Shell dirname result does not have a /. Dir and basename get directories, while the Shell can split out the name of the parent and word directories. To align to a Makefile, dir and notdir have the same effect as Shell dirname and basename by stripping the ‘/’.

Here is the transformation:

$ cat Makefile
makefile:
	@echo $(patsubst %/,%,$(dir $(patsubst %/,%,$a)))
	@echo $(notdir $(patsubst %/,%,$a))

shell:
	@echo $(shell dirname $a)
	@echo $(shell basename $a)

$ make makefile a=/path/to/abc.efg.tgz
/path/to
abc.efg.tgz
$ make shell a=/path/to/abc.efg.tgz
/path/to
abc.efg.tgz

$ make shell a=/path/to/
/path
to
$ make makefile a=/path/to/
/path
to
Copy the code

As you can see, after the transformation, the results are aligned with the Shell results.

Use comma and space variables in Makefile expressions

Commas and Spaces are special symbols in Makefile expressions that require special handling if you want to use them.

empty :=
space := $(empty) $(empty)
comma := ,
Copy the code

Differentiate software version numbers in makefiles

Makefiles usually need to pass different parameters based on the software version, so it is often necessary to compare software version numbers.

For example, after Linux 4.19, oldnoconfig was deleted and replaced with olddefconfig. Therefore, oldnoconfig used before cannot be used in the new version, and it cannot be used in the old version directly, so it has to be differentiated.

What do you think you should do about it? Think about it before you see the answer.

Here’s the key snippet:

LINUX_MAJOR_VER := $(subst v,,$(firstword $(subst .,$(space),$(LINUX)))) LINUX_MINOR_VER := $(subst v,,$(word 2,$(subst .,$(space),$(LINUX)))) ifeq ($(shell [ $(LINUX_MAJOR_VER) -lt 4 -o $(LINUX_MAJOR_VER) -eq 4 -a $(LINUX_MINOR_VER) -le 19 ]; echo ??) ,0) KERNEL_OLDDEFCONFIG := oldnoconfig else KERNEL_OLDDEFCONFIG := olddefconfig endifCopy the code

Similarly, if you want to be compatible with different versions of GCC, you have to pass different compilation options depending on the GCC version, which can also be identified as above. There are many requirements for this in the Linux source code.

But it took the try – run the way of a cc – option – yn (see Linux stable/scripts/Kbuild include), it is a form of trial and error, to avoid the accumulated a lot of code, but here in the version of the judgment is not much, And call this kind of target is expensive, not necessary, directly add judgment can be.

It’s important to note that given the potential inconsistencies in the naming of version numbers, for example, by adding -rc1 to the end, and then adding something else, this logic can be replaced by, for example, Linux-stable /scripts/Makefile/grep olddefconfig

KCONFIG_MAKEFILE := $(KERNEL_SRC)/scripts/kconfig/Makefile KERNEL_OLDDEFCONFIG := olddefconfig ifeq ($(KCONFIG_MAKEFILE), $(wildcard $(KCONFIG_MAKEFILE))) ifneq ($(shell grep olddefconfig -q $(KCONFIG_MAKEFILE); echo ??) ,0) ifneq ($(shell grep oldnoconfig -q $(KCONFIG_MAKEFILE); echo ??) ,0) KERNEL_OLDDEFCONFIG := oldconfig else KERNEL_OLDDEFCONFIG := oldnoconfig endif endif endifCopy the code

A simple way to modify the default execution target

If you hit make without specifying a target, the first target in the Makefile will be executed. This is the natural logic, but in some cases, for example, the Makefile provides a mechanism to change the default execution target if the code needs to be moved from the Makefile to the beginning of the file as the code evolves. This can be a big change.

Take a look at the example above:

$ make -p | grep makefile | grep -v ^#
.DEFAULT_GOAL := makefile
makefile:
Copy the code

As you can see, the makefile is assigned to the.default_goal variable. By overriding the variable, you can set any target.

$ make -p .DEFAULT_GOAL=shell a=/path/to/abc.efg.tgz | grep ^.DEFAULT_GOAL
.DEFAULT_GOAL = shell
Copy the code

It is possible to overwrite this, and to make it permanent, just add it to the Makefile:

override .DEFAULT_GOAL := shell
Copy the code

Two ways to check if a file exists

In makefiles, you usually need to check whether some environment or tool is Ready. To check whether the file exists, you can use wildcard to expand and match, or you can use Shell to determine.

ifeq ($(TEST_FILE), $(wildcard $(TEST_FILE))) $(info file exists) endif ifeq ($(shell [ -f $(TEST_FILE) ]; echo ??) , 0) $(info file exists) endifCopy the code

The second method is more liberal and can be extended to check whether a file is executable or to call grep to do more complex text content checking. In complex scenarios, calling the Shell through the second method is a good choice.

How to use the target as a variable like a normal program

What if you make test-run arg1 arg2 and want arg1 arg2 as an argument to the test-run target? You can use the eval directive, which dynamically builds compilation targets.

Make arg1, arg2 null with the eval command. If arg1, arg2 is present, it will not be executed. Then test-run will be executed.

Roughly implemented as:

$ cat Makefile # Must put this at the end of Makefile, to make sure override the targets before here # If the first argument is "xxx-run"... first_target := $(firstword $(MAKECMDGOALS)) reserve_target := $(first_target:-run=) ifeq ($(findstring -run,$(first_target)),-run) # use the rest as arguments for "run" RUN_ARGS := $(filter-out $(reserve_target),$(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))) # ... and turn them into do-nothing targets $(eval $(RUN_ARGS):; @:) endif test-run: @echo $(RUN_ARGS) $ make test-run test1 test2Copy the code

For example, if you want to call a kernel compile target from an external target, you usually need to enter the kernel source code and then execute make target, which may require writing several such targets:

kernel-target1:
	@make target1 -C /path/to/linux-src

kernel-target2:
	@make target2 -C /path/to/linux-src
Copy the code

With the above support, you can do something like this:

kernel-run:
	@make $(arg1) -C /path/to/linux-src
Copy the code

It is not complicated to use, and various targets of the kernel can be passed in as parameters:

$ make kernel-run target1
$ make kernel-run target2
Copy the code

Although, arg1 above could also be written like this:

$ make kernel-run arg1=target1
$ make kernel-run arg1=target2
Copy the code

But in the use of efficiency is obviously not as direct as the former.

Makefile instance template

The content of this article to collect the most Linux Lab: examples/makefile/template.

Here is a free experience card for you

Welcome to Linux Knowledge Planet via the free experience card below: