banner
Zein

Zein

x_id

make Best Practices Handbook

# Phony targets
PHONY := __build
__build:             # No operation
 
# Clear required variables
obj-y :=            # List of target files to compile
subdir-y :=          # List of subdirectories
EXTRA_CFLAGS :=      # Extra compilation options
 
# Include sibling directory Makefile
include Makefile
 
# Get the directory names of the subdirectories that need to be compiled in the current Makefile
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y))   : c/ d/
# __subdir-y  : c d
# subdir-y    : c d
__subdir-y  := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y  += $(__subdir-y)
 
# Generate the list of target files for each subdirectory: target files in subdirectories are packed into dir/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
 
# Filter obj-y to get the filenames that need to be included in the program as targets
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
# Ensure that modifying header files .h allows recompilation after re-running make (important)
dep_files := $(foreach f,$(cur_objs),.$(f).d)
# Include all dependency files
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)        # Not empty (i.e., there are dependency files), then execute the next operation
  include $(dep_files)  # The Make tool will read these dependency files to ensure that the Makefile knows the correct dependency relationship between source files and header files, allowing recompilation of affected source files when header files are modified
endif


PHONY += $(subdir-y)
# First target
__build : $(subdir-y) built-in.o
# Prioritize compiling content from subdirectories; -C switches to the subdirectory to execute $(TOPDIR)/Makefile.build
$(subdir-y):
  make -C $@ -f $(TOPDIR)/Makefile.build 
 
# Link the built-in.o from subdir and cur_objs into the overall built-in.o target
built-in.o : $(cur_objs) $(subdir_objs)
  $(LD) -r -o $@ $^

dep_file = [email protected]
 
# Generate cur_objs target
%.o : %.c
  $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
  
.PHONY : $(PHONY)

Makefile in subdirectory for adding obj-y information#

# The following two variables can be omitted
EXTRA_CFLAGS  := 
CFLAGS_view.o := 

obj-y += $(patsubst %.c,%.o,$(shell ls *.c))
# obj-y += subdir/

The automation build tool needs to achieve:

  1. If the project has never been compiled, all files must be compiled and linked.
  2. If certain files in the project are modified, only the modified files should be compiled and linked to the target program.
  3. If the project's header files are changed, compile the C files that reference these header files and link to the target program.
    *) Package some shell commands for easy project management, such as packaging, backing up, and cleaning intermediate files.

Running make#

Starting make#

Default script priority: GNUmakefile > makefile > Makefile

make -f custom.mk       # Specify the entry makefile

Command printing#

When make executes a command, it will print the command; using the @ character in front of the command will prevent it from being printed;
For example, echo Compiling XXX module, the terminal displays:
echo Compiling XXX module Compiling XXX module
If it is @echo Compiling XXX module, it will not output.

make -s           # Silent execution of command

Shell environment for command execution#

The make command uses the shell specified by the environment variable SHELL by default. If the user does not specify SHELL, the Unix shell defaults to $(SHELL)=/bin/sh; In a Windows environment, make will automatically look for a suitable command interpreter, such as cmd.exe or the Unix-style shell you specified (e.g., bash).

-------------------To apply the result of the previous command to the next command, separate the two commands with a semicolon. If written on separate lines, the effect of the previous command will not be retained.
.RECIPEPREFIX = >
exec:
>  cd /home/hchen; pwd
# Prints /home/hchen

.RECIPEPREFIX = >
exec:
>  cd /home/hchen
>  pwd
# Prints /home

Handling make command failures#

————————Ignore errors and do not terminate the execution of make
clean:
    -rm -f *.o           # Add a minus sign in front of the command

.IGNORE: clean           # Special target .IGNORE specifies that the clean rule ignores errors and does not terminate the execution of make
clean:
    rm -f *.o

make -i                  # Use -i or --ignore-errors option
make --ignore-errors

————————Only skip the current failed rule and continue executing other rules, will not terminate make execution
make -k
make --keep-going

Viewing make rules#

make -p                # Output all rules and variables.
make -n                # Print the rules and commands that will be executed, but do not execute them
make --debug=verbose   #
make -t                # Equivalent to UNIX touch, updates the modification date of the target to the latest, equivalent to a fake compile, just marks the target as compiled
make -q                # Check if the target exists, the return code indicates the result (0=needs updating, 2=error).
make -W <file>         # Generally specify source files (or dependency files), Make deduces the commands that depend on this file based on the rules, usually used with -n to view the rule commands that occur for this file

Exiting make#

Make exit codes:
1)0: Execution successful.
2)1: An error occurred during execution.
3)2: When the -q option is enabled, the target does not need to be updated.

Make parameters#

