Makefile

一、通用示例

CC = gcc
CFLAGS = -lpthread -L ./tlACSClib -lNetDEVSDK
GFLAGS = -g -Wall -O3
TARGET = tlACSC
SRCS = $(wildcard *.c)
OBJS = $(patsubst %c, %o, $(SRCS))

$(TARGET) : $(OBJS)
$(CC) $(GFLAGS) $^ -o $(TARGET) $(CFLAGS)

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

.PHONY : clean
clean:
rm -rf $(OBJS) $(TARGET)

二、= 和 :=

      1、“=”

      make会将整个makefile展开后,再决定变量的值。也就是说,变量的值将会是整个makefile中最后被指定的值。看例子:

            x = foo
            y = $(x) bar
            x = xyz

      在上例中,y的值将会是 xyz bar ,而不是 foo bar 。

      2、“:=”

      “:=”表示变量的值决定于它在makefile中的位置,而不是整个makefile展开后的最终值。

            x := foo
            y := $(x) bar
            x := xyz

      在上例中,y的值将会是 foo bar ,而不是 xyz bar 了。

3、

other= 是最基本的赋值
:= 是覆盖之前的值
?= 是如果没有被赋值过就赋予等号后面的值
+= 是添加等号后面的值

4、include、-include、sinclude

include包含其他的Makefile文件或者.mk文件。从而可以使用包含文件中的变量。
-include是指定编译器,即使没有这个文件也照常编译,不报错。编译继续进行。
sinclude和-include的效果一样

5、命令前加@和-

如果make执行的命令前面加了@字符,则不显示命令本身而只显示它的结果; 

通常make执行的命令如果出错(该命令的退出状态非0)就立刻终止,不再执行后续命令,但如果命令前面加了-号,即使这条命令出错,make也会继续执行后续命令

6、多级目录make -C

确实,当一个Makefile包含另一个Makefile时,可以实现递归构建,这在管理多目录项目时非常有用。这种方法允许你将大型项目分解成更小的、更易于管理的部分,每个部分都有自己的Makefile。然后,主Makefile可以调用这些子Makefile来构建整个项目。

递归构建通常通过以下方式实现:

在主Makefile中使用make -C命令:
-C选项告诉make切换到指定的目录,并在那里执行Makefile。这允许你从项目的顶层Makefile中调用位于子目录中的Makefile。例如,如果你的项目结构如下:

project/

├── Makefile # 主Makefile

├── src/

│ └── Makefile # 子Makefile

└── include/

└── …

你可以在顶层Makefile中这样写:

all: 
    make -C src

这将切换到src目录并执行那里的Makefile。

在子Makefile中定义目标:
每个子Makefile应该定义它自己的构建目标,这些目标可以被顶层Makefile调用。这样,你可以在每个子目录中独立地管理构建过程。

传递变量:
有时,你可能需要在子Makefile中使用顶层Makefile中定义的变量。你可以使用make的命令行参数来传递这些变量。例如,在顶层Makefile中:

COMMON_FLAGS = -Wall -g

all:

make -C src COMMON_FLAGS=”$(COMMON_FLAGS)”

然后在子Makefile中,你可以这样使用这些变量:

gcc $(COMMON_FLAGS) -o my_program my_program.c

递归深度:
注意,递归构建可能会增加构建的复杂性,特别是当项目非常大且包含许多子目录时。确保你的Makefile结构清晰,并且每个子Makefile都明确其职责,以避免不必要的复杂性。

使用include指令:
虽然include指令本身不直接支持递归构建,但它可以用于将公共的Makefile片段(如变量定义、规则等)包含到多个Makefile中,从而减少重复代码并提高可维护性。

递归构建是多目录项目管理中的一种强大工具,但也需要谨慎使用,以确保构建过程的清晰性、可维护性和效率。

7、在Makefile的上下文中,由于每行通常都被视为一个独立的命令

在Makefile中使用if语句时,为了可读性和维护性,通常建议将if语句的各个部分(如条件判断、then子句、else子句和fi结束标记)写成多行。

Makefile中的if语句实际上是通过调用shell脚本来实现的,因此它们遵循shell脚本的语法规则。在shell脚本中,if语句可以跨越多行,而Makefile只是提供了一个执行这些脚本的环境。

