Common Makefile Syntax and Functions in Chip Development

1. Why do Makefiles exist?

In software or chip development, Makefile is generally used. It is a text file that contains instructions on how to compile and link programs. Makefile is used by the make tool, which is an automation build tool that can automatically execute the compilation and linking process based on the instructions in the Makefile.

The main roles of Makefile in chip development include:

  • Automating the compilation process: Makefile can specify the source code files to be compiled, the compiler, and the compilation options. This allows chip developers to easily compile the entire project without manually executing each compilation step.
  • Managing dependencies: Makefile can specify the dependencies between source files and target files. This allows the make tool to determine which files need to be recompiled, thus optimizing the compilation process and saving time.
  • Linking target files: Makefile can specify the target files to be linked, the linker, and the linking options. This allows chip developers to easily link multiple target files into a single executable file or library.
  • Executing tests and simulations: Makefile can include rules for executing test and simulation scripts. This allows chip developers to automate the testing and simulation process, thus improving development efficiency.

2. Starting with the simplest Makefile

1. Syntax of Makefile

targets: prerequisites
 command
 command
 command

Here, targets are the names of the instructions we want to execute, prerequisites are the conditions that need to be met before executing the instructions, and command is the instruction we want to execute. Note that the command must be preceded by a tab (multiple tabs are also acceptable), but spaces cannot be used.

hello:
 echo "Hello, World"

Executing make will yield the following result:

Common Makefile Syntax and Functions in Chip Development
As we can see, after executing make, it first prints the command that we want to execute and then executes that command.
If we only want to check whether our Makefile is correct, i.e., show the specific commands to be executed without executing them, we can execute make -n: make hello -n, and the result will be as follows:
echo "Hello, World"

For the echo command, if we only want to see the execution result without seeing the echoed result, we can use @echo:

hello:
 @echo "Hello, World"

If the command is preceded by a space, the execution will prompt:

*** missing separator.  Stop.

2. Multiple targets

If there are multiple targets, for example:
hello1:
 echo "Hello1, World"

hello2:
 echo "Hello2, World"
We can specify which target to execute: make hello2. If not specified, executing make will default to executing the first target.

3. Leading and trailing spaces

In a Makefile, trailing spaces are not removed, while leading spaces are ignored. This means that if you need to create a variable that contains a single space, directly using a space in the variable assignment may lead to unexpected results due to the ignored leading space. To solve this problem, you can use the $(nullstring) special variable to ensure that the single space is correctly included in the variable value.

(nullstring) and space can ensure that the space is treated as a valid character included in the variable value, rather than being treated as meaningless leading whitespace.
with_spaces = hello   # with_spaces has many spaces after "hello"
after = $(with_spaces)there

nullstring =
space = $(nullstring) # Make a variable with a single space.

all:
 echo "$(after)"
 echo start"$(space)"end

The running result is as follows:

Common Makefile Syntax and Functions in Chip Development

4. Multiline

If a line of content is too long and needs to be broken into multiple lines, use the \ symbol:
some_file:
 echo This line is too long, so \
  it is broken up into multiple lines

5. .PHONY

In Makefile, we often see .PHONY before clean:
some_file:
 touch some_file
 touch clean

.PHONY: clean
clean:
 rm -f some_file
 rm -f clean
The purpose of adding this is mainly to avoid command names conflicting with file names in the directory. If there is a file named clean in the directory, when we execute make clean, the make tool will think that clean has no dependency files and will not execute make clean. Adding .PHONY can avoid this situation.

6. make clean

The clean command is used to clean up generated files from compilation, which is a commonly used instruction. Of course, the target name does not have to be clean, but it is generally called that.
compile:
 touch compile_files

run:
 touch run_files

clean:
 rm -f compile_files run_files
Thus, to delete files generated during compilation or execution, simply execute make clean.
Generally, we will also clean up the files generated in the last build before compiling:
compile: clean
 touch compile_files

run:
 touch run_files

clean:
 rm -f compile_files run_files
Thus, when executing compile, it will first execute clean.

3. Variables in Makefile