OptionFunction
-b, -mIgnore warnings related to compatibility with other versions.
-B, --always-makeForce recompilation of all targets.
-C <dir>Switch to the specified directory to run Makefile.
--debug[=<options>]Output debug information, <options> includes all, basic, verbose, implicit, jobs, makefile.
-dEquivalent to --debug=all
-e, --environment-overridesEnvironment variables override variables defined in the Makefile.
-f=<file>Specify the Makefile file.
-hDisplay help information
-i, --ignore-errorsIgnore all errors.
-I <dir>Specify a search path for importable makefiles. Multiple "-I" parameters can be used to specify multiple directories.
-j [<jobsnum>]Specify the maximum number of parallel tasks to execute.
-k, --keep-goingIgnore failed targets and continue processing other targets.
-l <load>Specify the maximum load value allowed; if exceeded, pause new tasks.
-nPrint the rules and commands that will be executed, but do not execute them.
-o <file>Do not regenerate the specified <file>, even if the target's dependency files are newer than it.
-p, --print-data-baseOutput all rules and variables.
-q, --questionCheck if the target exists, the return code indicates the result (0=needs updating, 2=error).
-rDisable all built-in rules.
-RDisable all built-in variables.
-s, --silentSuppress command output.
-S, --no-keep-goingDisable the effect of the -k parameter.
-tEquivalent to UNIX touch, updates the modification date of the target to the latest, equivalent to a fake compile, just marks the target as compiled.
-v, --versionDisplay version information.
-w, --print-directoryDisplay the current directory and nested call information.
--no-print-directorySuppress the "-w" option.
-W <file>Generally specify source files (or dependency files), Make deduces the commands that depend on this file based on the rules, usually used with -n to view the rule commands that occur for this file.
--warn-undefined-variablesWarn about undefined variables.

Rules#

A build rule consists of dependencies and commands:
target: The target file, executable file, or label to be generated.
prerequisites: The source files or intermediate files that the target file depends on.
command: The shell command called; usually the build tool command; must start with a tab character Tab; or the command can be on the same line as the dependencies, separated by ;.

When executing the make command, Make will:

  1. Look for the Makefile or makefile file in the current directory. You can also specify the path make -f ~/makefile.
  2. Look for the first target in the Makefile; you can also specify the target make target.
  3. Check if the target's prerequisites (.c/.o) need to be updated. If the prerequisites are newer than the target or the target does not exist, execute the command.
  4. Prerequisites (.o) may also depend on other files (.c); Make will recursively trace the dependencies of .o files until the final source file .c is compiled.
  5. If there are compilation errors or missing files, Make will report an error and terminate execution; adding - in front of the command ignores warnings about missing files and continues execution.

For example: If the file.c file is modified, file.o will be recompiled, and edit will be relinked.

target: prerequisites; command; command
    command
    command

Nested execution of make#

In large projects, dividing the code into multiple modules or subdirectories, each maintaining an independent Makefile, is a common project management approach. This method makes the compilation rules for each module clearer and simplifies the maintenance of the Makefile.

Used shell variables:
MAKE: A special variable in GNU make used for recursively calling make itself; assuming make -j4 is executed in the shell, the main Makefile has $(MAKE) -C subdir; this is equivalent to executing make -j4 -C subdir in the subdir.
MAKEFLAGS: A system-level variable that is automatically passed to the lower-level Makefile; it can be set to empty to prevent passing (MAKEFLAGS=).
MAKELEVEL: A system variable that records the current nesting level of Make execution. If you have nested make calls, MAKELEVEL will tell you which level you are currently at.

Assuming there is a subdirectory called subdir, and there is a Makefile in this directory that specifies the compilation rules for the files in this directory.

Main control Makefile#

.RECIPEPREFIX = >
# If the current make is executed at the outermost level (MAKELEVEL= 0); define some system-related variables,
ifeq (0,${MAKELEVEL})
    cur-dir   := $(shell pwd)
    whoami    := $(shell whoami)  # Current user
    host-type := $(shell arch)    # Architecture
    MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif

.PHONY: all subsystem

all: subsystem   # all depends on subsystem 

subsystem:
>  $(MAKE) -C subdir MAKEFLAGS= VAR=value     # -C subdir indicates executing make in the specified directory
# or>  cd subdir && $(MAKE)

subdir/Makefile#

.PHONY: all

all:
    echo "Received VAR = $(VAR)"

Execution result of the example:

-w or --print-directory is a good helper for debugging nested Makefiles, as it will output the current working directory information. If the -C parameter is used, -w will be automatically enabled. If -s or --silent parameters are included, then -w will be disabled.

make: Entering directory `subdir'
Received VAR = value
make: Leaving directory `subdir'

Command package#

define <command package name>
<command1>
<command2>
...
endef

-------eg
.RECIPEPREFIX = >

