makefile 入门使用

通常一个大型程序由多个模块文件构成的,按照其功能划分,模块文件会分布在不同的目录中,模块文件之间有包含有头文件,调用函数的情况,他们之间存在依赖关系。

大多数情况下,我们只是修改了其中某些文件,而不是所有文件,按理说,我们只需要把那些修改过的文件,并且依赖于这些文件的相关文件重新编译即可,用不着重新编译全部文件。

所以问题来了,有没有办法自动对那些有改过的文件重新编译呢?这实际上要分为 2 个问题:

  • 目标文件依赖哪些文件?
  • 依赖的文件是否更新?

为了解决这2个问题,业界就有了 make 和 makefile。总的来说,它俩并不是用来编译程序,它们只负责找出哪些文件变化,并且根据依赖关系找出受影响的文件,然后执行事先在 makefile 中定义好的命令规则。只是该规则大多数情况是调用 gcc 或 nasm 进行编译。因此这也就完成了我们上面提到的 2 个问题。

makefile 基本语法

目标文件:依赖文件
[tab] 命令

eg:
a.o: a.c
  gcc -c a.c -o a.o
a.out: a.o
  gcc a.o -o a.out
  • 目标文件是指此规则中想要生成的文件,可以是 .o 结尾的目标文件,也可以是可执行文件,也可以是伪目标。
  • 依赖文件是指要生成此规则中的目标文件。通常不止一个,是一个依赖文件的列表。
  • 命令是指此规则中要执行的动作,各种 shell命令,一个命令单独占用一行,行首必须以 Tab 开头。

根据以上规则,我们已经知道了目标文件依赖哪些文件,但还需要解决第2个问题。怎么判断依赖的文件是否更新?

在 Linux 中,每个文件有3种时间:

  • atime
    access time 访问文件数据部分时间
  • ctime
    change time 文件属性部分或数据部分的改变时间
  • mtime
    modify time 文件数据部分的修改时间
1:2
    @echo "makefile test ok"

这个makefile文件的意义就是 " 如果文件2的 mtime 比文件1的 mtime 要新,则打印 "makefile test ok" "。

综上,依赖关系定义在文件 makefile 中,make 程序通过解析 makefile 文件,根据mtime标签自动找出 变更的文件 以及 依赖此变更文件的目标文件,然后对所有受影响的相关文件执行事先定义好的命令规则。

makefile 的文件名并不固定,可以用 -f 参数指定。默认是先寻找名为 GNUmakefile 的文件,若该文件不存在再去找名为 makefile 的文件,若 makefile 也不存在,最后去找名为 Makefile 的文件。

t1:1
  @echo "target1"
t2:1
  @echo "target2"

若采用make 目标名称,比如make t2,则会单独执行目标名称处的规则。
若采用make ,则会单独执行在 makefile 中第一个出现的目标。

变量

= := ?= 区别
目标变量

伪目标

有时我们有"并不关心是否产生真实的目标文件,我们只希望通过 make 不要考虑 mtime,而是总能去执行一些命令",比如

clean:
    rm *.o     # 清理文件

这时,make规定,当规则中不存在依赖文件时,这个目标文件名就称为 -- 伪目标。

  • .PHONY

  • 约定俗成的伪目标名称

伪目标名称 功能描述
all 通常用于完成所有模块的编译工作
clean 通常用于清空编译完成的所有目标文件
dist 将打包文件再压缩成gz 文件
Install 通常将编译好的程序复制到安装目录下
printf 通常用于打印已经发生改变的文件
tar 通常用于将文件打包成tar文件
test 测试makefile 流程

make: 递归式推导目标

test1.o : test1.c
  gcc -c test1.c -o test1.o

test2.o : test2.c
  gcc -c test2.c -o test2.o

test.bin : test1.o test2.o
  gcc test1.o test2.o -o test.bin

all : test.bin
  @echo "compile done"

当执行make all时,make 首先发现 all 依赖于 test.bin,寻找 test.bin 不存在,于是便去寻找 test.bin的依赖文件 test1.o test2.o,同样不存在,于是寻找能生成 test1.o 和 test2.o 规则。找到后执行相应规则,有了test1.o 和 test2.o 再返回生成 test.bin,最后在执行生成 all 的规则。

自定义变量与系统变量

makefile中定义变量基本格式:

变量名=值    # 多个值之间用空格分开,值仅仅支持字符串类型。即使数字也会当作字符串处理
eg:

objfiles = test1.o test2.o
test.bin: $(objfiles)

除了自定义的变量,make也定义了很多系统变量

变量名 描述
AS 汇编语言编译器,默认是 as
CC C语言编译器,默认是 gcc
CXX C++语言编译器,默认 g++
RM 删除命令,默认是 rm -f
ASFLAGS 汇编语言编译器参数,无默认值
CFLAGS C语言编译器参数,无默认值
CXXFLAGS C++编译器参数,无默认值
LDFLAGS 链接器参数,无默认值

隐含规则

对于一些使用频率非常高的规则,make 把它们当成是默认的,不需要显式地写出来,当用户未在 makefile 中显示定义规则时,将默认使用隐含规则进行推导。

隐含规则只限于那些编译过程中基本固定的依赖关系,比如C语言代码文件扩展名为.c,编译生成的目标文件扩展名是.o。并且若想通过隐含规则自动推导生成目标,存在于文件系统上的文件,除扩展名之外的文件名部分必须相同。

自动化变量

为了方便,make 还支持一种自动化变量:

自动化变量 描述
$@ 表示规则中目标文件名集合
$< 表示规则中依赖文件中的第1个文件
$^ 表示规则中所有依赖文件的集合
$? 表示规则中所有比目标文件mtime更新的依赖文件的集合
test1.o : test1.c
  gcc -c test1.c -o test1.o

