@Title: Managing Projects with GNU Make
@Edition: 3.Xth Edition
[TOC]
1. 前言
2. 简单例子
使用GNU Make可以直接输入make,也可以在后面加入变量,以及使用-f指定Makefile的位置。
make
make -f /path/to/Makefile
make -f /path/to/AliasMakefile # 不一定非要叫做Makefile,也可以是其它名字,不过最好一致
make CFLAGS=-O2 # 这会将Makefile中的变量$(CFLAGS)替换为`-O2`
Makefile中目标的格式:
target: prerequisites
commands
3. 规则
3.1 伪目标(PHONY Target)
3.2 变量
最简单的格式$(variable-name),$用于区分标识是变量,除了单字符变量名无需(),其它的则需要使用其包围住。
$${USER}是SHELL的变量,而${USER}才是Make的变量。
3.3 自动变量
| 自动变量 | 释义 |
|---|---|
$@ | 当前目标的名字 |
$% | 仅当目标是函数库文件时,表示规则中的目标成员名。 |
$< | 依赖中第一个依赖的名字 |
$? | 比目标要新的所有依赖名,用空格隔开。即上次执行后发生改变的依赖 |
$^ | 空格隔开的所有依赖。如果依赖中有多个是重复的,会自动去除重复的 |
$+ | 类似$^,但是它不会去除重复的依赖 |
$* | 目标的stem,所谓stem即是不带后缀名的文件名,即%.c中的%。不鼓励在模式规则之外使用 |
为了保持和其它make的兼容性,上述自动变量有两个变体。第一个变体是返回目录部分,通过后面加D,如$(@D)、$(<D)等;第二个变体是返回值部分,通过后面加F,如$(@F)、$(<F)等。请注意,务必带上小括号使用。当然GNU Make提供了两个函数:dir以及not dir。
3.4 使用VPATH和vpath查找文件
默认情况下,Make只会在当前目录查找目标文件和依赖文件。但是可以通过VPATH和vpath指定查找的目录。
VPATH = directory-list,目录使用空格隔开,*nix可使用:,Windows可使用;,但是为了统一,还是使用空格。
vpath pattern directory-list,这是为了避免同名文件在不同位置,而VPATH不会区分只获取第一个的问题。
vpath %.c src
vpath %.l src
vpath %.h include
3.5 模式规则
make --print-data-base:Make默认规则(以及变量)集。
%在这里就像正则表达式中的*,代表的是任意字符,也可以出现在任意的地方,例如:%, v、s%.o、wrapper_%。
3.5.1 静态模式规则
只是用于指定的目标列表的。
$(OBJECTS) : %.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
3.5.2 后缀规则
最初的写Makefile的方法,并且已过时,因为和其它Make工具可能不兼容,但是仍可能会见到。
.c.o:
$(COMPILE.c) $(OUTPUT_OPTION) $<
# 上述规则等同于:
%.o : %.c
$(COMPILE.C) $(OUTPUT_OPTION) $<
而没有后缀的文件的规则:
.p:
$(LINK.p) $^ $(LOADLIBES) $(LDLIBS) -o $@
# 等同于
%: %.p
$(LINK.p) $^ $(LOADLIBES) $(LDLIBS) -o $@
默认可以使用后缀规则的是:
.SUFFIEXES: .out .a .ln .o .c .cc .C .cpp .p .f .F .r .y .l
自己添加后缀:
.SUFFIXES: .pdf .fo .html .xml
删除所有已知后缀:
.SUFFIXES: # 什么都不写就是删除所有已知后缀
或者使用--no-builtin-rules或-r命令行选项。
3.6 隐式规则数据库
可以用--print-data-base选项查看内置规则(简写用-p)。可以用--no-builtin-rules(-r)和--no-builtin-variables(-R)选项禁用内置规则。
使用就很简单,就是只写目标和依赖,而不写命令。
--just-print(-r)选项用于打印要执行的命令,但是不会真的执行。如make -n foo。
3.7 特殊的目标
最常用的几个:
.INTERMEDIATE # 它的依赖会被当成中间文件,就像化学中的“中间产物”,会随着make结束而被自动删除
.SECONDARY # 依赖也会被当成中间文件,但是不会被自动删除
.PRECIOUS # make执行中中断,就会删除正在更新的文件。使用了该目标就不会删除了
.DELETE_ON_ERROR # 和.PRECIOUS正好相反
3.8 自动生成依赖
gcc中读取源码生成依赖的方法:-M选项
$ echo "#include <stdio.h>" > stdio.c
$ gcc -M stdio.c
stdio.o: stdio.c /usr/include/stdc-predef.h /usr/include/stdio.h \
/usr/include/x86_64-linux-gnu/bits/libc-header-start.h \
/usr/include/features.h /usr/include/x86_64-linux-gnu/sys/cdefs.h \
/usr/include/x86_64-linux-gnu/bits/wordsize.h \
/usr/include/x86_64-linux-gnu/bits/long-double.h \
/usr/include/x86_64-linux-gnu/gnu/stubs.h \
/usr/include/x86_64-linux-gnu/gnu/stubs-64.h \
/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h \
/usr/include/x86_64-linux-gnu/bits/types.h \
/usr/include/x86_64-linux-gnu/bits/typesizes.h \
/usr/include/x86_64-linux-gnu/bits/types/__FILE.h \
/usr/include/x86_64-linux-gnu/bits/types/FILE.h \
/usr/include/x86_64-linux-gnu/bits/libio.h \
/usr/include/x86_64-linux-gnu/bits/_G_config.h \
/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h \
/usr/lib/gcc/x86_64-linux-gnu/7/include/stdarg.h \
/usr/include/x86_64-linux-gnu/bits/stdio_lim.h \
/usr/include/x86_64-linux-gnu/bits/sys_errlist.h
这些依赖怎么导入Makefile呢?两种方法:
-
Makefile文件末尾写上
# Automatically generated dependencies follow - Do Not Edit,然后再写一个shell脚本更新生成依赖; -
使用
include指令,在执行构建之前,先执行生成依赖。depend: count_words.c lexer.c counter.c $(CC) -M $(CPPFLAGS) $^ > $@ include depend
但是人们常常忘了更新依赖,有两种方法解决。
-
一个算法(ugly)
counter.o counter.d: src/counter.c include/counter.h include/lexer.h %.d: %.c $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$ -
一个功能
VPATH = src include CPPFLAGS = -I include SOURCES = count_words.c \ lexer.c \ counter.c count_words: counter.o lexer.o -lfl count_words.o: counter.h counter.o: counter.h lexer.h lexer.o: lexer.h include $(subst .c,.d,$(SOURCES)) %.d: %.c $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$
3.9 管理库
创建一个库
$ ar rv libcounter.a counter.o lexer.o
a - counter.o
a - lexer.o
rv选项指示使用列出的目标文件替换libcounter.a库中的成员,并且需要verbosely打印其操作。
3.9.1 创建和更新
libcounter.a: counter.o lexer.o
$(AR) $(ARFLAGS) $@ $^
# 更好的选择,使用$?
libcounter.a: counter.o lexer.o
$(AR) $(ARFLAGS) $@ $?
3.9.2 把库作为依赖
xpong: $(OBJECTS) /lib/X11/libX11.a /lib/X11/libXaw.a
$(LINK) $^ -O $@
# 简单起见
xpong: $(OBJECTS) -lX11 -lXaw
$(LINK) $^ -O $@
4. 变量和宏
Make的变量名中不允许的字符只有:,#以及=,但是不建议使用乱七八糟的符号。同时变量也是大小写敏感的,提取变量值使用$(VAR_Name),而单字符变量名可不用小括号,如自动变量,但最好不用单字符变量名,花括号也是可以的。
4.1 变量类型
简单扩展变量使用:=来赋值,而递归扩展变量使用=来赋值。简单扩展的问题是,如果符号右边有变量,那必须之前就存在,否则就是空;而递归扩展则会解决这个问题:
#### simple extended variable
MAKE_DEPEND := $(CC) -M
# 如果在之前CC未定义,则MAKE_DEPENDE就是<space>-M
#### recursively expanded variable
MAKE_DEPEND = $(CC) -M
...
# Some time later
CC = gcc
# 这样,MAKE_DEPEND是gcc -M
其实递归扩展变量不仅仅是延迟赋值,而是每次使用该变量都会重新计算其右侧的值。这有时候很有用,但是如果每次使用值都会变化也会带来困扰,如时间date。
还有其它的赋值类型:?=以及+=。
| 赋值符号 | 释义 | 举例 |
|---|---|---|
:= | 简单扩展,就是按当前,直接就是右侧的值 | MAKE_DEPEND := $(CC) -M |
= | 递归扩展,每次使用都重新计算右侧的值 | MAKE_DEPEND = $(CC) -M |
?= | 条件变量,只有该变量没值的时候才计算右侧并赋值 | OUTPUT_DIR ?= $(PROJECT_DIR)/out |
+= | 添加变量,就是添加 | simple := $(simple) new stuff |
请注意,如果使用=那么不能类似+=那样使用,否则Make无法处理。
4.2 宏
# 定义
define create-jar
@echo Creating $@ ...
$(RM) $(TMP_JAR_DIR)
$(MKDIR) $(TMP_JAR_DIR)
$(CP) -R $^ $(TMP_JAR_DIR)
cd $(TMP_JAR_DIR) && $(JAR) $(JARFLAGS) $@ .
$(JAR) -ufm $@ $(MANIFEST)
$(RM) $(TMP_JAR_DIR)
endef
# 使用
$(UI_JAR) : $(UI_CLASSES)
$(create-jar)
默认使用命令会先打印再执行,而使用@标识一行会不打印,直接执行。
宏在Make中是当作字符串解析的,所以如注释是行不通的。
4.3 变量扩展时机
Make执行有两个阶段。第一阶段,Make读取Makefile以及其包含的Makefile。此时,变量和规则加载到了Make内部数据库并创建了依赖图。第二阶段,Make执行依赖图,并确定需要更新的目标,然后执行命令脚本以更新目标。
- 对于变量赋值,左侧总是在Make第一阶段读取行的时候立刻扩展;
=以及?=右侧在第二阶段使用的时候推断;:=右侧会立即扩展;+=右侧是简单变量时会立即扩展,否则推迟定值;- 对于
define定义的宏,宏名立即扩展,宏体直到使用时才定值; - 对于规则、目标和依赖总是立即扩展,而命令总是推迟定值。
举例:
| 定义 | a扩展 | b扩展 |
|---|---|---|
a = b | 立即 | 推迟 |
a ?= b | 立即 | 推迟 |
a := b | 立即 | 立即 |
a += b | 立即 | 推迟或立即 |
define ab...endef | 立即 | 推迟 |
请记住:总是应该在使用一个变量之前定义它。在执行make时,也是可以变更变量的。例如:
$ make test_assert CFLAGS+="-D NDEBUG"
gcc -D NDEBUG test_assert.c -o test_assert
4.4 指定目标变量
指定目标变量是在目标内使用的,当目标开始才会进行赋值,当目标命令执行结束消失。例如:
gui.o: CPPFLAGS += -DUSE_NEW_MALLOC=1
gui.o: gui.h
$(COMPILE.c) $(OUTPUT_OPTION) $<
这样当gui.o生成之后,CPPFLAGS又会恢复原来的值,即没有-DUSE_NEW_MALLOC。
4.5 变量哪里来
- 文件:本Makefile文件中定义或引入的其它Makefile文件
- 命令行:如果多于一个,需要
''包裹,或者转义空格。文件中的变量也可以覆盖,使用override指令。
make CFLAGS=-g CPPFLAGS='-DBSD -DDEBUG'
override LDFLAGS = -EB
- 环境:所有环境变量会在Make开始时,自动定义为Make的变量,但至少比上两者优先度低。可以使用命令行选项
--environment-overrides或-e来显示指定用环境变量覆盖Makefile的变量。从父Makefile传递到子Makefile时只有原本是环境变量的才是环境变量。可以使用export指令把任意变量指定为环境变量。单独使用export一行,会把所有的变量指定为环境变量。
5. builtins 函数
5.1 文本处理
- subst
- patsubst
- strip
- findstring
- filter
- filter-out
- sort
- word
- wordlist
- firstword
5.2 文件名处理
- dir
- notdir
- suffix
- basename
- addsuffix
- addprefix
- join
- wildcard
5.3 控制函数
- error
- warning
5.4 其它函数
- if
- foreach
- call
- value
- eval
- origin
- shell
Make Demo
CFLAGS = -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG $(OPTFLAGS) # usual CFLAGS
LIB = -ldl $(OPTLIBS) # used when linking a library
PREFIX ?= /usr/local # optional variable, only works when PREFIX not set before
SOURCES = $(wildcard src/**/*.c src/*.c) # find all *.c files
OBJECTS = $(patsubst %.c,%.o, $(SOURCES)) # replace *.c with *.o
TEST_SRC = $(wildcard tests/*_tests.c) # again
TESTS = $(patsubst %.c, %.o, $(TEST_SRC)) # again
TARGET = build/libYOUR_LIBRARY.a
SO_TARGET = $(patsubst %.a, %.so, $(TARGET))
all: $(TARGET) $(SO_TARGET) tests # the first target will run by default
dev: CFLAGS=-g -Wall -Isrc -Wextra $(OPTFLAGS)
dev: all
$(TARGET): CFLAGS += -fPIC
$(TARGET): build $(OBJECTS)
ar rcs $@ $(OBJECTS)
ranlib $@
$(SO_TARGET): $(TARGET) $(OBJECTS)
$(CC) -shared -o $@ $(OBJECTS)
build:
@mkdir -p build
@mkdir -p bin
.PHONY: tests
tests: CFLAGS += $(TARGET)
tests: $(TESTS)
sh ./tests/runtests.sh
valgrind:
VALGRIND="valgrind --log-file=/tmp/valgrind-%p.log" $(MAKE)
clean:
rm -rf build $(OBJECTS) $(TESTS)
rm -f tests/tests.log
find . -name "*.gc" -exec rm {} \;
rm -rf `find . -name "*.dSYM" -print` # *.dSYM directories Apple's XCode leaves behind for debugging
install: all
install -d $(DESTDIR)/$(PREFIX)/lib/
install $(TARGET) $(DESTDIR)/$(PREFIX)/lib/
BADFUNCS='[^_.>a-zA-Z0-9](str(n?cpy|n?cat|xfrm|n?dup|str|pbrk|tok|_)|stpn?cpy|a?sn?printf|byte_)'
check:
@echo Files with potentially dangerous functions.
@egrep $(BADFUNCS) $(SOURCES) || true