define run-yacc
yacc $(firstword $^)    # $(firstword $^) will get the first dependency file from $^.
mv y.tab.c $@           # Rename the generated y.tab.c file to foo.c
endef

foo.c : foo.y
>  $(run-yacc)

Variables#

Variables are used to store strings, similar to macros in C/C; they improve the maintainability of scripts and avoid duplicate code; for example, you only need to maintain the objects variable without modifying the rules;

Naming rules: can include letters, numbers, underscores (_), and can start with numbers. Case-sensitive; system variables are usually all uppercase, such as CC, CFLAGS. User-defined variables are recommended to use camel case, such as MyFlags.
Declaration: VAR = value
Reference: $(VAR)

# Change the command prefix to > instead of the default tab (tab)
.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

# First target
edit: $(objects)
    cc -o edit $(objects)

# Rules for each target file
main.o: main.c defs.h
    cc -c main.c
kbd.o: kbd.c defs.h command.h
    cc -c kbd.c
# Other .o file rules...

Automatic variables#

Meaningeg
$@Takes the target from the target set one by one and executes it in the command
$<Represents the dependency file taken from the dependency set one by one and executes it in the command
$^The collection of all dependency files (deduplicated)
$+The collection of all dependency files (not deduplicated)
$?All dependency files that are newer than targettarget: dependency1 dependency2 command $? If dependency1 is newer than target, then $? is dependency1.
$*Takes the target from the target set of the format rule, removes the suffix, and retains the path parttarget: %.c command $* If the target is file.c, then $* is file.
$%Only when the target is a static library, represents the members in the library one by onelibfoo.a(bar.o): bar.o ar r $@ $% If the target is the static library libfoo.a, then $% is the member bar.o.
$(@D)Takes the directory part of the target from the target set one by one and executes it in the commandtarget = dir/subdir/foo.o $(target): @echo $(@D) # dir/subdir
$(@F)Takes the filename part of the target from the target set one by one and executes it in the commandtarget = dir/subdir/foo.o $(target): @echo $(@F) # foo.o
$(<D)Takes the directory part of the dependency file from the dependency set one by one and executes it in the commandtarget: dir/file.c command $(<D) If the first dependency file is dir/file.c, then $(<D) is dir.
$(<F)Takes the filename part of the dependency file from the dependency set one by one and executes it in the commandtarget: dir/file.c command $(<F) If the first dependency file is dir/file.c, then $(<F) is file.c.
$(^D)The collection of directory parts of all dependency files (deduplicated)target: dir/file1.c dir/file2.c command $(^D) If the dependency files are dir/file1.c and dir/file2.c, then $(^D) is dir.
$(^F)The collection of filename parts of all dependency files (deduplicated)target: dir/file1.c dir/file2.c command $(^F) If the dependency files are dir/file1.c and dir/file2.c, then $(^F) is file1.c file2.c.
$(+D)The collection of directory parts of all dependency files (not deduplicated)target: dir/file1.c dir/file2.c dir/file1.c command $(+D) If the dependency files are dir/file1.c dir/file2.c dir/file1.c, then $(+D) is dir dir dir.
$(+F)The collection of filename parts of all dependency files (not deduplicated)target: dir/file1.c dir/file2.c dir/file1.c command $(+F) If the dependency files are dir/file1.c dir/file2.c dir/file1.c, then $(+F) is file1.c file2.c file1.c.
$(?D)The directory parts of updated dependency filestarget: file1.c file2.c command $(?D) If file1.c is updated, then $(?D) is file1.
$(?F)The filename parts of updated dependency filestarget: file1.c file2.c command $(?F) If file1.c is updated, then $(?F) is file1.c.
$(*D)Takes the target from the target set of the format rule, removes the suffix and filename part%.o: %.c @echo $(*D)
$(*F)Takes the target from the target set of the format rule, removes the suffix and directory part%.o: %.c @echo $(*F)

Static pattern rules:

  1. <targets ...> Target set, can have wildcards.
  2. <target-pattern> Target set file format, matches the new target set from <targets ...>.
  3. <prereq-patterns ...> Dependency file set format, matches the dependency set of the new target set.
.RECIPEPREFIX = >
bigoutput littleoutput: text.g
>  generate text.g -$(subst output,,$@) > $@              # Parameter replacement: $(subst output,,$@) means replacing output in $@ with an empty string.

# Equivalent to above
.RECIPEPREFIX = >
bigoutput: text.g
>  generate text.g -big > bigoutput

littleoutput: text.g
>  generate text.g -little > littleoutput

-------------------------------------Static pattern rules
.RECIPEPREFIX = >
<targets ...> : <target-pattern> : <prereq-patterns ...>
>  <commands>