test2.o : test2.c
  gcc -c test2.c -o test2.o

objfiles = test1.o test2.o
test.bin : $(objfiles)
  gcc $^ -o $@

all : test.bin
  @echo "compile done"

再方便的就是利用正则表达式匹配,%.o 表示多有以 .o 结尾的文件,make 会拿这个字符串模式去文件系统上查找文件,默认当前路径。

%.o : %.c  # test1.c test2.c 都在当前目录。
  gcc -c $^ -o $@

objfiles = test1.o test2.o
test.bin : $(objfiles)
  gcc $^ -o $@

all : test.bin
  @echo "compile done"

makefile 中执行shell命令

SVN_VERSION:=$(shell svnversion)
COMP_DATE:=$(shell date)

vim 编辑makefile

vim设置 tab 键为 4 个空格,编辑 makefile 时 可以先敲 ctrl-v 组合键,再敲 tab键,这样就不会被转换成空格了。

makefile中常用函数

  • wildcard
    扩展通配符

  • notdir
    去除目录信息,只保留文件名。

  • patsubst
    替换通配符

    # tree
    .
    ├── a
    │   ├── a2.cpp
    │   └── a.cpp
    ├── b
    │   ├── b2.cpp
    │   └── b.cpp
    └── Makefile
    
    # cat Makefile
    src=$(wildcard ./a/*.cpp ./b/*.cpp)
    dir=$(notdir $(src))
    obj=$(patsubst %.cpp,%.o,$(dir))
    
    all:
      @echo $(src)
      @echo $(dir)
      @echo $(obj)
    

    输出:

    ./a/a2.cpp ./a/a.cpp ./b/b2.cpp ./b/b.cpp
    a2.cpp a.cpp b2.cpp b.cpp
    a2.o a.o b2.o b.o
    
  • foreach

    $(foreach ,,)
    

    这个函数的意思是,把参数中的单词逐一取出放到参数所指定的变量中,然后再执行所包含的表达式。
    每一次会返回一个字符串,循环过程中,的所返回的每个字符串会以空格分隔,最后当整个循环结束时,所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。

    # cat Makefile
    SRC_PATH=./a/ ./b/
    SRC_FILE=$(foreach SUB_DIR,$(SRC_PATH),$(wildcard $(SUB_DIR)*.cpp))
    ALL_FILE=$(notdir $(SRC_FILE))
    OBJ_FILE=$(patsubst %.cpp,%.o,$(ALL_FILE))
    
    info:
      @echo $(SRC_FILE)
      @echo $(ALL_FILE)
      @echo $(OBJ_FILE
    

    输出

    ./a/a2.cpp ./a/a.cpp ./b/b2.cpp ./b/b.cpp
    a2.cpp a.cpp b2.cpp b.cpp
    a2.o a.o b2.o b.o
    

一个简单的Makefile模板

目录规划
# tree
.
├── log
│   ├── AsyncLog.cpp
│   └── AsyncLog.h
├── main.cpp
├── Makefile
├── net
│   ├── Acceptor.cpp
│   ├── Acceptor.h
│   ├── Buffer.cpp
│   ├── Buffer.h
...
Makefile
CXX=g++

COMP_DATA:=$(shell date)

OUT_PATH=./bin/
OBJ_PATH=./obj/
DEBUG_PATH=debug/
RELEASE_PATH=release/
OUT_NAME=main

SRC_PATH=./ ./net/ ./log/
VPATH = ./ : ./net/ : ./log/
SRC_FILE=$(foreach SUB_DIR,$(SRC_PATH),$(wildcard $(SUB_DIR)*.cpp))
ALL_FILE=$(notdir $(SRC_FILE))
OBJ_FILE=$(patsubst %.cpp,%.o,$(ALL_FILE))

OBJ_DEBUG_FILE=$(addprefix $(OBJ_PATH)$(DEBUG_PATH),$(notdir $(OBJ_FILE)))
OUT_DEBUG_FILE=$(OUT_PATH)$(DEBUG_PATH)$(OUT_NAME)

DEBUG_CXXFLAGS=-g -Wall -Wno-conversion-null -Wno-format-security -Werror -DCOMP_DATE='"$(COMP_DATA)"'

debug: pre_debug $(OUT_DEBUG_FILE)

pre_debug:
    -$(shell mkdir $(OBJ_PATH) -p)
    -$(shell mkdir $(OBJ_PATH)$(DEBUG_PATH) -p)
    -$(shell mkdir $(OUT_PATH) -p)
    -$(shell mkdir $(OUT_PATH)$(DEBUG_PATH) -p)

$(OUT_DEBUG_FILE) : $(OBJ_DEBUG_FILE)
    $(CXX) $(DEBUG_CXXFLAGS) $(addprefix $(OBJ_PATH)$(DEBUG_PATH), $(notdir $^)) -o $@ -lpthread

$(OBJ_PATH)$(DEBUG_PATH)%.o : %.cpp
    $(CXX) -c $(DEBUG_CXXFLAGS)  $< -o $@

clean:
    @echo "make clean"
    -$(shell rm $(OBJ_PATH) -rf )
    -$(shell rm $(OUT_PATH)$(DEBUG_PATH)$(OUT_NAME) -f)
    -$(shell rm $(OUT_PATH)$(RELEASE_PATH)$(OUT_NAME) -f)

info:
    $(info SRC_FILE:  $(SRC_FILE))
    $(info ALL_FILE:  $(ALL_FILE))
    $(info OBJ_FILE:  $(OBJ_FILE))
    $(info OBJ_DEBUG_FILE:  $(OBJ_DEBUG_FILE))
    $(info OUT_DEBUG_FILE:  $(OUT_DEBUG_FILE))

你可能感兴趣的:(makefile 入门使用)