make命令

make 命令 #

make是一个构建自动化工具,用于管理源代码的编译和构建过程。它根据指定的规则(通常定义在名为"Makefile"的文件中)确定哪些部分的程序需要重新编译,并发出必要的命令来重新编译它们。

语法 #

make [选项] [目标] [变量=]

常用选项 #

选项 描述
-f, --file=文件 指定Makefile文件(默认为"Makefile"或"makefile")
-j, --jobs=N 允许N个作业同时运行(并行构建)
-k, --keep-going 遇到错误时继续执行,不停止
-n, --just-print 只打印命令,不执行(干运行)
-s, --silent 不显示执行的命令
-B, --always-make 无条件重新构建所有目标
-C, --directory=目录 在执行前先切换到指定目录
-d 打印调试信息
-e, --environment-overrides 环境变量覆盖Makefile中的变量
-i, --ignore-errors 忽略命令错误
-p, --print-data-base 打印make的内部数据库
-q, --question 不运行命令;如果目标已是最新的则退出状态为0
-r, --no-builtin-rules 禁用内置隐含规则
-t, --touch 更新目标的时间戳而不实际重新构建它们
-v, --version 显示版本信息
-w, --print-directory 打印当前目录
--help 显示帮助信息

Makefile基本结构 #

Makefile由一系列规则组成,每个规则定义了如何构建一个目标文件:

目标: 依赖1 依赖2 ...
	命令1
	命令2
	...
  • 目标:要创建的文件名或动作名称
  • 依赖:构建目标所需的文件或其他目标
  • 命令:构建目标的shell命令(必须以制表符开头)

常见用法 #

1. 基本构建 #

make

这将构建Makefile中的第一个目标(默认目标)。

2. 构建特定目标 #

make target_name

3. 并行构建 #

make -j4

这将允许最多4个作业同时运行,加速构建过程。

4. 干运行(不实际执行命令) #

make -n

5. 使用特定的Makefile #

make -f MyMakefile

6. 在特定目录中运行make #

make -C src

7. 传递变量 #

make CFLAGS="-O2 -Wall"

8. 忽略错误继续构建 #

make -k

Makefile示例 #

1. 简单的C程序Makefile #

CC = gcc
CFLAGS = -Wall -g

all: program

program: main.o utils.o
	$(CC) $(CFLAGS) -o program main.o utils.o

main.o: main.c utils.h
	$(CC) $(CFLAGS) -c main.c

utils.o: utils.c utils.h
	$(CC) $(CFLAGS) -c utils.c

clean:
	rm -f program *.o

.PHONY: all clean

2. 使用变量和模式规则 #

CC = gcc
CFLAGS = -Wall -g
SOURCES = main.c utils.c helper.c
OBJECTS = $(SOURCES:.c=.o)
TARGET = program

all: $(TARGET)

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

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

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

.PHONY: all clean

3. 多目录项目 #

CC = gcc
CFLAGS = -Wall -g
INCLUDES = -Iinclude
SOURCES = src/main.c src/utils.c
OBJECTS = $(SOURCES:.c=.o)
TARGET = bin/program

all: directories $(TARGET)

directories:
	mkdir -p bin

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

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

clean:
	rm -f $(TARGET) $(OBJECTS)
	rmdir bin

.PHONY: all clean directories

特殊变量和函数 #

特殊变量 #

变量 描述
$@ 当前目标的名称
$< 第一个依赖项的名称
$^ 所有依赖项的列表,去除重复项
$+ 所有依赖项的列表,保留重复项
$* 目标的主干部分(不包括后缀)
$? 比目标更新的依赖项列表
$(@D) 目标的目录部分
$(@F) 目标的文件部分
$(<D) 第一个依赖项的目录部分
$(<F) 第一个依赖项的文件部分

常用函数 #

函数 描述 示例
$(wildcard 模式) 获取匹配模式的文件列表 $(wildcard *.c)
$(patsubst 模式,替换,文本) 模式替换 $(patsubst %.c,%.o,$(SOURCES))
$(subst 字符串1,字符串2,文本) 字符串替换 $(subst .c,.o,$(SOURCES))
$(dir 名称) 提取目录部分 $(dir src/file.c) 返回 src/
$(notdir 名称) 提取文件名部分 $(notdir src/file.c) 返回 file.c
$(addprefix 前缀,名称) 添加前缀 $(addprefix src/,file1.c file2.c)
$(addsuffix 后缀,名称) 添加后缀 $(addsuffix .c,file1 file2)
$(shell 命令) 执行shell命令并返回结果 $(shell ls -1)
$(foreach 变量,列表,文本) 对列表中的每个元素执行操作 $(foreach file,$(FILES),$(file).o)
$(if 条件,真值,假值) 条件判断 $(if $(DEBUG),debug,release)
$(filter 模式,文本) 过滤匹配模式的单词 $(filter %.c %.h,$(FILES))
$(filter-out 模式,文本) 过滤不匹配模式的单词 $(filter-out %.o,$(FILES))
$(sort 列表) 排序并去重 $(sort $(FILES))
$(word N,文本) 返回文本中的第N个单词 $(word 2,foo bar baz) 返回 bar
$(wordlist 开始,结束,文本) 返回文本中的单词范围 $(wordlist 2,3,foo bar baz)
$(words 文本) 返回文本中的单词数 $(words foo bar baz) 返回 3
$(firstword 文本) 返回文本中的第一个单词 $(firstword foo bar) 返回 foo
$(lastword 文本) 返回文本中的最后一个单词 $(lastword foo bar) 返回 bar