#eg
.RECIPEPREFIX = >
objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c                    # Matches the target file format of %.o, taken one by one from $@; the dependency file format for the target set is matched as %.c, taken one by one from $<
>  $(CC) -c $(CFLAGS) $< -o $@

# Equivalent to above
foo.o: foo.c
>  $(CC) -c $(CFLAGS) foo.c -o foo.o

bar.o: bar.c
>  $(CC) -c $(CFLAGS) bar.c -o bar.o

Command line environment variables#

In addition to the variables defined within the Makefile, the makefile can reference 1) os environment variables and 2) command line environment variables passed via make options using $(var).

Priority of same-named variables: command line passed variable value > Makefile defined variable value > os environment variable.

  1. To elevate the priority of Makefile defined variable values: use the override variable prefix.
  2. To elevate the priority of os environment variables: use make -e.

For example: make CFLAGS="-O2 -Wall" passes the CFLAGS environment variable; export CC=gcc passes the os environment variable CC.

# Makefile
all:
    echo $(CFLAGS)      # Passed in -O2 -Wall
    echo $(CC)

$$ in bash represents the current process's PID (process ID).
$$$$ will insert the last four digits of the current process's PID. This is usually used to generate unique temporary filenames.

Passing variables between makefiles#

export                       # Pass all variables
export variable += value;    # Pass variable variable to the lower-level Makefile
unexport variable := value;  # Do not want variable variable to be passed to the lower-level Makefile

Assignment#

The value of a variable can depend on the value of other variables.

-----------------=  Recursive assignment: expands the dependent variable values recursively; this assignment method is flexible and can postpone variable definitions, but the downside is that it can lead to infinite recursive assignments, and each time the dependent variable is redefined, it must be recalculated.
all:
    echo $(foo)
    
foo = $(bar)
bar = $(ugh)
ugh = Huh?

-----------------:=  Immediate assignment: expands the right-side expression at the time of assignment and stores the result in the variable, which is more efficient than recursive assignment and safer.
x := foo
y := $(x) bar    # foo bar; since it was immediately expanded, the next line does not affect the value of y
x := later

-----------------?=  Conditional assignment: only takes effect if the variable is undefined; does nothing if the variable is already defined, rather than overwriting the original definition.
FOO ?= bar

-----------------# Can be used not only for comments but also to mark the end of variable definitions.
dir := /foo/bar    # directory to put the frobs in

-----------------+=  Append variable value; if the original variable is defined with :=, then += is immediate assignment; if the variable is defined with =, then += is recursive assignment.
objects = main.o foo.o bar.o utils.o
objects += another.o             # Equivalent to objects = $(objects) another.o

-----------------Specify that certain variables defined in the makefile have the highest priority and will not be overwritten by command line variables passed to make.
override <variable> := <value>
override <variable> += <more text>
override <variable> = <value>

-----------------Multi-line variable definition: similar to command packages.
define <variable>
<value>
endef

Variable value replacement#

-----------------Simple replacement
${var:a=b}   # Replace parts ending with a in variable var with b.

foo := a.o b.o c.o
bar := $(foo:.o=.c) 

-----------------Pattern replacement
$(var:%.suffix1=%.suffix2)   # Replace %.suffix1 in variable var with %.suffix2.

foo := a.o b.o c.o
bar := $(foo:%.o=%.c)

Target-specific Variable#

Variables are only effective for specific targets and their dependencies, avoiding affecting the settings of other targets.

-----------------------------------------Variable expressions are only effective for specific target <target> and its dependencies.
<target> : <variable-assignment>              # Variable expressions are only effective for specific target <target> and its dependencies.
<target> : override <variable-assignment>
#eg: Regardless of the global value of CFLAGS, the target prog and its related targets (prog.o, foo.o, bar.o) will use CFLAGS = -g.
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
    $(CC) $(CFLAGS) prog.o foo.o bar.o

prog.o : prog.c
    $(CC) $(CFLAGS) prog.c

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

bar.o : bar.c
    $(CC) $(CFLAGS) bar.o

---------------------------Variable expressions are only effective for specific format targets.
<pattern ...>; : <variable-assignment>;
<pattern ...>; : override <variable-assignment>;
%.o : CFLAGS = -O2           # Variable expressions are only effective for targets ending with .o.

Dynamic variable names#

dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)        # $(dir)_sources is defined as all c files in the $(dir) directory.
define $(dir)_print                             # Even dynamically define command packages.
lpr $($(dir)_sources)                           # Print the value of $(dir)_sources.
endef

Escaping special values#

$$ In the Makefile, a single $ is used for variable referencing. Any $$ will be treated as an escaped $ character.

-----------------Define a variable containing spaces
nullstring :=
space := $(nullstring)