In Makefile, variables can only be of string type. Let’s look at some variable assignment operations and distinguish between ‘=’, ‘:=’, ‘?=’ and ‘+=’.

  • = (Normal assignment): When assigning a variable using the equal sign (=), it defines a recursively expanded variable. This means that when the variable is defined, Makefile does not immediately expand its value but treats it as a macro to be processed later, only expanding it when the variable is referenced in subsequent rules or expressions.

  • := (Immediate assignment): When assigning with the colon equal sign (:=), it defines a simply expanded variable. This assignment method immediately expands and replaces all variable references known at the time of definition, without retaining any pending macros. Once the assignment is complete, the variable’s value is fixed and no longer affected by subsequent variable definitions.

  • ?= (Conditional assignment): When assigning with the question mark equal sign (?=), it will only assign the specified value if the variable has not been assigned yet (i.e., is undefined or empty). If the variable already has a non-empty value, this assignment operation will be ignored.

  • += (Append assignment): When you assign a value to a variable using +=, the newly specified value will be added to the end of the variable’s current value, effectively concatenating the two values. This operation is particularly useful for accumulating or adding a series of related values, such as gradually collecting compilation options, source file lists, link library paths, etc.

Let’s illustrate with an example:
one = one ${later_variable}
two := two ${later_variable}

later_variable = later

all:
   echo $(one)
   echo $(two)

The running result is as follows:

Common Makefile Syntax and Functions in Chip Development

As we can see, later_variable is defined later, so when one is applied, it gets the value later, while two is assigned at the beginning and at that time later_variable has not yet been assigned, so the later_variable referenced in two is empty.

one = hello
one ?= will not be set
two ?= will be set

all:
 echo $(one)
 echo $(two)

The result of executing make is as follows:

Common Makefile Syntax and Functions in Chip Development
We can also assign values when executing make: make all two=123, and the result will be as follows:
Common Makefile Syntax and Functions in Chip Development
+= is unambiguous, it simply concatenates:
foo := start
foo += more

all:
 echo $(foo)

The result is as follows:

start more

For undefined variables, they are just empty strings.

all:
 # Undefined variables are just empty strings!
 echo $(nowhere)
As we mentioned, using ?= allows us to specify the value of a variable when executing the make command. If we use = to assign a value to a variable, can we still overwrite this variable from the terminal?
Of course, we can, using the override keyword. We execute: make all option_one=123, and we can overwrite.
# Overrides command line arguments
override option_one = did_override
# Does not override command line arguments
option_two = not_override
all:
 echo $(option_one)
 echo $(option_two)

4. Each line is a separate shell

One important thing to note in Makefile is that each line is a separate shell; variables defined in the previous line are not valid in the next line. Let’s compare the following two Makefile styles:

Style one:
all:
 @export foo=hello
 @echo $${foo}

Style two:

all:
 @export foo=hello; echo $${foo}
The echo content of the first style will not print, while the echo content of the second style will print hello.

This is because in the first style, export foo=hello and echo $${foo} are executed in two separate shells, thus the second shell cannot access the environment variable defined in the first shell.

In the second style, defining and accessing in the same line works correctly.

Of course, you can also escape by adding a backslash at the end of the first line:
all:
 @export foo=hello \
echo $${foo}

5. Usage of $$ symbol

As we used echo $${foo}, why do we need to use two symbols? The $$ symbol is mainly used to convert references in Makefile to shell references.

When first encountering Makefile, it can be somewhat confusing.

First, it is essential to clarify that executing the make command does not occur in a shell environment; the shell is only executed when a particular operation in the Makefile is executed.

A single symbol is used to reference the value of a variable defined in the shell.
foo = one two three
all:
 for i in $(foo); do \
  echo $i; \
 end

After preprocessing by make, it becomes:

for i in one two three; do \
 echo ; \
end
Since foo has been defined, $(foo) becomes one two three; while the variable i has not been defined in the Makefile, so it is converted to empty.
If we change it to the following style:
foo = one two three
all:
 for i in $(foo); do \
  echo $$i; \
 end
It will print: one two three.

This is because the $$i command is translated by make to the shell’s $i, and at this point, the value of i in the shell is one two three.

6. Wildcards

In Makefile, * and % are both wildcards. Let’s look at their usage.