为了将if语句写成多行,你可以使用反斜杠(\)来指示当前行的继续,或者在每个逻辑行的末尾使用换行符。然而,在Makefile的上下文中,由于每行通常都被视为一个独立的命令,因此你通常需要使用反斜杠来明确指示if语句的跨行继续。

以下是一个Makefile中使用if语句并写成多行的示例:

target:  
    @if [ "$(SOME_VARIABLE)" = "some_value" ]; then \  
        echo "SOME_VARIABLE is set to some_value"; \  
    else \  
        echo "SOME_VARIABLE is not set to some_value"; \  
    fi

在这个例子中,if语句被分成了多行,每行的末尾都使用了反斜杠来指示下一行是当前逻辑行的继续。这样做提高了代码的可读性,使得其他开发者更容易理解你的意图。

因此,尽管技术上可能将if语句的所有部分压缩到同一逻辑行(通过省略反斜杠并使用分号分隔命令),但这样做通常是不推荐的,因为它会牺牲代码的可读性和可维护性。最佳实践是将if语句写成多行,以清晰地表达条件逻辑和相应的操作。

Makefile的条件判断【ifeq、else、endif】在解析阶段就执行了,不是在命令执行阶段。可以使用shell判断代替make条件。

参考手册:

Makefile 命令大全

一、基本命令语法

1. 命令格式

target: dependencies
    command1
    command2
    # 注释

2. 命令前缀

前缀 说明 示例
(无) 显示执行的命令 echo "hello"
@ 不显示命令本身 @echo "hello"
- 忽略错误继续执行 -rm -f file
+ 即使使用 -n/-t/-q 也要执行 +make clean

二、常用命令操作

1. 文件操作

# 创建目录
mkdir -p dir/subdir

# 复制文件
cp source dest
cp -r source_dir/ dest_dir/

# 移动/重命名
mv old new

# 删除文件
rm -f file           # 强制删除
rm -rf directory/    # 递归删除目录

# 创建空文件
touch file

# 创建链接
ln -s target link_name

2. 文本处理

# 输出文本
echo "text"
echo -n "no newline"

# 写入文件
echo "content" > file
echo "more" >> file      # 追加

# 读取文件
cat file
head -n 10 file
tail -n 5 file

# 文本替换
sed 's/old/new/g' file
sed -i 's/old/new/g' file  # 直接修改文件

# 查找文本
grep "pattern" file
grep -r "pattern" dir/     # 递归查找

# 行数统计
wc -l file

3. 条件判断

# if 语句
if [ condition ]; then \
    command1; \
else \
    command2; \
fi

# 测试文件
if [ -f file ]; then        # 文件存在
if [ -d dir ]; then         # 目录存在
if [ -x file ]; then        # 文件可执行
if [ -s file ]; then        # 文件非空
if [ -z "$var" ]; then      # 变量为空
if [ -n "$var" ]; then      # 变量非空

# 字符串比较
if [ "$a" = "$b" ]; then    # 相等
if [ "$a" != "$b" ]; then   # 不相等
if [ "$a" \< "$b" ]; then   # 小于(字典序)

# 数字比较
if [ $a -eq $b ]; then      # 等于
if [ $a -ne $b ]; then      # 不等于
if [ $a -lt $b ]; then      # 小于
if [ $a -gt $b ]; then      # 大于
if [ $a -le $b ]; then      # 小于等于
if [ $a -ge $b ]; then      # 大于等于

4. 循环控制

# for 循环
for i in 1 2 3 4 5; do \
    echo $$i; \
done

# while 循环
counter=0; \
while [ $$counter -lt 5 ]; do \
    echo $$counter; \
    counter=$$(($$counter + 1)); \
done

# until 循环
counter=0; \
until [ $$counter -ge 5 ]; do \
    echo $$counter; \
    counter=$$(($$counter + 1)); \
done

5. 变量操作

# 定义变量
VAR = value
VAR := immediate_value
VAR ?= default_if_empty
VAR += additional_value

# 使用变量
echo $(VAR)
echo $${SHELL_VAR}    # Make变量用$(),shell变量用$$

