JDWA 技术文档
首页
  • 数据库
  • 前端开发
  • 后端开发
  • 开发工具
  • 虚拟化技术
  • KVM显卡直通
  • FPGA仿真固件
  • 项目实战
  • 踩坑记录
  • 开发心得
  • 软件工具
  • 学习资料
  • 开发环境
更新日志
关于我
Gitee
GitHub
首页
  • 数据库
  • 前端开发
  • 后端开发
  • 开发工具
  • 虚拟化技术
  • KVM显卡直通
  • FPGA仿真固件
  • 项目实战
  • 踩坑记录
  • 开发心得
  • 软件工具
  • 学习资料
  • 开发环境
更新日志
关于我
Gitee
GitHub
  • 数据库

    • 数据库教程
    • MySQL免安装版使用指南
    • MySQL性能优化实践
    • Redis入门与实践
    • MinIO快速部署指南
    • MinIO基础使用教程
  • 前端开发

    • 前端开发教程
    • Vue.js开发最佳实践
    • CSS常用技巧与解决方案
    • JavaScript实用技巧与编程模式
    • CSS Grid布局教程
  • 后端开发

    • 后端开发教程
    • Spring Boot实战指南
    • Node.js Express 框架开发实战指南
    • Python Flask 框架开发指南
  • 开发工具

    • 开发工具教程
    • Git 基础教程
    • Git工作流实践指南
    • VS Code 全面使用指南
    • VS Code必装插件推荐
    • Docker基础入门
    • IntelliJ IDEA 使用技巧
    • Eclipse配置与优化
    • Sublime Text 高级技巧
    • Vim 从入门到精通
    • Maven 详解
    • Gradle 入门与进阶
    • Webpack 配置指南
    • npm 与 yarn 使用技巧
    • Makefile 编写指南
    • Navicat 使用指南
    • MCP本地部署教程
  • 虚拟化技术

    • JDWA虚拟化技术专题
    • KVM虚拟机去虚拟化技术详解
  • KVM显卡直通

    • KVM显卡GPU直通教程
  • FPGA仿真固件

    • FPGA仿真固件开发指南
    • 基础-完整设备仿真定制固件开发指南
    • 中级-完整设备仿真定制固件开发指南
    • 高级-完整设备仿真定制固件开发指南

Makefile 编写指南

Tips

本教程全面介绍 Makefile 的编写方法和使用技巧,从基础语法到高级应用,适用于 GNU Make 3.8 及更高版本。

1. Makefile 简介

Make 是一个自动化构建工具,用于管理源代码的编译和构建过程。它通过读取 Makefile 文件中的规则,智能地确定需要更新的文件,并执行相应的命令。尽管 Make 最初是为编译 C 程序设计的,但它已成为通用的项目构建工具。

1.1 Make 的核心功能

  • 依赖跟踪:Make 会分析文件之间的依赖关系
  • 增量构建:只重新构建必要的部分,节省时间
  • 并行执行:可以并行运行多个任务,提高效率
  • 可移植性:支持不同的操作系统和环境

1.2 Makefile 的作用

Makefile 定义了:

  • 构建目标和依赖的关系
  • 构建每个目标需要执行的命令
  • 变量和函数,使构建过程更灵活和可维护
  • 条件和控制逻辑,适应不同的构建环境

2. Makefile 基础语法

2.1 规则结构

Makefile 的基本构建单元是规则(rule),规则的一般形式为:

target: prerequisites
	commands
  • target:要构建的目标(文件名或标签)
  • prerequisites:构建目标所需的依赖(文件或其他目标)
  • commands:构建目标所需执行的命令(必须以Tab键开头)

例如:

hello: main.c functions.c
	gcc -o hello main.c functions.c

2.2 伪目标

伪目标是一个标签,而不是实际的文件名。使用 .PHONY 指令声明伪目标:

.PHONY: clean all test

clean:
	rm -f *.o hello

all: hello

test: hello
	./hello --test

伪目标的优点:

  • 总是执行其命令,不考虑文件时间戳
  • 避免与同名文件冲突
  • 提供便捷的操作接口

2.3 多目标和模式规则

多目标规则:

prog1 prog2 prog3: common.h
	cc -o $@ $@.c

模式规则(使用 % 通配符):

%.o: %.c
	gcc -c -o $@ $<

2.4 include 指令

可以将 Makefile 内容分割到多个文件中:

include config.mk functions.mk

3. 变量与执行控制

3.1 变量定义与使用

Makefile 中的变量在使用时展开:

# 简单变量定义(:=)- 立即展开
CC := gcc
CFLAGS := -Wall -O2

# 递归变量定义(=)- 使用时展开
SOURCES = main.c helper.c $(OTHER_FILES)

# 条件变量定义(?=)- 仅在变量未定义时设置
DEBUG ?= 0

# 追加定义(+=)
CFLAGS += -g

# 使用变量
prog: $(SOURCES)
	$(CC) $(CFLAGS) -o $@ $^

3.2 自动变量

Make 提供了一些特殊的自动变量:

变量含义
$@当前目标的名称
$<第一个依赖项
$^所有依赖项(去重)
$+所有依赖项(保留重复)
$*匹配模式规则中 % 的部分
$(@D)目标的目录部分
$(@F)目标的文件名部分

例如:

output/%.o: src/%.c
	@mkdir -p $(@D)
	$(CC) $(CFLAGS) -c -o $@ $<

3.3 条件指令

Makefile 支持条件控制:

# if-else 条件
ifeq ($(DEBUG), 1)
    CFLAGS += -g -DDEBUG
else
    CFLAGS += -O2
endif

# 检查变量是否定义
ifdef VERBOSE
    SILENT =
else
    SILENT = @
endif

# 检查变量是否为空
ifneq ($(strip $(SOURCES)),)
    # 处理非空情况
endif

3.4 函数

Make 内置了许多函数用于文本处理:

