系统构建_Makefile

image

from pixiv

前言

教程来自:Github-Makefile_tutor

V1:Simplest C project

Makefile中的内置变量

Makefile 中的内置变量(builtin variables)是 Make 提供的一些默认变量,这些变量用于简化常见的构建任务,如编译、链接和其他操作。

它们可以被用户在 Makefile 中直接使用,也可以在需要时覆盖它们的默认值。

您可以在数据库中找到的许多其他变量:

make --print-data-base | less

与模式规则相关的变量

%:用于模式规则中的通配符,表示任意匹配的字符。

例如,%.o: %.c 规则表示任何 .o 文件都可以通过与之同名的 .c 文件生成。

%.o: %.c
    $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

模式规则是目标包含 % 字符的规则(此处为 %.o: %.c)。

这个字符的意思是“正是其中之一”。这里用它来表示每个 .o 需要一个具有相同名称的 .c,并且 $(CC)... 将执行与 .o: .c 对一样多的次数,从而为每个源创建其相应的对象,一次一个。

输入输出文件相关的变量

  • $@:目标文件的名称,即当前规则生成的文件或目标。

  • $<:第一个依赖文件的名称,通常是源文件。

  • $^:所有依赖文件的名称列表,空格分隔。

  • $?:所有比目标文件更新的依赖文件的列表。

  • $(@D) 目标文件名的目录部分

  • $(@F)目标文件名的文件部分

自动递归调用相关的变量

MAKE 和 MAKEFLAGS 是内置变量。

MAKE 值对应于正在运行的 make 可执行文件,MAKEFLAGS 对应于其标志。

当从另一个 Makefile 执行一个 Makefile 时,被调用者的 MAKE MAKEFLAGS 变量将从调用者的 MAKE MAKEFLAGS 值继承。

  • 为什么有用?

    • 递归调用:如果一个 Makefile 调用另一个 Makefile,你可以使用 $(MAKE) 来确保调用的也是同一个 make 程序,而不是某个其他版本的 make。

    • 系统兼容性:不同系统上可能有不同版本或位置的 make 程序。使用 $(MAKE) 可以确保你正在运行的 Makefile 中使用的是启动这个构建过程的 make,而不是其他版本。

Makefile中的隐式规则

顾名思义,隐式规则是隐式的,不需要编写。除了内置变量之外,所有隐式规则都可以在数据库中找到:

make -p -f/dev/null | less

C 编译隐式规则如下所示:

%.o: %.c
    $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

这说明将.c文件编译成.o文件这个步骤我们是不需要重复写了。

隐式规则中的文件查找

我一直有个疑惑,隐式规则中文件都是只写了个%.c之类的,那么这些文件到底是在哪里找到的?Makefile会自动搜索当前目录找吗?还是可以自动的递归搜索找?

都不是!以下以一个案例进行讲解:

若我在~/ics2023/abstract-machine目录下执行了@$(MAKE) -s -C $(AM_HOME)/klib archive

$(AM_HOME)/klib/Makefile的内容为:

NAME = klib
SRCS = $(shell find src/ -name "*.c")
//SRCS内容为src/cpp.c src/int64.c src/stdio.c src/stdlib.c src/string.c
include $(AM_HOME)/Makefile

其中$(AM_HOME)/Makefile参与的主要内容如下:

ARCH = riscv32-nemu
WORK_DIR  = $(shell pwd) //即~/ics2023/abstract-machin/klib
DST_DIR   = $(WORK_DIR)/build/$(ARCH) //即~/ics2023/abstract-machin/klib/build/riscv32-nemu

$(DST_DIR)/%.o: %.c
	@mkdir -p $(dir $@) && echo + CC $<
	@$(CC) -std=gnu11 $(CFLAGS) -c -o $@ $(realpath $<)

$(ARCHIVE): $(OBJS)
	@echo + AR "->" $(shell realpath $@ --relative-to .)
	@$(AR) rcs $(ARCHIVE) $(OBJS)

OBJS      = $(addprefix $(DST_DIR)/, $(addsuffix .o, $(basename $(SRCS))))


archive: $(ARCHIVE)