Shell wildcards:
*(matches any length of characters)
? (matches a single character)
~ (home directory or environment variable)

If the special variable VPATH is not specified, make will only look for dependency files and target files in the current directory. If this variable is defined, then make will look for files in the specified directories when it cannot find them in the current directory.

VPATH = src:../headers       # Now look in the current directory first, then in src and ../headers.

vpath <pattern> <directories>
vpath %.h ../headers     # When not found in the current directory; search for all files ending with .h in the ../headers directory; .. is the parent directory.
vpath %.c foo            # .c files, search in the order of "foo", "blish", "bar".
vpath %   blish
vpath %.c bar

objects := $(wildcard *.o)                        # Expand wildcard *.o, listing all .o files.

$(patsubst %.c,%.o,$(wildcard *.c))               # Replace %.c strings with %.o strings, listing all .c files corresponding to .o files.

objects := $(patsubst %.c,%.o,$(wildcard *.c))    # Compile and link all .c and .o files.
foo : $(objects)
>  cc -o foo $(objects)

Implicit rules#

You only need to write the rules for the project linking phase, while the build rules from source files to target files can be automatically deduced by make based on 1) built-in rule library, user-defined 2) pattern rules, and 3) suffix rules.

Implicit rule priority: Rules match in built-in order, and higher-priority rules will override lower-priority ones; make allows overriding the default implicit rules; generally, this is done in the form of Pattern Rules.

Below are commonly used built-in automatic deduction rules:

targetprerequisitescommand
<n>.o<n>.c$(CC) -c $(CFLAGS) $(CPPFLAGS)
<n>.o<n>.cc / <n>.C$(CXX) -c $(CXXFLAGS) $(CPPFLAGS)
<n><n>.o$(CC) $(LDFLAGS) <n>.o $(LDLIBS)
<n>.c<n>.y$(YACC) $(YFLAGS)
<n>.c<n>.l$(LEX) $(LFLAGS)

And the default variables:

Command variableDescriptionDefault value
ARArchive utilityar
ASAssembleras
CCC compilercc
CXXC++ compilerg++
COExpand files from RCSco
CPPC preprocessor$(CC) -E
FCFortran and Ratfor compilerf77
LEXLexical analyzer for C or Ratforlex
PCPascal compilerpc
YACCYacc parser for C programsyacc
MAKEINFOProgram to convert Texinfo files to Info formatmakeinfo
Parameter variableDescriptionDefault value
ARFLAGSParameters for the AR commandrv
ASFLAGSParameters for the assemblerEmpty
CFLAGSParameters for the C compilerEmpty
CXXFLAGSParameters for the C++ compilerEmpty
COFLAGSParameters for RCS commandsEmpty
CPPFLAGSParameters for the C preprocessorEmpty
FFLAGSParameters for the Fortran compilerEmpty
LDFLAGSParameters for the linkerEmpty
LFLAGSParameters for the Lexical analyzerEmpty
PFLAGSParameters for the Pascal compilerEmpty
RFLAGSParameters for the Ratfor compilerEmpty
YFLAGSParameters for the Yacc parserEmpty

Automatically deduced build rules#

GNUmake can automatically deduce certain prerequisites to the target build rules, for example, each x.o file in a C project must have a corresponding x.c dependency, and there must be a cc -c x.c command.

The problem with automatic deduction is the priority of rule matching: for example, if you explicitly specify foo.o : foo.p; but there exists foo.c in the file directory; at this point, it will automatically deduce the rule to build foo.o from foo.c; make -r or make --no-builtin-rules can disable all built-in implicit rules. Adding explicit rules can override implicit rules.

.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit: $(objects)
    cc -o edit $(objects)

# Automatic deduction rules, make will deduce these rules
main.o: defs.h
kbd.o: defs.h command.h
command.o: defs.h command.h
# Other rules...

Another style: actually further merging the targets with the same .h files; I don't like it.

.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit: $(objects)
    cc -o edit $(objects)

# Merge dependencies of multiple files
$(objects): defs.h
kbd.o command.o files.o: command.h
display.o insert.o search.o files.o: buffer.h

.PHONY: clean
clean:
    rm edit $(objects)

Pattern Rules#

Use a generic format to describe the relationship between a set of targets and dependency files. Use % in the target or dependency files to represent any character or part of a filename.

#eg: Overriding the built-in .c to .o rule with Pattern Rules
%.o : %.c
    $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

%.o : %.s    # Cancel the built-in implicit rule, just don't write a command after it.

Suffix rules#

Maintaining compatibility with old rules is not recommended; look at such projects again when encountered.

Referencing other Makefiles#

Load the rules and variables from other Makefile files into the current Makefile; when using include to introduce files, Make will look for files in the following locations:

  1. Current directory
  2. Directory specified with -I
  3. Default system directories (like /usr/include)
