Follow for more updates, let’s go From Zero To Hero!
Introduction
In Go language development, we want to standardize code style so that each member can format code with one click and check for syntax errors; we want to run unit tests, generate test reports, and compile, package, and release projects with one click, which requires the use of Make. There are many types of Make, but the one we commonly use is GNU Make. With Make, we can greatly improve the efficiency of project development, testing, and release.
Make was originally designed to serve the compilation and building of C and C++ projects, so it has many features specific to C and C++. However, these features are not applicable in Go development. Since everyone’s time is limited, we will only learn the necessary knowledge for using Make in Go development.
The rules for Make are written in the Makefile file. In this article, we will learn about Makefile commands and variables.
The make command is not supported on Windows and needs to be run in a Linux or Mac environment.
Commands
Command Output
When running a target, the command is executed and by default, the command output is displayed in the console.
.PHONY: echo_test
echo_test:
echo "hello"
echo "world"
➜ make echo_test
echo "hello"
hello
echo "world"
world
If you add a @
symbol before the command, it will execute the command without displaying it.
.PHONY: echo_test
echo_test:
@echo "hello" # This command will not be displayed
echo "world"
➜ make echo_test
hello
echo "world"
world
The @
symbol only applies to a single command. If you want it to apply to all commands, you can use the -s
or --silent
option to silence the output:
➜ make -s echo_test
hello
world
If you want to only display the command without executing it, use the following options: make -n or make –just-print.
# .PHONY means this is a phony target
.PHONY: echo_test
echo_test:
echo "hello"
@echo "world"
➜ make -n echo_test
echo "hello"
echo "world"
Command Execution
If you want the second command to depend on the first command, you need to separate the commands with a semicolon; commands written on two lines are executed separately:
exec:
cd /tmp
pwd
➜ make -s exec
/Users/admin/makefile_study
From the above example, we can see that the second command did not execute in the /tmp directory, indicating that the first command did not affect the second command.
We can separate the two commands with a semicolon:
exec:
cd /tmp; pwd
➜ make -s exec
/tmp
Command Errors
When executing commands, errors may occur that affect subsequent commands. For some errors, we can ignore them and let the command continue executing. For example, if we want to create a folder that already exists, the creation will produce an error, but we need the folder to exist, so this error can be ignored.
The current directory test folder does not exist, so we will create a folder and a file, and display the file list:
exec:
@mkdir test; cd test; touch a.txt; ls
The first run is normal:
➜ make exec
a.txt
The second run will prompt an error, but the commands on the same line will continue executing.
➜ make exec
mkdir: test: File exists
a.txt
If there are multiple commands and the first command fails, the subsequent commands will not execute:
exec:
@mkdir test
@echo "hello world"
➜ make exec
mkdir: test: File exists
make: *** [exec] Error 1
If we want to ignore this error, there are several ways:
-
Add a minus sign ‘-‘ before the command to indicate that this error should be ignored and continue executing the following commands.
exec:
-@mkdir test
@echo "hello world"
➜ make exec
mkdir: test: File exists
make: [exec] Error 1 (ignored)
hello world
-
Add the .IGNORE flag to the rule to ignore errors in all commands of that rule.
.IGNORE: exec
exec:
@mkdir test
@echo "hello world"
➜ make exec
mkdir: test: File exists
make: [exec] Error 1 (ignored)
hello world
-
Add -i
or--ignore-errors
to make, so that all commands executed will ignore errors.
exec:
@mkdir test
@echo "hello world"
➜ make -i exec
mkdir: test: File exists
make: [exec] Error 1 (ignored)
hello world
Defining Command Templates
Sometimes there may be some commonly used functions, and we can abstract them into a template for other commands to call. The syntax for defining a template is: define
, followed by the template name; the commands in the middle, and finally ending with endef
.
define template-name
...commands...
endef
Using a command template is just like using a regular variable:
define my_template
@echo "this is a template"
endef
.PHONY: test
test:
$(my_template)
➜ make test
this is a template
Automatic Variables
When using command templates, it is often necessary to use automatic variables, which are similar to positional parameter variables in shell. The commonly used ones are:
$@ represents the target file
$^ represents all the dependency files
$< represents the first dependency file
$? represents the list of dependencies that are newer than the target
.PHONY: test
test: hello.go world.go
@echo $@ # test
@echo $^ # hello.go world.go
@echo $< # hello.go
@echo $? # hello.go world.go
We can define a template to output the target and all prerequisites, and then use this template:
define my_template
@echo $@ $^
endef
.PHONY: hello
hello: hello.go
$(my_template)
.PHONY: world
world: world.go
$(my_template)
➜ make hello
hello hello.go
➜ make world
world world.go
Variables
Declaring and Using Variables
Like other programming languages, Makefile can also define variables, which can be used in targets, prerequisites, and commands. Variable names consist of letters, numbers, and underscores (can start with a number), and are case-sensitive.
When defining variables, use ‘=’ to connect the variable name and value; spaces around ‘=’ are not sensitive. When using variables, you can use the form $+variable_name
, but the more recommended way is to use $(variable_name)
to wrap the variable.
name= tom
age = 18
info:
echo $name
echo $(age)
➜ make info
echo tom
tom
echo 18
18
Variables can also be used in targets:
MyTarget = info
$(MyTarget):
go build hello.go
go build world.go
➜ make info
go build hello.go
go build world.go
Variables can also be used in prerequisites:
files = hello.go
info: $(files)
go build $(files)
➜ make info
go build hello.go
Variables in Makefile will be expanded when used, directly replacing with the value you defined. You can use such operations, but they are not recommended:
type = go
build:clean
go build hello.$(type)
go build world.$(type)
.PHONY: clean
clean:
-@rm -f hello world
➜ make build
go build hello.go
go build world.go
Using Other Variables
We can use other variables within a variable:
name = tom
age = 18
detail = $(name) $(age)
info:
@echo $(detail)
➜ make info
tom 18
In our regular programming process, referenced variables must be defined beforehand, but in Makefile, there is no such restriction. You can reference variables defined later (which can be understood as lazy loading, only expanded when used, and at this time the entire file has been loaded, so it doesn’t matter if the used variable is defined before or after):
detail = $(name) $(age)
name = tom
age = 18
info:
@echo $(detail)
➜ make info
tom 18
If you can reference variables defined later, recursive situations may occur, such as the following example where a references b, and b references a. Makefile will detect this recursion:
a = $(b)
b = $(a)
info:
@echo $(b)
➜ make info
Makefile:4: *** Recursive variable `b' references itself (eventually). Stop.
Lazy Loading
As mentioned above, variables in Makefile are lazily loaded, meaning they are only expanded when used. This means that later defined variable values will overwrite previous ones:
name = tom
age = 18
detail = $(name) $(age)
info:
@echo $(detail)
age = 22
➜ make info
tom 22 # Overwritten by the later value
Of course, we can force the current variable value by using