需要注意一下点:

  1. $(MAKE) -C /path表示到/path目录下执行Makefile,所以WORK_DIR = $(shell pwd)才是会为~/ics2023/abstract-machin/klib,而不是~/ics2023/abstract-machin/

  2. 依赖步骤:

    1. 首先执行make archive,发现其依赖$(ARCHIVE)
    2. 然后发现$(ARCHIVE)要依赖$(OBJS)$(OBJS)中的内容是:~/ics2023/abstract-machin/klib/build/riscv32-nemu/src/cpp.o~/ics2023/abstract-machin/klib/build/riscv32-nemu/src/int64.o~/ics2023/abstract-machin/klib/build/riscv32-nemu/src/stdio.o、...
    3. 接下来是我自己的一点思考,不一定正确:
      $(OBJS)在显示规则中未发现自己的依赖,所以开始到隐式规则中寻找依赖。
      $(OBJS)正好能和隐式规则中的$(DST_DIR)/%.o匹配上,如:
      $(OBJS)中的~/ics2023/abstract-machin/klib/build/riscv32-nemu/src/cpp.o ...
      $(DST_DIR)/%.o中的~/ics2023/abstract-machin/klib/build/riscv32-nemu/%.o
      %.o匹配上了src/cpp.o ...

    那么对应的%.c匹配上了src/cpp.c ...,那么这些c文件是在当前工作目录下的%.c查找,即在~/ics2023/abstract-machin/klib/src/cpp.c ...找。

.PHONY:

Makefile 中,.PHONY 是一个伪目标(phony target),用来指定某些目标不对应实际的文件,而是为了执行某些命令。它的作用是避免文件名与目标名冲突,确保 make 在执行这些目标时不会因为目标名称与文件名称相同而产生混淆。

  1. 避免文件与目标重名时的冲突

    • 如果 Makefile 中有一个目标名为 clean,但是当前目录中也存在一个叫 clean 的文件,make 在执行时可能会认为这个目标已经完成,因为这个文件存在,导致不再执行目标命令。
    • 通过将 clean 声明为 .PHONY 目标,make 就不会去检查是否存在与目标同名的文件,而是每次都执行该目标的命令。
  2. 提高构建效率

    • 对于伪目标,make 不需要检查目标文件的时间戳与依赖项的时间戳是否一致(这是 make 判定目标是否需要重建的方式)。make 直接执行伪目标中的命令。

假设有如下 Makefile

clean:
    rm -f *.o myprogram

如果在项目目录中存在一个名为 clean 的文件,make clean 就不会执行 rm 命令,因为 make 会认为 clean 目标已经完成(由于存在同名文件)。为了解决这个问题,我们可以使用 .PHONYmakeclean 视为伪目标:

.PHONY: clean

clean:
    rm -f *.o myprogram
  • make 不会将 .PHONY 目标和实际文件名相关联,也不会因为目录中存在同名文件而跳过该目标。
  • 即使有与目标同名的文件,make 也会执行 .PHONY 目标中的命令。
  • 它通常用于构建非文件相关的目标,比如 cleaninstallalltest 等。

:==的区别

Makefile 中,:== 都用于定义变量,但它们的工作方式不同,具体区别如下:

1. =(延迟展开 / 惰性赋值)

  • 延迟展开:使用 = 定义的变量在每次被引用时才进行 展开(计算/替换),也就是在使用变量时才会计算它的值,而不是在定义时计算。
  • 如果变量的值依赖于其他变量,那么 = 会等到变量实际被使用时,去读取其他变量的最新值。

示例:

A = $(B)
B = hello
  • 在这个例子中,A 的值会在 使用 A 时才确定,而在那时,B 的值已经是 hello,因此 A 的值也是 hello

延迟展开的效果

  • 如果 B 在定义后被修改,A 的值也会随着 B 的修改而改变。
A = $(B)
B = hello
B = world

在这种情况下,当 A 被引用时,A 的值会是 world

2. :=(立即展开 / 立即赋值)

  • 立即展开:使用 := 定义的变量会在 定义时 立即计算并确定值,之后无论再引用该变量多少次,值都保持不变。
  • 如果 := 定义的变量依赖于其他变量,那么在定义时就会立即计算其他变量的值,而不会在引用时重新计算。

示例:

A := $(B)
B = hello
  • 在这个例子中,A 在定义时就已经被赋值为 hello,因为 B 在定义 A 时的值是 hello。即使之后 B 的值改变,A 的值仍然保持为 hello