-include other.make *.mk $(othermake)         # Can be a filename or use wildcards.

sinclude other.make *.mk $(othermake)         # Same as above.

Phony targets#

Indicate that the target is not a file but an operation label following make. Used for packaging shell commands; phony targets cannot have the same name as files; to ensure this, explicitly declare with .PHONY, so that even if a file with the same name exists, the phony target can still be executed.

Phony targets can depend on other phony targets, forming a hierarchical structure. Operations are executed in depth-first order.

GNU conventions:

  1. all: Default target, compiles everything.
    2) clean: Deletes all generated files, including target files and compressed files.
    3) install: Installs the compiled program into the system path.
    4) print: Lists source files modified since the last build.
    5) tar: Packages source programs and related files, generating a .tar file.
    6) dist: Compresses the tar file, generating a .tar.gz file.
    7) TAGS: A placeholder rule that can integrate ctags or etags to generate code indexing.
    8) check/test: Runs tests, usually linked to unit testing frameworks or scripts.
.RECIPEPREFIX = >
.PHONY: cleanall cleanobj cleandiff

cleanall: cleanobj cleandiff
>  rm -f program

cleanobj:
>  rm -f *.o

cleandiff:
>  rm -f *.diff

Automatically generating dependencies#

Make can determine which .c and .h files have been modified that need to be recompiled based on dependencies; automatically generating dependencies avoids the manual management difficulties of large projects.
gcc -MM file.c generates dependencies: file.o: file.c defs.h
gcc -M main.c generates dependencies (including standard library header files): main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h \ /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
The GNU organization recommends generating a Makefile for each name.c file that stores the dependencies corresponding to .d. Let make automatically update or generate .d files and include them in the main Makefile to automate the generation of dependencies for each file.
sed 's,pattern,replacement,g': String replacement command.

  1. pattern: The content to match; \($*\)\.o[ :]*; $ represents matching zero or more characters, * indicates that the preceding element appears zero or more times, the capture group \(\) indicates that the string will be captured into $*; \.o indicates matching .o; [ :]* indicates matching multiple :.
  2. replacement: The content to replace the regex match; \1.o $@ :; \1 indicates the first capture group; $@ indicates the target in the Makefile rule; replaced with main.o main.d :; thus, the .d file will also be automatically updated and generated.
  3. g: Indicates replacing all matching places.
.RECIPEPREFIX = >

sources = foo.c bar.c

%.d: %.c
>  @set -e; rm -f $@; \                                     # @set -e sets the script to exit immediately if an error is encountered (command return value is non-zero); rm -f $@ deletes the existing intermediate file.
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \                    # Build dependencies one by one from %.c, redirecting to a temporary file $@.$$$$, equivalent to %.d.$$$$.
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \   # < $@.$$$$ indicates reading input from the file %.d.$$$$; the replaced content is output to %.d; this step is for automatic updating of sub.d.
    rm -f $@.$$$$
    
include $(sources:.c=.d)            # Replace all .c strings in the variable $(sources) with .d; then include the dependency relationships.

Intermediate target handling#

.INTERMEDIATE: mid         # Explicitly declare that the mid file is an intermediate target. Make will not treat it as a final target; after generating the mid file, it will not be automatically deleted.
.SECONDARY: sec            # The sec file is an intermediate target, but make will not delete it after generating the final target.
.PRECIOUS: %.o             # Instruct make to keep all .o files, even if they are intermediate targets.

Conditional expressions#

<conditional-directive>
<text-if-true>             #
else
<text-if-false>
endif

-----------------ifeq and ifneq
.RECIPEPREFIX = >
foo = bar

ifeq ($(foo),bar)
>  echo "foo is bar"
endif

ifneq ($(foo),baz)
>  echo "foo is not baz"
endif

-----------------ifdef and ifndef
.RECIPEPREFIX = >
foo = bar

ifdef foo
>  echo "foo is defined"
else
>  echo "foo is not defined"
endif

Functions#

Function call syntax#

$(<function> <arguments>)
${<function> <arguments>}

# <function> is the function name; <arguments> are parameters, separated by commas; function name and parameters are separated by spaces.

String processing#

$(subst <from>,<to>,<text>)   # String replacement: Replace <from> in the string <text> with <to>; and return.
#eg: $(subst ee,EE,feet on the street) 

$(patsubst <pattern>,<replacement>,<text>)   # String replacement: Replace <pattern> in the string <text> with <replacement>; and return.
#eg: $(patsubst %.c,%.o,x.c.c bar.c) 

$(strip <string>)   # Remove leading and trailing spaces from the string.
#eg: $(strip  a b c  ) 