# 文件名处理
SOURCES := $(wildcard src/*.c)
OBJECTS := $(patsubst src/%.c,build/%.o,$(SOURCES))

# 文本操作
LIBS := math util io
LINKS := $(addprefix -l,$(LIBS))  # 结果: -lmath -lutil -lio

# 条件函数
OPTIONAL_FLAGS := $(if $(DEBUG),-g,)

# 循环
dirs := a b c
files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))

# 调用 shell 命令
COMMIT_ID := $(shell git rev-parse --short HEAD)

4. 高级 Makefile 技巧

4.1 自动依赖生成

对于 C/C++ 项目,自动跟踪头文件依赖非常重要:

# 生成和包含依赖文件
DEPFILES := $(OBJECTS:.o=.d)

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

-include $(DEPFILES)

4.2 多目录项目结构

管理多目录项目:

SRC_DIR := src
OBJ_DIR := obj
BIN_DIR := bin

SOURCES := $(wildcard $(SRC_DIR)/*.c)
OBJECTS := $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SOURCES))
TARGET  := $(BIN_DIR)/program

$(TARGET): $(OBJECTS) | $(BIN_DIR)
	$(CC) $(LDFLAGS) -o $@ $^

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
	$(CC) $(CFLAGS) -c -o $@ $<

$(BIN_DIR) $(OBJ_DIR):
	mkdir -p $@

4.3 并行构建与性能优化

# 使用 make -j8 可并行执行8个任务

# 避免不必要的重新构建
.NOTPARALLEL: setup    # 特定规则禁用并行
.PHONY: all test clean

# 次序依赖(order-only prerequisites)
$(OBJECTS): | setup_environment

# 使用 FORCE 模式确保某些目标总是检查
.PHONY: FORCE
timestamp: FORCE
	@if [ -f $@ ]; then touch --no-create $@; else touch $@; fi

4.4 调试 Makefile

# 打印变量值
debug:
	@echo "SOURCES: $(SOURCES)"
	@echo "OBJECTS: $(OBJECTS)"
	@echo "CFLAGS: $(CFLAGS)"

# 启用调试模式
# 使用 make --debug=basic 或 make --debug=all

5. 特定类型项目的 Makefile

5.1 C/C++ 项目 Makefile

完整的 C 项目 Makefile 示例:

# 项目配置
CC := gcc
CFLAGS := -Wall -Wextra -std=c11
LDFLAGS := -lm
TARGET := myprogram

# 目录结构
SRC_DIR := src
INC_DIR := include
OBJ_DIR := obj
BIN_DIR := bin

# 文件查找
SRCS := $(wildcard $(SRC_DIR)/*.c)
OBJS := $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SRCS))
DEPS := $(OBJS:.o=.d)

# 默认目标
.PHONY: all clean
all: $(BIN_DIR)/$(TARGET)

# 链接目标
$(BIN_DIR)/$(TARGET): $(OBJS) | $(BIN_DIR)
	$(CC) -o $@ $^ $(LDFLAGS)

# 编译规则
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
	$(CC) $(CFLAGS) -I$(INC_DIR) -MMD -MP -c -o $@ $<

# 创建目录
$(BIN_DIR) $(OBJ_DIR):
	mkdir -p $@

# 包含依赖文件
-include $(DEPS)

# 清理规则
clean:
	rm -rf $(OBJ_DIR) $(BIN_DIR)

5.2 多语言项目示例

包含 C++ 和 CUDA 的混合项目:

# 编译器设置
CXX := g++
NVCC := nvcc
CXXFLAGS := -Wall -std=c++17 -O2
NVCCFLAGS := -arch=sm_75 -std=c++17

# 文件和目录
CPP_SRCS := $(wildcard src/*.cpp)
CUDA_SRCS := $(wildcard src/*.cu)
CPP_OBJS := $(patsubst src/%.cpp,obj/%.o,$(CPP_SRCS))
CUDA_OBJS := $(patsubst src/%.cu,obj/%.o,$(CUDA_SRCS))
OBJS := $(CPP_OBJS) $(CUDA_OBJS)

# 主要目标
all: bin/program

bin/program: $(OBJS) | bin
	$(CXX) -o $@ $^ -lcudart -L/usr/local/cuda/lib64

# C++ 编译规则
obj/%.o: src/%.cpp | obj
	$(CXX) $(CXXFLAGS) -c -o $@ $<

# CUDA 编译规则
obj/%.o: src/%.cu | obj
	$(NVCC) $(NVCCFLAGS) -c -o $@ $<

# 创建目录
bin obj:
	mkdir -p $@

# 清理规则
clean:
	rm -rf obj bin

5.3 Web 前端项目示例

前端项目构建 Makefile:

.PHONY: all build clean deploy test

# 目录设置
SRC_DIR := src
DIST_DIR := dist
NODE_MODULES := node_modules

# 依赖检查
$(NODE_MODULES):
	npm install

# 构建目标
all: build

build: $(NODE_MODULES)
	npm run build

# 开发服务器
dev: $(NODE_MODULES)
	npm run dev

# 测试
test: $(NODE_MODULES)
	npm run test

# 部署
deploy: build
	rsync -avz --delete $(DIST_DIR)/ user@server:/var/www/site/

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

6. Makefile 最佳实践

6.1 使用变量和函数

  • 使用变量定义常量,避免硬编码
  • 使用函数自动找到源文件,避免手动列表维护
  • 使用模式规则简化重复命令
  • 将通用设置提取到变量中,便于配置

6.2 提高可读性和可维护性

  • 添加注释说明复杂规则和变量的用途
  • 使用有意义的目标名称
  • 将相关目标分组并添加描述性注释
  • 使用 help 目标提供可用命令说明
.PHONY: help
help:
	@echo "使用方式:"
	@echo "  make [target]"
	@echo ""
	@echo "目标:"
	@echo "  all       构建完整项目"
	@echo "  clean     删除所有生成的文件"
	@echo "  test      运行测试"
	@echo "  install   安装到系统"

6.3 增强健壮性和可移植性

  • 使用 @ 前缀减少输出噪音
  • 使用 -k 选项继续执行,不因一个目标失败而停止
  • 使用条件判断适应不同环境
  • 使用 $(shell uname) 检测操作系统
# 操作系统检测
UNAME := $(shell uname)

ifeq ($(UNAME), Linux)
    # Linux 特定配置
    CC := gcc
else ifeq ($(UNAME), Darwin)
    # macOS 特定配置
    CC := clang
else
    # Windows 或其他系统配置
    CC := gcc
endif

6.4 常见错误和调试技巧

常见错误:

  • Tab vs 空格错误(Make 要求命令以 Tab 开头)
  • 循环依赖问题
  • 缺少声明伪目标
  • 变量引用错误(${} vs $())

调试技巧:

# 打印执行的命令
VERBOSE ?= 0
ifeq ($(VERBOSE), 1)
    SILENT :=
else
    SILENT := @
endif

target:
	$(SILENT)echo "Building $@"
	$(SILENT)gcc -o $@ $^

# 跟踪特定变量变化
$(warning CFLAGS=$(CFLAGS))

# 使用 --just-print 选项测试而不执行
# make -n target

7. GNU Make 高级特性

7.1 二次展开

使用二次展开(secondary expansion)解决复杂依赖:

.SECONDEXPANSION:
MODULES = a b c
OBJECTS = $(foreach mod,$(MODULES),$(mod).o)

all: prog

prog: $$(patsubst %,%.o,$(MODULES))
	$(CC) -o $@ $^

7.2 自定义函数

定义和使用自定义函数:

# 定义函数
define compile_module
$(1).o: $(1).c $(1).h common.h
	$(CC) $(CFLAGS) -c -o $$@ $$<
endef

# 使用函数
$(foreach mod,$(MODULES),$(eval $(call compile_module,$(mod))))

7.3 Makefile 目标执行控制

  • .ONESHELL:使一个规则中的所有命令在同一 shell 中执行
  • .NOTPARALLEL:禁用并行执行
  • .EXPORT_ALL_VARIABLES:导出所有变量到子进程
  • .SILENT:禁止打印执行的命令
  • .IGNORE:忽略命令错误
# 在同一 shell 中执行多行命令
.ONESHELL:
variables:
	cd $(SRC_DIR)
	for file in *.c; do
		echo "Processing $$file"
	done

7.4 内置目标属性

使用内置目标属性:

# 强制目标总是过时,始终重新构建
.PHONY: FORCE
timestamp: FORCE
	touch $@

# 排列目标优先级
.PRECIOUS: %.o  # 不删除中间文件
.INTERMEDIATE: parser.c  # 标记为中间文件
.NOTINTERMEDIATE: special.o  # 不作为中间文件

8. 实际项目案例分析

8.1 大型 C/C++ 项目 Makefile

Linux 内核构建系统借鉴:

# 基础设置
KBUILD_OUTPUT ?= build
KCONFIG ?= .config

# 加载配置
-include $(KCONFIG)

# 源码目录
SRCDIRS := core drivers net fs

# 收集源文件
SOURCES := $(foreach dir,$(SRCDIRS),$(wildcard $(dir)/*.c))
OBJECTS := $(patsubst %.c,$(KBUILD_OUTPUT)/%.o,$(SOURCES))

# 主要目标
.PHONY: all modules clean

all: prepare modules

prepare:
	@mkdir -p $(KBUILD_OUTPUT)
	@for dir in $(SRCDIRS); do \
		mkdir -p $(KBUILD_OUTPUT)/$$dir; \
	done

modules: $(OBJECTS)
	$(CC) -o $(KBUILD_OUTPUT)/kernel $^

# 编译规则
$(KBUILD_OUTPUT)/%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

# 配置目标
config:
	@echo "CONFIG_DEBUG=y" > $(KCONFIG)
	@echo "CONFIG_EXPERIMENTAL=n" >> $(KCONFIG)

# 清理目标
clean:
	rm -rf $(KBUILD_OUTPUT)

8.2 自动构建工具项目示例

创建自己的构建工具项目:

# 项目信息
PROJECT := mybuild
VERSION := 1.0.0

# 安装目录
PREFIX ?= /usr/local
BINDIR := $(PREFIX)/bin
LIBDIR := $(PREFIX)/lib/$(PROJECT)
CONFDIR := $(PREFIX)/etc/$(PROJECT)

# 源文件
SRCS := $(wildcard src/*.c)
OBJS := $(SRCS:.c=.o)

# 编译和链接
CC := gcc
CFLAGS := -Wall -O2
LDFLAGS := -lm

# 主要目标
all: $(PROJECT)

$(PROJECT): $(OBJS)
	$(CC) -o $@ $^ $(LDFLAGS)

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

# 安装
.PHONY: install uninstall
install: all
	install -d $(DESTDIR)$(BINDIR) $(DESTDIR)$(LIBDIR) $(DESTDIR)$(CONFDIR)
	install -m 755 $(PROJECT) $(DESTDIR)$(BINDIR)
	install -m 644 config/* $(DESTDIR)$(CONFDIR)

uninstall:
	rm -f $(DESTDIR)$(BINDIR)/$(PROJECT)
	rm -rf $(DESTDIR)$(LIBDIR)
	rm -rf $(DESTDIR)$(CONFDIR)

# 打包
dist: clean
	mkdir -p $(PROJECT)-$(VERSION)
	cp -r src config Makefile README.md $(PROJECT)-$(VERSION)
	tar czf $(PROJECT)-$(VERSION).tar.gz $(PROJECT)-$(VERSION)
	rm -rf $(PROJECT)-$(VERSION)

# 清理
clean:
	rm -f $(OBJS) $(PROJECT) *.tar.gz

结语

Makefile 是一个强大的构建工具,尤其在 C/C++ 开发和系统级编程中占据重要地位。虽然现代项目可能使用更特定的构建系统(如 CMake、Bazel 等),但 Make 的基本概念和技术仍然具有广泛的应用价值。掌握 Makefile 编写不仅可以提高项目构建效率,还能加深对软件构建过程的理解。

Prev
npm 与 yarn 使用技巧
Next
Navicat 使用指南