# 特殊变量
$@     # 目标名
$<     # 第一个依赖
$^     # 所有依赖
$?     # 比目标新的依赖
$*     # 模式匹配的部分

三、函数使用

1. 字符串函数

# 字符串替换
$(subst from,to,text)          # 文本替换
$(patsubst pattern,replacement,text) # 模式替换
$(strip string)                # 去除空格

# 查找过滤
$(findstring find,in)          # 查找子串
$(filter pattern...,text)      # 保留匹配项
$(filter-out pattern...,text)  # 排除匹配项

# 排序去重
$(sort list)                   # 排序并去重
$(word n,text)                 # 取第n个单词
$(wordlist s,e,text)           # 取单词列表
$(words text)                  # 单词个数
$(firstword names...)          # 第一个单词
$(lastword names...)           # 最后一个单词

2. 文件名函数

# 目录/文件部分
$(dir names...)                # 目录部分
$(notdir names...)             # 非目录部分
$(suffix names...)             # 后缀部分
$(basename names...)           # 前缀部分
$(addsuffix suffix,names...)   # 添加后缀
$(addprefix prefix,names...)   # 添加前缀

# 路径操作
$(abspath names...)            # 绝对路径
$(realpath names...)           # 真实路径

3. 条件函数

# 条件判断
$(if condition,then-part[,else-part])
$(or condition1[,condition2...])
$(and condition1[,condition2...])

4. 文件操作函数

# 文件存在性
$(wildcard pattern)            # 通配符匹配
$(shell command)               # 执行shell命令

5. 控制函数

# 循环
$(foreach var,list,text)       # 循环处理

# 调用用户函数
$(call variable,param,...)     # 调用自定义函数

# 值函数
$(value variable)              # 获取未展开的值
$(eval text)                   # 动态生成make代码
$(origin variable)             # 变量来源
$(flavor variable)             # 变量类型

四、高级命令技巧

1. 多行命令

target:
    command1 && \
    command2 && \
    command3

# 或使用 .ONESHELL
.ONESHELL:
target:
    command1
    command2
    command3

2. 错误处理

# 忽略错误
target:
    -false
    echo "继续执行"

# 检查命令返回值
target:
    if ! command; then \
        echo "命令失败"; \
        exit 1; \
    fi

# set -e 模式
target:
    set -e; \
    command1; \
    command2

3. 调试命令

# 调试输出
$(info 信息: $(VAR))
$(warning 警告: $(VAR))
$(error 错误: $(VAR))

# 命令调试
target:
    @echo "调试: VAR=$(VAR)"
    $(info 调试: 目标=$@)

# 使用 --debug 选项
# make --debug=v 或 make --debug=i

4. 时间戳命令

target:
    @echo "开始: $$(date +%Y-%m-%d_%H:%M:%S)"
    # 执行命令
    @echo "结束: $$(date +%Y-%m-%d_%H:%M:%S)"

# 计算执行时间
target:
    @start=$$(date +%s); \
    # 执行命令
    @end=$$(date +%s); \
    echo "耗时: $$((end - start)) 秒"

五、实用命令模板

1. 编译C/C++项目

CC = gcc
CFLAGS = -Wall -O2
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
TARGET = program

$(TARGET): $(OBJS)
    $(CC) $(CFLAGS) -o $@ $^

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

clean:
    rm -f $(OBJS) $(TARGET)

2. 安装脚本

PREFIX = /usr/local
BINDIR = $(PREFIX)/bin
DATADIR = $(PREFIX)/share/myapp