$(findstring <sub_text>,<text>)   # Find substring <sub_text> in the string <text>.
#eg: $(findstring a,a b c) # Return value: a

$(filter <pattern...>,<text>)   # Filter out parts of <text> that match the format <pattern...>.
#eg: $(filter %.c %.s,bar.c baz.s ugh.h) 

$(filter-out <pattern...>,<text>)   # Filter out parts of <text> that do not match the format <pattern...>.
#eg: $(filter-out main1.o main2.o,main1.o foo.o main2.o) 

$(sort <list>)   # Sort and deduplicate.
#eg: $(sort foo bar lose foo) 

$(word <n>,<text>)   # Extract the nth word from <text>.
#eg: $(word 2,foo bar baz)  # Returns bar

$(wordlist <start>,<end>,<text>)   # Extract a range of words.
#eg: $(wordlist 2,3,foo bar baz)  # Return value: bar baz

$(words <text>)   # Count the number of words.
#eg: $(words foo bar baz)    # Return value: 3

$(firstword <text>)   # Get the first word.
#eg: $(firstword foo bar) 

Filename operations#

$(dir <path...>)            # Extract the directory part.
#eg: $(dir src/foo.c hacks) # Return value: src/ ./ 

$(notdir <path...>)      # Extract the non-directory part.
#eg: $(notdir src/foo.c hacks)      # Return value: foo.c hacks

$(suffix <names...>)      # Extract the file suffix.
#eg: $(suffix src/foo.c src-1.0/bar.c hacks) # Return value: .c .c

$(basename <names...>)      # Extract the file prefix.
#eg: $(basename src/foo.c src-1.0/bar.c hacks)    # Return value: src/foo src-1.0/bar hacks

$(addsuffix <suffix>,<names...>)      # Add suffix <suffix> to files in <names...>.
#eg: $(addsuffix .c,foo bar)          # Return value: foo.c bar.c

$(addprefix <prefix>,<names...>)      # Add prefix <prefix>.
#eg: $(addprefix src/,foo bar)        # Return value: src/foo src/bar

$(join <list1>,<list2>)      # Join two lists.
#eg: $(join aaa bbb,111 222 333)     # Return value: aaa111 bbb222 333

Advanced functions#

$(foreach <var>,<list>,<text>)            # Loop through the list: Take words from <list> one by one, assign them to <var>, and then compute using the expression <text>, ultimately returning a string joined by spaces from each computation result.
#eg: files := $(foreach n,a b c d,$(n).o)     # Return value: a.o b.o c.o d.o

$(if <condition>,<then-part>,<else-part>)      # Conditional judgment  
#eg: $(if 1,yes,no)     #yes

$(call <expression>,<parm1>,<parm2>,...)      # Custom function call: Use placeholders $(1), $(2), etc., to represent parameters, pass them to <expression> for computation and return.
#eg: 
reverse = $(2) $(1)
foo = $(call reverse,a,b) 
# Return value: b a

$(origin <variable>)      # Check the source of the variable.
#Return value:
undefined:    Undefined
default:      Default definition
environment:  Environment variable
file:         Defined in Makefile
command line: Command line definition
override:     Override definition
automatic:    Automatic variable

$(shell <command>)      # Execute shell command.
#eg: files := $(shell echo *.c) 

$(error <text...>)      # Terminate the execution of make and display the specified error message.
#eg: Check if the variable is empty.
MY_VAR :=

ifeq ($(MY_VAR),)
$(error MY_VAR is not defined or is empty)
endif

$(warning <text...>)      # Output a warning, but does not interrupt the build process.
#eg: Output debug information.
MY_VAR :=

ifeq ($(MY_VAR),)
$(warning MY_VAR is not defined or is empty)
endif

Updating function library files#

-j parallel building of libraries may affect ar packaging.

#eg: Package all .o files into the foolib function library.
foolib(*.o) : *.o
    ar cr foolib *.o

Template#

tree#

<PROJECT_NAME >/
├── build/          # Stores compiled target files, dependency files, etc.
├── bin/            # Stores the final generated executable files.
├── inc/            # Stores header files.
├── lib/            # Stores library files.
├── src/            # Stores source code files (.c).
├── tests/          # Stores test code.
├── submodule1/     # First submodule.
│   └── Makefile    # Makefile for the submodule.
├── submodule2/     # Second submodule.
│   └── Makefile    # Makefile for the submodule.
├── Makefile        # Top-level Makefile.
└── <other files>   # May include other documents, configuration files, etc.

Main control makefile#

Delete generated target files to allow recompilation. A more robust approach is to use .PHONY declaration and add - before the delete command, so that even if some files do not exist, it will not report an error:

.RECIPEPREFIX = >
# -------------------------------
# General settings
# -------------------------------
PROJECT_NAME := 
VERSION := 1.0.0