Print all c files:
print: $(wildcard *.c)
 ls -la $?

It is essential to note that when using the * symbol, it must be used in conjunction with the wildcard keyword.

thing_wrong := *.o # Don't do this! '*' will not get expanded
thing_right := $(wildcard *.o)

all: one two three four

# Fails, because $(thing_wrong) is the string "*.o"
one: $(thing_wrong)

# Stays as *.o if there are no files that match this pattern :(
two: *.o 

# Works as you would expect! In this case, it does nothing.
three: $(thing_right)

# Same as rule three
four: $(wildcard *.o)
% is often used when compiling c code, which we will elaborate on later.

7. Runtime added commands

When running Make, you can use the following commands to improve our debugging efficiency:

-n: Displays the commands to be executed in the terminal but does not execute them, allowing us to check whether the commands to be executed are correct.

The Makefile content is as follows:
one:
 false1
 false2
 @echo "this is one"
Common Makefile Syntax and Functions in Chip Development

-i: Displays all errors that occur during Make execution; otherwise, it will stop at the first error.

Common Makefile Syntax and Functions in Chip Development

-: In the Makefile, this also achieves the same function as -i, but only for that line. Transform the Makefile into the following:

one:
 -false1
 -false2
 @echo "this is one"

After executing make one, it will display the same functionality as -i:

Common Makefile Syntax and Functions in Chip Development

8. Conditional statements

There are also conditional statements in Makefile:
foo = ok

all:
ifeq ($(foo), ok)
 echo "foo equals ok"
else
 echo "nope"
endif

Check if a variable is empty:

nullstring =
foo = $(nullstring) # end of line; there is a space here

all:
ifeq ($(strip $(foo)),)
 echo "foo is empty after being stripped"
endif
ifeq ($(nullstring),)
 echo "nullstring doesn't even have spaces"
endif

Check if a variable is defined:

bar =
foo = $(bar)

all:
ifdef foo
 echo "foo is defined"
endif
ifndef bar
 echo "but bar is not"
endif

9. Functions in Makefile

Makefile has many built-in functions for us to call directly:

1. subst function

bar := ${subst not,totally, "I am not superman"}
all:
 @echo $(bar)
Common Makefile Syntax and Functions in Chip Development
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))

all:
 @echo $(bar)

The result is:

a,b,c
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space), $(comma) , $(foo))

all:
 @echo $(bar)

The result is:

, a , b , c

2. patsubst function

foo := a.o b.o l.a c.o
one := $(patsubst %.o,%.c,$(foo))
# This is a shorthand for the above
two := $(foo:%.o=%.c)
# This is the suffix-only shorthand, and is also equivalent to the above.
three := $(foo:.o=.c)

all:
        @echo $(one)
        @echo $(two)
        @echo $(three)

The result is:

Common Makefile Syntax and Functions in Chip Development

3. foreach function

foo := who are you
# For each "word" in foo, output that same word with an exclamation after
bar := $(foreach wrd,$(foo),$(wrd)!)

all:
 # Output is "who! are! you!"
 @echo $(bar)

4. if function

foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)

all:
 @echo $(foo)
 @echo $(bar)

5. call function

sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)

all:
 # Outputs "Variable Name: sweet_new_fn First: go Second: tigers Empty Variable:"
 @echo $(call sweet_new_fn, go, tigers)

6. shell function

all:
 @echo $(shell ls -la) # Very ugly because the newlines are gone!
Usually, using Makefile for tasks related to chip development such as VCS simulation compilation, the knowledge of Makefile presented above is generally sufficient. However, Makefile is also frequently used for compiling C files. We will explain these contents in the next article.

Many examples in this article are sourced from: Makefile Tutorial By Example

Common Makefile Syntax and Functions in Chip Development

END

Source: Cat of Fourier

Copyright belongs to the original author. If there is infringement, please contact to delete.

Recommended Reading

Comic | The Sad State of Chinese Software Development

A Huawei C Language Interview Question That Many People Failed!

A Hard Disk That Cannot Be Described Broke Down, But I Am Too Embarrassed to Fix It…

→ Follow to Avoid Getting Lost ←

Leave a Comment