高级特性 #

1. 伪目标 #

伪目标是不代表实际文件的目标,通常用于执行操作。使用.PHONY声明伪目标:

.PHONY: clean all install test

clean:
	rm -f *.o program

all: program

install: program
	cp program /usr/local/bin/

test: program
	./program --test

2. 条件语句 #

DEBUG = 1

ifeq ($(DEBUG), 1)
  CFLAGS = -g -Wall
else
  CFLAGS = -O2 -Wall
endif

ifdef VERBOSE
  ECHO = @echo
else
  ECHO = @:
endif

3. 包含其他Makefile #

include common.mk
include rules/*.mk

4. 自动依赖生成 #

SOURCES = main.c utils.c
DEPENDS = $(SOURCES:.c=.d)

%.d: %.c
	@$(CC) -MM -MP -MT "$*.o $@" $< > $@

-include $(DEPENDS)

5. 目录搜索 #

VPATH = src:include

program: main.o utils.o
	$(CC) $(CFLAGS) -o $@ $^

实用示例 #

1. 自动查找源文件 #

SOURCES := $(wildcard src/*.c)
OBJECTS := $(SOURCES:.c=.o)
TARGET = program

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

2. 多目标构建 #

all: program1 program2

program1: program1.o common.o
	$(CC) $(CFLAGS) -o $@ $^

program2: program2.o common.o
	$(CC) $(CFLAGS) -o $@ $^

3. 静态模式规则 #

OBJECTS = main.o utils.o helper.o

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

4. 递归调用make #

SUBDIRS = lib src tests

all:
	for dir in $(SUBDIRS); do \
		$(MAKE) -C $$dir; \
	done

clean:
	for dir in $(SUBDIRS); do \
		$(MAKE) -C $$dir clean; \
	done

5. 自定义错误处理 #

check-env:
ifndef ENV_VAR
	$(error Environment variable ENV_VAR is not set)
endif

target: check-env
	echo "ENV_VAR is set to $(ENV_VAR)"

常见问题排查 #

1. 制表符与空格 #

Makefile中的命令必须以制表符开头,不能使用空格。如果使用空格,会出现以下错误:

Makefile:3: *** missing separator. Stop.

2. 循环依赖 #

当目标直接或间接依赖于自身时,会出现循环依赖错误:

make: Circular target1 <- target2 <- target1 dependency dropped.

3. 命令执行错误 #

当命令返回非零退出状态时,make会停止执行:

make: *** [target] Error 1

使用-k选项可以让make在遇到错误时继续执行其他目标。

4. 文件时间戳问题 #

make依赖文件的修改时间来决定是否需要重新构建目标。如果时钟被调整或文件系统不支持精确的时间戳,可能会导致不必要的重新构建或跳过必要的构建。

使用touch命令或-t选项可以更新文件的时间戳。

与其他构建工具的比较 #

工具 优点 缺点 适用场景
make 简单、广泛可用、灵活 语法严格、跨平台性差 小到中型项目、C/C++项目
CMake 跨平台、生成本地构建文件 学习曲线陡峭 大型跨平台项目
Ninja 非常快速、简单 不适合直接编写 大型项目、作为其他构建系统的后端
Ant/Maven Java生态系统集成 主要针对Java Java项目
Gradle 灵活、基于Groovy/Kotlin 较复杂 大型Java/Android项目

提示 #

  • 使用-j选项启用并行构建,加速构建过程
  • 使用-n选项进行干运行,查看将要执行的命令
  • 使用变量和函数使Makefile更加灵活和可维护
  • 为常用操作(如clean、install、test)创建伪目标
  • 使用自动依赖生成避免手动维护依赖关系
  • 将复杂的Makefile分解为多个文件,使用include包含它们
  • 使用条件语句处理不同的构建配置
  • 使用.PHONY声明所有不代表实际文件的目标
  • 使用$(shell)函数与系统交互,但要注意可移植性
  • 使用make -p查看make的内部数据库,了解内置规则和变量