install:
    install -d $(BINDIR) $(DATADIR)
    install -m 755 myapp $(BINDIR)/
    install -m 644 data/* $(DATADIR)/

uninstall:
    rm -f $(BINDIR)/myapp
    rm -rf $(DATADIR)

3. 备份脚本

BACKUP_DIR = backups
TIMESTAMP = $(shell date +%Y%m%d_%H%M%S)

backup:
    mkdir -p $(BACKUP_DIR)
    tar -czf $(BACKUP_DIR)/backup_$(TIMESTAMP).tar.gz src/ data/
    find $(BACKUP_DIR) -name "*.tar.gz" -mtime +30 -delete

4. 测试脚本

test:
    @echo "运行单元测试..."
    @for test in tests/*_test; do \
        echo "运行 $$test"; \
        if ! ./$$test; then \
            echo "测试失败: $$test"; \
            exit 1; \
        fi; \
    done
    @echo "所有测试通过"

5. 环境检查

check-env:
    @echo "检查依赖..."
    @for cmd in gcc make python3; do \
        if ! command -v $$cmd >/dev/null 2>&1; then \
            echo "错误: $$cmd 未安装"; \
            exit 1; \
        fi; \
    done
    @echo "所有依赖已安装"

六、特殊目标

1. 伪目标

.PHONY: all clean install uninstall help

2. 特殊内置目标

.DEFAULT:            # 默认目标
.ONESHELL:           # 所有命令在同一个shell执行
.SILENT:             # 静默模式(不显示命令)
.IGNORE:             # 忽略错误
.EXPORT_ALL_VARIABLES: # 导出所有变量
.NOTPARALLEL:        # 禁止并行执行

3. 模式规则

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

%.txt: %.md
    pandoc $< -o $@

七、命令行选项

1. 常用选项

make                   # 执行默认目标
make target           # 执行指定目标
make -j4              # 并行执行(4个任务)
make -n               # 只显示命令,不执行
make -s               # 静默模式
make -k               # 出错继续执行其他目标
make -i               # 忽略所有错误
make -B               # 强制重建所有目标
make -C dir           # 进入目录执行
make -f file          # 使用指定Makefile

2. 调试选项

make --debug[=FLAGS]  # 调试输出
make -d               # 详细调试信息
make -p               # 打印数据库
make -q               # 查询是否需要重建
make -r               # 不使用内置规则
make -R               # 不使用内置变量

八、环境变量

1. Make相关

MAKELEVEL             # Make递归调用层级
MAKEFLAGS             # Make标志
MAKECMDGOALS          # 命令行指定的目标

2. 编译相关

CC                    # C编译器
CXX                   # C++编译器
CFLAGS                # C编译选项
CXXFLAGS              # C++编译选项
LDFLAGS               # 链接选项

九、最佳实践示例

1. 完整项目模板

# 项目配置
PROJECT = myapp
VERSION = 1.0.0
PREFIX = /usr/local

# 源文件
SRC_DIR = src
INC_DIR = include
BUILD_DIR = build
DIST_DIR = dist

# 工具链
CC = gcc
CFLAGS = -Wall -O2 -I$(INC_DIR)
LDFLAGS = -lm

# 自动查找源文件
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))

# 默认目标
all: $(BUILD_DIR)/$(PROJECT)

# 链接
$(BUILD_DIR)/$(PROJECT): $(OBJS)
    @mkdir -p $(@D)
    $(CC) -o $@ $^ $(LDFLAGS)

# 编译
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
    @mkdir -p $(@D)
    $(CC) $(CFLAGS) -c $< -o $@

# 清理
clean:
    rm -rf $(BUILD_DIR) $(DIST_DIR)

# 安装
install: all
    install -d $(PREFIX)/bin
    install -m 755 $(BUILD_DIR)/$(PROJECT) $(PREFIX)/bin/

# 打包
dist: all
    @mkdir -p $(DIST_DIR)
    tar -czf $(DIST_DIR)/$(PROJECT)-$(VERSION).tar.gz \
        $(BUILD_DIR)/$(PROJECT) \
        README.md LICENSE

# 帮助
help:
    @echo "可用目标:"
    @echo "  all      - 编译项目(默认)"
    @echo "  clean    - 清理构建文件"
    @echo "  install  - 安装到系统"
    @echo "  dist     - 打包发布"
    @echo "  help     - 显示此帮助"

.PHONY: all clean install dist help

2. 多配置管理

# 配置选择
CONFIG ?= release

ifeq ($(CONFIG),debug)
    CFLAGS += -g -DDEBUG -O0
else ifeq ($(CONFIG),release)
    CFLAGS += -O3 -DNDEBUG
else ifeq ($(CONFIG),profile)
    CFLAGS += -pg -O2
endif

# 根据配置输出到不同目录
BUILD_DIR = build/$(CONFIG)