立即展开的效果

  • 即使 B 的值在定义后被修改,A 的值也不会受到影响。
A := $(B)
B = hello
B = world

在这种情况下,A 的值仍然是 hello,即使 B 被修改成了 world,因为 A 的值在定义时已经固定为 hello

@ 与 MAKEFLAGS

@写在构建规则之前作用是不打印规则本身

all: $(NAME)
     @echo "In Makefile1: MAKE=$(MAKE), MAKEFLAGS=$(MAKEFLAGS)"

如上述若没有@,那么echo "In Makefile1: MAKE=$(MAKE), MAKEFLAGS=$(MAKEFLAGS)"将会被输出

MAKEFLAGS是在执行make时自动加上的参数

MAKEFLAGS   += --no-print-directory

若去掉--no-print-directory这行,那么将会输出类似如下的信息:
image

默认目标(default goal)

默认情况下,当你执行 make 而不指定任何目标时,Makefile 会构建这个默认目标。

  1. 第一个出现的目标Makefile 中第一个定义的目标就是默认目标。如果没有显式地指定其他默认目标,它将作为默认目标。

    • 示例:
      all: target1 target2
      target1:
          echo "Building target1"
      target2:
          echo "Building target2"
      
      在这个例子中,all 是默认目标,因为它是 Makefile 中第一个出现的目标。如果你运行 make,它会默认执行 make all
  2. 可以自定义默认目标的名字:尽管 all 常常被用作默认目标,但你可以将默认目标设置为任何名字,只要它是第一个出现的目标。例如,你可以用 buildapp 代替 all

    • 示例:
      build: target1 target2
      target1:
          echo "Building target1"
      target2:
          echo "Building target2"
      
      在这个例子中,build 是默认目标,因为它是第一个出现的目标。

GNU make 提供了一个内置函数 MAKECMDGOALS 和特殊目标 .DEFAULT_GOAL,可以用来显式地指定默认目标。

  1. 使用 .DEFAULT_GOAL
    你可以通过设置 .DEFAULT_GOAL 来显式定义默认目标,即使它不是第一个目标。

    • 示例:
      .DEFAULT_GOAL := my_default_goal
      
      my_default_goal:
          echo "This is the default goal"
      
      other_target:
          echo "This is another target"
      
      现在,无论 my_default_goal 是否在 Makefile 中是第一个目标,它都会是默认目标。当你运行 make 时,它会执行 my_default_goal
  2. 查看当前默认目标
    如果你想知道当前默认目标是什么,可以运行以下命令:

    make -p | grep '.DEFAULT_GOAL'
    

    这将显示默认目标(如果它被显式设置)。如果没有显式设置,结果将为空,并且第一个目标会被当作默认目标。

V2:Project that include headers

静默

.PHONY: clean fclean re
.SILENT:

通常,make 在执行之前会打印规则配方的每一行。特殊目标 .SILENT:静默指定为先决条件的规则输出.

当在没有先决条件的情况下使用它时,它会静默所有规则(包括隐式规则)。

我们上述@是单行静默

还可以再MAKEFLAGS中+=--silent,这样的效果和.SILENT:后面不加任何先决条件,即静默所有规则的效果是一样的。

info打印自定义消息

%.o: %.c
    $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
    $(info CREATED $@)

C编译隐式规则被覆盖

这里使用 info 函数来打印有关刚刚构建的内容的自定义消息。

我们更喜欢 info 而不是 shell echo,因为它是一个 make 函数。与只能在配方中使用的 echo 不同,info 可以在 Makefile 中的任何位置使用,这使得它对于调试非常强大。

处理头文件

CPPFLAGS 专用于预处理器的标志,例如 -I <include_dir>,它允许您不再需要编写标头的完整路径,而只需在源中编写其文件名

如果你有一个头文件在路径 /path/to/include/icecream.h,通常在源代码中需要写成:

#include "../../path/to/include/icecream.h"

但是如果你在 Makefile 中的 CPPFLAGS 里加入了 -I/path/to/include,编译器就知道去 /path/to/include 路径下寻找头文件,你就可以在源代码中简单地写:

#include "icecream.h"

V3:Project with any kind of directory structure

反斜杠换行

SRCS        := \
    main.c          \
    arom/coco.c     \
    base/milk.c     \
    base/water.c

