Linux | Red Hat Certified | IT Technology | Operations Engineer
👇 Join the technical exchange QQ group of 1000 people, note “public account” for faster approval
Basic Structure
A simple Makefile usually includes the following parts:
Variable Definitions: You can define compilers, compilation options, source files, etc.
Rules: Specify how to build targets. The syntax format of rules is as follows:
target: dependency (a strict tab in front) command #eg. main: main.c gcc -c main.c -o main
Phony Targets: Such as clean, used for performing special tasks (e.g., cleaning up intermediate files).
Scripts: Variables
Makefile and scripts have some relationship, for example:
A Makefile defines the dependencies and build rules among various files in a project, used for automating the compilation and build process. A script (like a bash script) is a program executed by a command-line interpreter, typically used to execute a series of commands to accomplish specific tasks, such as automating system management, data processing, etc.
Both can be used for automating tasks, but Makefile focuses more on file generation and management, especially tasks related to code compilation. Scripts can be used for a wider range of tasks, including file processing, network requests, system monitoring, etc., beyond the scope of building.
Makefile is typically used in Linux systems with the make tool, which automatically selects the commands to execute according to the rules and dependencies. Scripts can run in any environment that supports the corresponding interpreter (such as bash, python, perl, etc.).
However, inevitably, both may require the same knowledge: variables!!!
This term is not unfamiliar to us, but what role do variables play in Linux? Usually, we may come across this concept when learning shell programming, so let’s take a brief look at it. Please move on to the main text below:
Shell Variables
Defining Variables
In shell programming, a variable is a name used to store data values. When defining a variable, the variable name does not need to have a dollar sign ($, which is required in PHP) in front, such as:
your_name="stark"
Note that there should be no spaces between the variable name and the equal sign, which may differ from all programming languages you are familiar with. At the same time, variable naming must follow these rules:
1. Only letters, numbers, and underscores: Letters are case-sensitive and cannot start with a number, similar to C/C++. Just be aware of this.
2. Avoid using keywords: Just like in C/C++, you should avoid keywords in Shell as well. Keywords to avoid include: (if, then, else, fi, for, while)
3. Use uppercase letters for constants: Shell constants are similar to macros, typically named in all uppercase letters, e.g., PI=3.14
4. Avoid special characters: Do not use special characters like $, @, * in naming, as these have specific functions and meanings in Shell syntax. Also, avoid spaces, as spaces are typically used to separate parameters and commands.
Tips: Most of these naming rules are consistent with C/C++, the only thing to note is that there should be no spaces around the equal sign, and you do not need to specify a data type.
# Valid naming: var1="123" var2="stark"# Invalid naming: var_$="33" # special character $? var="111" # special character? a*b="356" # special character* c and d="156" # avoid spaces
Using Variables
To use a defined variable, simply add a dollar sign $ in front of the variable name (pronounced: dollar–刀乐儿, also known as 美刀), such as:
var1="stark" echo $var1 echo ${var1}
The curly braces around the variable name are optional; it can be included or not. Adding curly braces helps the interpreter recognize the variable’s boundaries, as sometimes it may not be clear. For example:
skill="write_code" echo "I am good at ${skill}and others"
If we do not use curly braces to specify the boundaries, we would write:
echo “I am good at $skilland others”, the interpreter attempts to find the variable skilland, which is not what we expect. Therefore, a good programming practice is to add boundaries (curly braces) to all variables.
Already defined variables can be reassigned, which is also easy to understand; however, at this point, we do not need to add $ when using the variable, as technically, reassigning is redefining, and adding $ would replace the variable’s value, making it impossible to assign.
There is also a concept called: read-only variable. Using the readonly command can make a variable a read-only variable, meaning its value cannot be modified, similar to const in C/C++. The syntax is as follows:
c_var_myname="i am stark!" readonly c_var_myname
Here, when adding modifiers to the variable, we also do not need to use the $ symbol. Currently, it appears that only when intending to use the variable’s value do we use the $ symbol.
Deleting Variables
Unlike general programming languages, the Shell scripting language has the concept of deleting variables. The syntax is:
unset var_name
After a variable is deleted, it cannot be used again, and unset cannot delete read-only variables. You can think of a read-only variable as having only read permissions; its value cannot be changed, nor can its existence state be altered.
Comments and Quotes
Comments: (Shell Variables | Beginner Tutorial) I generally only use # single-line comments
# Single-line comment # Using Here Document: :<<eof #="" 'multi-line="" +="" --="" 1,="" <<="" <<!="" <<'comment="" <<'comment'="" be="" can="" code="" colon="" colon:="" comment="" comment:="" content="" content!="" content':="" content...="" directly="" eof="" is="" multi-line="" other="" quote="" quote'<="" replaced="" single="" space="" symbols:="" syntax="" use="" with=""></eof>
Quotes:
When defining variables, you can choose to add quotes or not. The quotes can be single or double quotes.
# Single quote str1='this is a string' # Double quote str2="this is a string" # No quotes var=hello
1. Limitations of Single Quotes:
Any character inside single quotes will be output as is; variables inside single quotes are ineffective.
A single quote cannot appear alone in a single-quoted string, and using an escape character on a single quote does not work.
2. Advantages of Double Quotes:
Variables can appear inside double quotes.
Escape characters can appear inside double quotes.
For example:
name="stark" var="hello,i am \" &{name} \"! \n" echo -e $var
Environment Variables
Special variables set by the operating system or user, used to configure Shell behavior and affect its execution environment. For example, the PATH variable contains the paths that the operating system searches for executable files:
echo $PATH
Special Variables
Some special variables in Shell have specific meanings, such as: $0 represents the script name, $1, $2, etc. represent script parameters, $# represents the number of parameters passed to the script, and $? represents the exit status of the previous command, etc.
With the above knowledge, we will not feel lost when using the more “advanced” usages of Makefile (the variables in Makefile differ slightly from the above variables, but it does not hinder understanding).
Makefile
sudo apt-get install make # Download make tool
Basic Principles:
1. To generate a target, check whether the dependency files in the rules exist; if not, look for rules to generate that dependency file.
2. Check whether the target in the rules needs updating; you must first check all its dependencies. If any dependency is updated, the target must be updated.
Analyze the relationships between various targets and dependencies Execute commands from bottom to top based on dependency relationships Determine updates based on modification time If the target does not depend on any conditions, execute the corresponding command to indicate an update
1 Rule:
object: link.c gcc link.c -o object # Template: target: dependency (tab) command
Example:
Create a test .c file: vim main.c
#include <stdio.h> int main(){ printf("------------"); return 0;}
Then create a Makefile file: vim makefile
main: main.c gcc main.c -o main
Then exit and go to the terminal to enter: make
After executing the make command, it will prompt the command automatically executed by make (gcc main.c -o main), and then use the ls command to check the current directory for files, finding an extra main file. This is equivalent to directly executing gcc main.c -o main. You may wonder why I should create a file and edit its contents according to the rules. Why not just execute the command directly? Certainly, but what if you need to use gcc in steps? Would you execute one command at a time each time? Then I configure the Makefile once, and afterward, just use make to do the job. (One-time setup, long-term benefit)
main: main.o gcc main.o -o main main.o: main.s gcc -c main.s -o main.o main.s: main.i gcc -S main.i -o main.s main.i: main.c gcc -E main.c -o main.i
Here, a series of four statements would be cumbersome to input every time, but now it only requires a single make command.However, we can also see: The execution order after make is according to the order of gcc step-by-step compilation, not the order we wrote in the file. Why is that? This is the first basic principle: Check whether the dependency files in the rules exist; if not, look for rules to generate that dependency file. While writing, our target is main, and the dependency file is main.o. Let’s check, the dependency file does not exist, so it will look for rules in the file that can generate that dependency file, finding the command of gcc -c, but the dependency file main.o also does not exist, so it continues to search until it finds main.c, and after generating main.i, it finally generates our target file main.
Next, based on this, we modify the main.c file to change its printed content and observe the result
Before the change, it output a line of asterisks:
Now, let’s check what files are in our directory:
Now, we re-execute the make command. Since we currently have the main.o file, do we need to look for rules to generate the dependency file? The answer is obviously YES!! Why? Please look at rule two: Check whether the target in the rules needs updating; you must first check all its dependencies. If any dependency is updated, the target must be updated. The target exists, so check its dependencies, and then check dependencies layer by layer. If any dependency in the chain has changed, then re-execute to update the target file. So, at this point, when executing main: It outputs a line of hyphens.
Next, we change main.c back: Then delete main, main.i, main.s, keeping main.o and main.c.
Now, initially the target does not exist, and we have the main.o dependency file. Are we going to compile and output a line of hyphens according to the current main.o, or recompile and output a line of asterisks? Please see the VCR:
The result is that main.o is updated, because at this point main.o, as a temporary target, will still look for its dependency chain, ultimately leading to main.o being updated, which in turn causes main to be updated.
However, do the .o, .s, .i files have to be written layer by layer in the makefile according to the dependency chain? Based on our execution order, it is obviously not necessary. But what if we try to write the rule for generating the main target below?
We execute make, and the result prompts that main.i is already up to date, indicating that make treated main.i as the final target file, but this is not our expected result; we expected to generate the main file.
We try to modify main.c to make main.i not up to date:
The result only executed one statement, and the remaining three rules’ target files are not in the dependency chain of main.i, so they will not be executed. At this point, we conclude:
The first rule will be treated by make as the final target file; as long as the target rule is the first valid line, the order of other rules does not affect the final result.
However, we can specify the final target by explicitly declaring it at the beginning, i.e., adding a line to the first valid line:
ALL: target_file
Now we have another requirement: How to encapsulate gcc main.c add.c sub.c -o a.out in the Makefile?
Currently, the only way we learned is to use it like this:
main: main.c add.c sub.c gcc main.c add.c sub.c -o main
Now, let’s start to get tricky: I first modify add.c, then make, and then execute ./main, the result is that the target has been updated. However, I only modified add.c, but executing gcc main.c add.c sub.c -o main caused sub.c to be recompiled as well. It seems unfair that add.c’s negligence caused sub.c to be dragged into overtime. By the way, add.c has changed, I only need to recompile it, right? But our previous syntax has already determined that all three must be compiled together to generate main; modifying one cannot go to modify main alone. So we need to redefine the rules:
Command format: (try writing it in Makefile rule form)
At this point, if any one of the files is changed, it will only recompile that one, and finally link them together for execution. We know that the linking phase is the most time-consuming, so we separate the most time-consuming part, leaving only some minor linking operations, which is still acceptable. It’s still the same example, add slacking off drags sub into overtime, but the boss is nice and only lets sub help add without doing the most tiring work.
2 Functions:
① src = $(wildcard *.c)
wildcard function: Finds all files with the .c suffix in the current directory and assigns them to src
wildcard: The Chinese meaning is wildcard. The syntax above is: define a variable src, call the function wildcard with the parameter *.c, and the result of the called function (file names) is composed into a list and assigned to src as the initial value.
② obj = $(patsubst %.c,%.o, $(src))
patsubst function: Replaces all files with the .c suffix in the src variable with .o
patsubst: Full name is “pattern substitution”, which means pattern replacement. This function can find words that match a specific pattern in given text and replace them with specified replacement text. In the above syntax: it replaces the text containing parameter 1 in parameter 3 with the text of parameter 2.
clean: clean is a set of rules without dependency files:
clean: -rm -f $(obj) a.out # The - before rm indicates: continue executing even if an error occurs # Terminal: localhost$ make clean -n # Simulate execution, will output the command to the terminal without executing localhost$ make clean # Execute clean
Next, let’s talk about the – before rm, now our directory has no aaa file, and we attempt to delete aaa:
But if we load – before rm, we will not be prompted for this error.
3 Automatic Variables:
Automatic Variables
Automatic variables can only appear in the command position; they cannot appear in the target and dependency positions.
① $@: represents the target in the rule /
② $<: represents the first dependency in the rule /
③ $^: represents all dependencies in the rule, forming a list separated by spaces; if there are duplicates in this list, duplicates are eliminated. /
The purpose of using automatic variables is to facilitate future expansion.
Pattern Rules
At least the target definition in the rule must include %, % represents one or more, and % can also be used in dependency conditions, the value of % in dependency conditions depends on its target.
%.o: %.c gcc -c $< -o $@
After this, we do not need to modify the Makefile when we create new .c files.
Static Pattern Rules:
$(obj): %.o: %.c gcc -c $< -o $@
Phony Targets:
.PHONY: all clean
In this statement, clean is a phony target.
Phony targets can be used to simplify the execution of common commands such as cleaning, installing, or testing, without relying on file generation.
Phony targets avoid conflicts with directory or file names. Since phony targets are always executed, their definitions are not affected by files with the same name, ensuring execution order and safety.
Using phony targets can improve the readability of the Makefile, helping developers better understand the available commands and operations.
Ultimate Form
# Variable Definitions CC = gcc CFLAGS = -Wall -g SRC = main.c demo01.c demo02.c OBJ = $(SRC:.c=.o) # Equivalent to OBJ = $(patsubst %.c,%.o, $(SRC)) TARGET = my_program # Default Rule all: $(TARGET) $(TARGET): $(OBJ) $(CC) $^ -o $@ %.o: %.c $(CC) -c $< -o $@ $(CFLAGS) # Clean Target clean: -rm -f $(OBJ) $(TARGET) # Phony Target .PHONY: all clean
Explanation:
1. Variable Definitions: CC: Specifies the compiler CFLAGS: Compilation options, -Wall enables warnings, -g generates debugging information (in addition, there are preprocessor options CPPFLAGS: like -I; linker options LDFLAGS: like -L -l) SRC: List of source files OBJ: Converts the list of source files to the list of target files. TARGET: Name of the final executable file 2. Default Rule: The target all is used by default to generate the executable file. $(TARGET) depends on the object files $(OBJ) and defines how to link these object files through rules 3. Pattern Rules: %.o: %.c rules allow each .c file to automatically generate the corresponding .o file 4. Cleaning: The clean target is used to delete temporary files and the final executable file generated during compilation, using rm -f for forced deletion 5. Phony Target: .PHONY directive indicates that clean and all are phony targets, which will not be affected by files with the same name.
Usage
Open the command line in the project directory and run the following commands:
make # Compile the project make clean # Clean the project
By using Makefile, the process of compiling and managing projects can be simplified, especially in large projects.
However, every time we use make or make clean, we notice that the commands used in the file are output to the terminal. To make the encapsulation more thorough, we can choose to add @ in front of the commands for anonymous execution:
Our gcc line is already executed anonymously, so the gcc command itself is not output to the terminal, while the echo command did not use the anonymous method @, so both the command itself and the command result are output to the terminal.
Thank you everyone!!! Finally, here is a commonly used Makefile content for C language:
# Compilation Variables --------------------------------------------------------------------------------- CC = gcc # Specify compiler # CPPFLAGS = -l # Preprocessing option CFLAGS = -Wall -g # Compiler options LDFLAGS = -l math -L /home/stark/class/filec/library/lib # Linker options # If not using dynamic libraries, this part can be omitted # File Variables --------------------------------------------------------------------------------- TARGET = a.out # Set ultimate target SRC = $(wildcard *.c) # All .c files as source files OBJ = $(patsubst %.c,%.o, $(SRC)) # Replace text .c -> .o # OBJ = $(SRC:.c=.o) # Define Rules --------------------------------------------------------------------------------- ALL: $(TARGET) # Specify ultimate target $(TARGET): $(OBJ) # Ultimate target formation rule $(CC) $< -o $@ $(LDFLAGS) %.o: %.c # Dependency -- OBJ generation rule $(CC) -c $^ -o $@ $(CFLAGS) # ---------------------------------------------------------------------------------------- # Phony Target .PHONY: all clean clean: rm -f $(OBJ) $(TARGET)
Course consultation add: HCIE666CCIE
↓ Or scan the QR code below ↓
What technical points and content do you want to see?
You can leave a message below to tell me!