Make以及Makefile

288 阅读4分钟

@Title: Managing Projects with GNU Make

@Edition: 3.Xth Edition

[TOC]

1. 前言

Official GNU Make manual

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 使用VPATHvpath查找文件

默认情况下,Make只会在当前目录查找目标文件和依赖文件。但是可以通过VPATHvpath指定查找的目录。

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默认规则(以及变量)集。

%在这里就像正则表达式中的*,代表的是任意字符,也可以出现在任意的地方,例如:%, vs%.owrapper_%

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呢?两种方法:

  1. Makefile文件末尾写上# Automatically generated dependencies follow - Do Not Edit,然后再写一个shell脚本更新生成依赖;

  2. 使用include指令,在执行构建之前,先执行生成依赖。

    depend: count_words.c lexer.c counter.c
        $(CC) -M $(CPPFLAGS) $^ > $@
    include depend
    

但是人们常常忘了更新依赖,有两种方法解决。

  1. 一个算法(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 $@.$$$$
    
  2. 一个功能

    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 a
b...
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 变量哪里来

  1. 文件:本Makefile文件中定义或引入的其它Makefile文件
  2. 命令行:如果多于一个,需要''包裹,或者转义空格。文件中的变量也可以覆盖,使用override指令。
make CFLAGS=-g CPPFLAGS='-DBSD -DDEBUG'
override LDFLAGS = -EB
  1. 环境:所有环境变量会在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