我们可以通过以反斜杠结尾来分割该行,以增加 SRCS 内容的可读性并方便其修改。

替换引用

SRC_DIR     := src
OBJ_DIR     := obj
SRCS        := \
    main.c          \
    arom/coco.c     \
    base/milk.c     \
    base/water.c
SRCS        := $(SRCS:%=$(SRC_DIR)/%)
OBJS        := $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)

字符串替换引用将变量的每一项的值替换为指定的更改。

$(SRCS:%=$(SRC_DIR)/%) 表示 % 表示的 SRCS 的每一项变成其自身 (%)加上 ``$(SRC_DIR)/ `更改,因此 main.c 变成 src/main.c。


非常需要注意的是:
OBJS := $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)

我们并不能将其写为:
OBJS := $(SRCS:%.c=$(OBJ_DIR)/%.o)

否则这相当于替换成了$(OBJ_DIR)/$(SRC_DIR)/%.o

举例来说,本来SRCS中的main.c,通过SRCS := $(SRCS:%=$(SRC_DIR)/%),得到src/main.c

若通过OBJS := $(SRCS:%.c=$(OBJ_DIR)/%.o),将会得到obj/src/main.o, 与预计的obj/main.o不符

V4: Static library

依赖文件(.d)

Makefile 中,依赖文件(Dependency Files)用于管理源代码文件之间的依赖关系,尤其是当源文件包含头文件(.h 文件)时。

依赖文件通常用于跟踪哪些源文件依赖哪些头文件,并在这些头文件发生变化时自动重新编译相关的目标文件(.o 文件)。

具体来说,.d 文件包含某个源文件所依赖的所有头文件列表。

例如,如果你有一个 main.c 文件,它包含了 main.h,那么 .d 文件中会列出 main.c 依赖于 main.h。如果 main.h 修改了,make 就会知道需要重新编译 main.c。

image


DEPS        := $(OBJS:.o=.d)
-include $(DEPS)

(1) DEPS := $(OBJS:.o=.d)

  • 这一行的作用是通过 模式替换OBJS 列表中的 .o 文件名替换为 .d 文件名,并将生成的 .d 文件列表存储在 DEPS 变量中。

  • OBJS 是目标文件(如 .o 文件),通常由 .c 源文件生成。

    假设 OBJS 的内容为:

    OBJS = main.o utils.o
    

    然后通过模式替换 $(OBJS:.o=.d),它会将 main.o 替换为 main.dutils.o 替换为 utils.d,因此:

    DEPS = main.d utils.d
    

    这意味着每个目标文件(.o 文件)都会有一个对应的依赖文件(.d 文件)来记录它的依赖关系。

(2) -include $(DEPS)

  • 这一行代码的作用是包含生成的 .d 依赖文件,使 make 知道每个目标文件依赖哪些头文件。
  • -include- 表示 “如果文件不存在,不要报错”。因为在第一次运行 make 时,.d 依赖文件可能还没有被生成,所以使用 -include 可以避免 make 在找不到 .d 文件时报错。

.d 文件被包含时,它会告诉 make 哪些头文件是每个目标文件的依赖项。这样,make 就能知道当某个 .h 文件发生变化时,哪些 .o 文件需要重新编译。

(3)依赖文件的生成

与源不同,当头文件被修改时,make 无法知道这一点,并且不会认为可执行文件已过期,因此不会重建它。

为了改变这种行为,我们应该添加适当的头文件作为附加先决条件:

#before                     #after
main.o: main.c              main.o: main.c icecream.h
    clang -c $< -o $@           clang -c $< -o $@

对多个源和标头手动执行此操作既乏味又容易出错。通过将 -MMD 添加到 CPPFLAGS,我们的编译器将自动为编译期间遇到的每个目标文件生成依赖项列表。 -MP 选项可防止头文件被删除或重命名时触发的错误。

makefile 中的 .d 文件通常是通过 GCC 的 -MMD 选项自动生成的。你可以在编译每个源文件时,让编译器同时生成对应的 .d 文件,用来记录依赖关系。

CC          := gcc
CFLAGS      := -Wall -Wextra -Werror
CPPFLAGS    := -MMD -MP -I include

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
        $(DIR_DUP)
        $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
        $(info CREATED $@)

生成静态库

关于静态库的前置知识

在C语言中,静态库是指一组预先编译好的目标文件(object files),这些文件可以被打包到一个单一的库文件中,然后链接到程序的可执行文件。

假设有多个源文件,你想将它们打包成一个静态库,常用工具是 ar(archive),这个工具可以将多个目标文件(.o)打包成一个库文件(.a)。

  1. 编译源文件:首先你需要将源文件编译成目标文件(.o)。这可以通过 gcc 完成:

    gcc -c file1.c file2.c file3.c
    

    这会生成 file1.ofile2.ofile3.o

  2. 打包静态库:使用 ar 工具将这些目标文件打包成一个静态库文件(通常以 .a 结尾)。

    ar rcs libmylibrary.a file1.o file2.o file3.o
    
    • r:添加文件到库中(或替换已存在的文件)。
    • c:创建库文件。
    • s:生成库文件的索引,以提高链接速度。

    这样就会生成一个名为 libmylibrary.a 的静态库文件。

  3. 使用静态库:在使用静态库时,你可以在编译你的程序时通过 -L-l 选项链接静态库:

    gcc -o myprogram main.c -L. -lmylibrary
    

    这里的 -L. 表示在当前目录下查找库文件,-lmylibrary 表示链接 mylibrary.a 静态库(注意,-l 后面的库名省略了后缀 .a)。

Make构建

NAME        := libicecream.a
AR          := ar
ARFLAGS     := -r -c -s

$(NAME): $(OBJS)
    $(AR) $(ARFLAGS) $(NAME) $(OBJS)
    $(info CREATED $(NAME))

使用 ar 创建静态库。

  • -r 用新对象替换旧对象,
  • -c 创建库(如果不存在)
  • -s 将索引写入存档或更新现有库。

V5:Project that uses libraries

打印调试Make规则

一般情况下我们是要给Makefile加上.SILENT:或者MAKEFLAGS+=--silent以达到静默规则的效果

但是有时候我们需要查看规则写的是否正确,或者查看规则过程:

info-%:
    $(MAKE) --dry-run --always-make $* | grep -v "info"
  1. info-%:
  • 模式规则info-%Makefile 中的一个模式规则,其中 % 是一个通配符,表示任意文本。例如,如果你运行 make info-myprogram% 就会匹配 myprogram,并将其作为 $* 传递给规则。
  • 目标:这个规则的目标是以 info- 开头,后面可以跟任意的目标名称。
  1. $(MAKE)
  • $(MAKE)make 的内置变量,它指向当前正在执行的 make 程序。这个变量确保无论你使用什么版本的 make,在递归调用时都使用相同的版本。
  1. --dry-run
  • --dry-run(或 -n)是 make 的一个选项,它会显示 make 将会执行的命令,而不实际执行这些命令。这意味着 make 会显示如何构建目标,但不会真正运行这些构建命令。
  1. --always-make
  • --always-make(或 -B)选项强制 make 总是执行规则,即使目标文件看起来是最新的也不会跳过它。这样,make 会展示如何重新构建目标,即使目标文件没有过期。
  1. $*
  • $* 是一个自动化变量,它代表模式规则中 % 匹配的部分。例如,如果你运行 make info-myprogram$* 就会是 myprogram。因此,$(MAKE) --dry-run --always-make $* 就会运行 make 并尝试构建 myprogram,但不实际执行,只是展示将会执行的命令。
  1. | grep -v "info"
  • grep -v "info" 通过 grep 过滤掉包含字符串 info 的行。因为规则的名字是 info- 开头的,make 在执行时可能会显示一些包含 info 的行(比如它要执行的命令),这一步骤就是用来忽略这些与目标不相关的输出。

调试方案

How to trace Makefile targets for troubleshooting?

或者还可以使用remake:博客

Makefile与git

.PHONY: update
update:
    git stash
    git pull
    git submodule update --init
    git stash pop
  1. git stash
  • 作用:将当前工作目录中未提交的更改(修改和未追踪的文件)暂存起来,以便能够在清洁的状态下执行后续的 git pull 等操作。暂存的更改会被保存到一个 Git 的 "stash" 栈中。
  • 目的:确保在执行 git pullgit submodule update 之前,当前工作目录的更改不会影响这些操作。
  1. git pull
  • 作用:从远程仓库中拉取最新的代码更改,并合并到当前的本地分支中。它会获取远程仓库的更新并将它们应用到你的本地代码上。
  • 目的:获取远程仓库的最新版本代码。
  1. git submodule update --init
  • 作用

    • git submodule update:更新项目中的 Git 子模块,使它们与 .gitmodules 文件中定义的版本同步。
    • --init:如果某些子模块还没有被初始化,这个选项会初始化它们,即下载并检出子模块的代码。
  • 目的:确保项目中的所有子模块被正确更新到指定的版本。

  • .gitmodules 是一个 Git 项目中的特殊配置文件,用于管理 Git 子模块。子模块是一种将其他 Git 仓库嵌入到主项目中的机制,允许你在一个项目中使用其他独立版本控制的仓库。

  1. git stash pop
  • 作用:恢复之前通过 git stash 暂存的更改。stash pop 会将 stash 栈顶的内容重新应用到当前工作目录,并从 stash 栈中删除这些暂存的更改。
  • 目的:在完成拉取代码和更新子模块后,将之前的未提交更改重新应用到工作目录。

addprefix

CPPFLAGS    := $(addprefix -I,$(INCS)) -MMD -MP
LDFLAGS     := $(addprefix -L,$(dir $(LIBS_TARGET)))
LDLIBS      := $(addprefix -l,$(LIBS))
  • CPPFLAGS:为头文件目录加上 -I 前缀,告知编译器头文件的搜索路径,并设置生成 .d 依赖文件的标志。
  • LDFLAGS:为库文件所在的目录加上 -L 前缀,告知链接器库文件的搜索路径。
  • LDLIBS:为每个库名加上 -l 前缀,指定需要链接的库名。

  1. CPPFLAGS
CPPFLAGS := $(addprefix -I,$(INCS)) -MMD -MP
  • CPPFLAGS预处理器标志,用于向编译器的预处理器传递参数。它通常用于指定 头文件的搜索路径 和一些 预处理选项
  • $(addprefix -I,$(INCS))
    • INCS 是包含头文件目录的变量列表,例如 INCS = include/ extra_includes/
    • addprefix -I, $(INCS) 会为 INCS 中的每个目录加上 -I 前缀(预处理器搜索路径标志)。最终效果类似于 -Iinclude/ -Iextra_includes/,表示让编译器在这两个目录下查找头文件。
  • -MMD:表示生成 依赖文件.d 文件),用来记录源文件和头文件之间的依赖关系。这用于自动化构建系统,使得 make 知道哪些文件需要重新编译。
  • -MP:用于生成 空的依赖目标。它与 -MMD 一起使用,确保当某个头文件被删除时,不会导致 make 出错。
  1. LDFLAGS
LDFLAGS := $(addprefix -L,$(dir $(LIBS_TARGET)))
  • LDFLAGS链接器标志,用于向链接器传递参数,通常用于指定 库文件的搜索路径
  • $(addprefix -L,$(dir $(LIBS_TARGET)))
    • LIBS_TARGET 是库文件的目标路径变量列表,假设它可能包含某些库文件路径,如 /usr/local/lib/libexample.a
    • $(dir $(LIBS_TARGET)) 提取 LIBS_TARGET 中每个库文件的 目录部分,例如 /usr/local/lib/
    • addprefix -L, $(dir $(LIBS_TARGET)) 会为每个提取出来的目录路径加上 -L 前缀,告诉链接器在这些目录中查找库文件。最终效果类似于 -L/usr/local/lib/
  1. LDLIBS
LDLIBS := $(addprefix -l,$(LIBS))
  • LDLIBS链接库标志,指定需要链接的 库名称。这些库通常在由 LDFLAGS 指定的路径中找到。
  • $(addprefix -l,$(LIBS))
    • LIBS 是库名列表,例如 LIBS = example math.
    • addprefix -l,$(LIBS) 会为 LIBS 中的每个库名加上 -l 前缀,生成 -lexample -lmath,表示链接时需要使用 example.amath.a.so 共享库。

dir

clean:
    for f in $(dir $(LIBS_TARGET)); do $(MAKE) -C $$f clean; done
    $(RM) $(OBJS) $(DEPS)

fclean: clean
    for f in $(dir $(LIBS_TARGET)); do $(MAKE) -C $$f fclean; done
    $(RM) $(NAME)

dir 函数意味着我们只想保留给定项目的目录部分,存在一个 notdir 函数,它执行相反的操作,仅保留给定项目的文件名。

V6:

Makefile内置变量

MAKECMDGOALS

MAKECMDGOALS 是 GNU make 中的一个内置变量,它存储了用户在命令行上指定的目标(goals)。

当你运行 make 时,make 会尝试构建你指定的目标,而 MAKECMDGOALS 就记录了这些目标的名字。

all: hello clean

hello:
    @echo "Hello World"

clean:
    @echo "Cleaning up..."

show-goals:
    @echo "Goals: $(MAKECMDGOALS)"

make show-goals: Goals: show-goals

Makefile函数

findstring

在 GNU Make 中,findstring 函数用于检查一个字符串是否包含另一个子字符串。

如果找到匹配项,findstring 返回这个子字符串;

如果没有找到匹配项,则返回空字符串。

$(findstring find, in)
  • find:你要查找的子字符串。
  • in:你要查找的字符串或文本。

wildcard

在 GNU Make 中,wildcard 函数用于匹配符合指定模式的文件名。

它通常用于查找目录中的文件,并生成文件列表。通过 wildcard 函数,你可以动态地获取目录下符合某种模式的文件,并将它们用于后续的构建规则中。

$(wildcard pattern)
  • pattern:通配符模式,通常是文件路径模式,比如 .c、src/.h。
  • 返回值:匹配到的文件名列表。如果没有匹配项,返回空字符串。

abspath

在 Makefile 中,abspath 是一个用于将相对路径转换为绝对路径的内置函数。它返回一个文件路径的绝对路径形式,而不必要求文件或目录实际存在。

$(abspath relative-path)

假设当前目录是 /home/user/project:

SRC_DIR := src
FULL_PATH := $(abspath $(SRC_DIR))

那么PULL_PATH就是/home/user/project/FULL_PATH

sort

sort 是 Makefile 的一个内置函数,它会对输入的单词列表进行排序,并去除重复的项。

排序结果按照字母顺序排列。

实战案例

处理普通C编译链接的Makefile

# 1. Basic Setup and Checks
# 2. Create Compilation Targets
WORK_LOAD = $(shell pwd)
DST_DIR = $(WORK_LOAD)/build
INCL = $(WORK_LOAD)/include
SRCS = $(shell find src/ -name "*.c")
OBJS = $(addprefix $(DST_DIR)/, $(addsuffix .o, $(basename $(SRCS))))
TARGET = $(WORK_LOAD)/rioTest

DEPS        := $(OBJS:.o=.d)
-include $(DEPS)

## 3. General Compilation Flags
CC 			:= gcc
CFLAGS      := -Wall -Wextra -Werror
CPPFLAGS    := -MMD -MP -I $(INCL)
LD 			:= $(CC)

## 4. Compilation Rules
$(DST_DIR)/%.o: %.c
	@mkdir -p $(dir $@) && echo + CC $<
	@$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

$(TARGET): $(OBJS)
	@echo + LD $^
	@$(LD) -o $@ $^

## 5. Miscellaneous
.PHONY: test clean

test : $(TARGET)

clean:
	rm -rf $(WORK_LOAD)/build && rm $(TARGET)

info-%:
	@echo $(OBJS)
	$(MAKE) --dry-run --always-make $* | grep -v "info"

需要注意一点若上述LD是ld而不是gcc,那么可能会出现一些奇怪的问题,比如:ld: warning: cannot find entry symbol _start;找不到标准库中的一些函数,printf,open之类的,即使添加了头文件

这是因为ld 默认并不会自动链接 C 标准库。如果你需要使用标准库函数,你需要显式添加 -lc 来链接 C 标准库

在 Linux 系统中,默认情况下,程序的入口点是 _start,这是由操作系统加载程序后执行的第一个位置。通常,_start 是由运行时启动代码(如 C 的 crt0.o 或 crt1.o)提供的。当使用 ld 直接链接时,ld 不会自动包含启动代码。这个启动代码通常是 crt0.o 或类似的文件,负责设置运行时环境并调用 main 函数。

但是对于上述问题gcc都会自动帮我们解决

posted @ 2024-10-18 10:58  次林梦叶  阅读(16)  评论(0编辑  收藏  举报