INC_DIR := inc
SRC_DIR := src
LIB_DIR := lib
BIN_DIR := bin
TEST_DIR := tests
BUILD_DIR := build
SUBMODULES := submodule1 submodule2
INSTALL_DIR := /usr/local/bin
TAR_FILE := $(PROJECT_NAME)-$(VERSION).tar.gz

# -------------------------------
# Compiler and flags
# -------------------------------
CC := cc
CXX := g++
AR := ar
AS := as
CO := co
CPP := $(CC) -E
LEX := lex
YACC := yacc
MAKEINFO := makeinfo
      
CFLAGS := -Wall -Wextra -Og
LIBS := 
ARFLAGS := rv
ASFLAGS := 
CXXFLAGS := 
COFLAGS := 
CPPFLAGS := 
LDFLAGS := 
LFLAGS := 
YFLAGS :=

# -------------------------------
# Non-independent build loading submodule Makefile (i.e., treat submodules as static libraries and source code); independent builds for each module do not require this.
# -------------------------------
#-include $(foreach submodule, $(SUBMODULES), $(submodule)/Makefile)

# -------------------------------
# Top-level Files
# -------------------------------
SRCS := $(shell find $(SRC_DIR) -name '*.c')
OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS))
DEPS := $(OBJS:.o=.d)

# -------------------------------
# Targets
# -------------------------------
.PHONY: all clean install print tar dist TAGS test $(SUBMODULES) # Should also add a target to check if related tools are installed.

all: $(PROJECT_NAME)

# Build the main target executable
$(PROJECT_NAME): $(OBJS) $(SUBMODULES)
> $(CC) $(OBJS) -o $(BIN_DIR)/$@ $(LDFLAGS) $(LIBS)

# Build submodules: Iterate through submodules, enter them to execute make; if the makefile is not found, print skip.
$(SUBMODULES):
> @$(foreach submodule, $(SUBMODULES), \
    $(MAKE) -C $(submodule) || echo "Skipping $(submodule)";)

# Automatically generate dependency files
-include $(DEPS)

# Clean target: remove build artifacts
clean:
> rm -rf $(BUILD_DIR) $(BIN_DIR) $(LIB_DIR) $(TAR_FILE)
> $(foreach submodule, $(SUBMODULES), $(MAKE) -C $(submodule) clean || echo "Skipping $(submodule)";)

# Install target: install the program
install: all
> install -m 755 $(BIN_DIR)/$(PROJECT_NAME) $(INSTALL_DIR)

# Print target: list modified source files
print:
> @echo "Modified source files since last build:"
> @find $(SRC_DIR) -type f -newer $(BIN_DIR)/$(PROJECT_NAME) -print || echo "No modified source files."

# Tarball target: create a source tarball
tar:
> tar -cvzf $(TAR_FILE) $(SRC_DIR) $(INC_DIR) $(LIB_DIR) Makefile $(SUBMODULES)

# Dist target: compress the tarball
dist: tar
> gzip $(TAR_FILE)

# TAGS target: generate ctags or etags
TAGS:
> ctags -R $(SRC_DIR) $(INC_DIR) $(SUBMODULES)

# Test target: run unit tests (assuming a separate test suite)
test:
> @echo "Running tests..."
> $(MAKE) -C $(TEST_DIR)

# -------------------------------
# Compilation rules
# -------------------------------
# Rule to create object files, here it is clever that .c and .h updates will update .o; .o updates reference this rule, and .d will also update.
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
> @mkdir -p $(@D)
> $(CC) $(CFLAGS) -MD -MP -c $< -o $@

# Include all dependency files
-include $(DEPS)

Independent build submodule makefile#

Non-independent build submodule makefile#

.RECIPEPREFIX = >

# Submodule configuration
MODULE_NAME := submodule1
SRC_DIR := src
INC_DIR := inc
BUILD_DIR := build
LIB_DIR := ../lib

CC := cc
CFLAGS := -Wall -Wextra -Og -I$(INC_DIR)
AR := ar
ARFLAGS := rv

SRCS := $(shell find $(SRC_DIR) -name '*.c')
OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS))
LIB := $(LIB_DIR)/lib$(MODULE_NAME).a
DEPS := $(OBJS:.o=.d)

.PHONY: all clean

# Default target
all: $(LIB)

# Generate static library
$(LIB): $(OBJS)
> @mkdir -p $(LIB_DIR)
> $(AR) $(ARFLAGS) $@ $^

# Compilation rules
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
> @mkdir -p $(@D)
> $(CC) $(CFLAGS) -MMD -MP -c $< -o $@

# Clean rules
clean:
> rm -rf $(BUILD_DIR) $(LIB)

# Automatically include dependency files
-include $(DEPS)
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.