大家好!😄
感谢大家的时间来阅读此文,如果您对以下内容感兴趣,欢迎关注我的公众号《叨叨叨的成长记录》,这里你可以收获以下内容:
- 专业的IT内容分享
- 前沿LLM技术和论文分享
- 个人对行业的思考
- 投资理财的经验和笔记
如果您也对这些感兴趣,欢迎在后台留言,大家多多交流!
前序文章
本篇文章,我们会介绍一些Makefile中的几个小技巧,让你更加高效的掌握Makefile文件的设计
强制执行目标命令
在 Makefile 中,.PHONY 是一个特殊的目标,用于声明某些目标是“虚拟”的,也就是说它们并不对应于实际存在的文件。这是 .PHONY 的主要作用和意义:
1. 防止文件名冲突
如果你的 Makefile 中有一个目标的名字与目录下某个文件同名,例如:clean:
rm -f *.o my_program
如果当前目录中有一个名为 clean 的文件,Make 在执行时会认为 clean 目标已经“最新”,因此不会执行目标的命令。这会导致你期望的清理操作无法进行。而通过将 clean 声明为虚拟目标:
.PHONY: clean
clean 就会总是被执行,避免与文件名的冲突。
2. 提高 Makefile 的灵活性
使用 .PHONY 声明虚拟目标,可以让你在目标的执行中更加灵活。例如,你可以创建不依赖于文件的命令,这些命令始终需要被执行:
.PHONY: all clean install
all: my_program
clean:
rm -f *.o my_program
install:
cp my_program /usr/local/bin/
在这个例子中,无论文件是否存在,clean 和 install 目标都将被执行。
3. 表达意图
使用 .PHONY 还能增加代码的可读性,显示出这些目标并不生成文件,而是执行某项任务,使得其他开发者更容易理解你的意图。
示例
这是一个完整的例子,展示了 .PHONY 的使用:
.PHONY: all clean install
all: my_program
my_program: main.o utils.o
gcc -o my_program main.o utils.o
clean:
rm -f *.o my_program
install:
cp my_program /usr/local/bin/
在这个例子中,clean 和 install 被声明为虚拟目标,确保它们总是被执行,而不管文件的存在与否。
总结
.PHONY 是用来声明不产生文件的目标,在避免与同名文件冲突、提高灵活性以及增强可读性等方面发挥着重要作用。合理使用 .PHONY 可以让 Makefile 的行为更加可预测,有助于简化构建过程。
条件参数控制
将 C 源文件(.c)和头文件(.h)分别放在不同的目录中,并将编译输出内容保存到 build 目录中,可以使项目结构更加清晰。以下是实现这一目标的 Makefile 及相应的目录结构。
目录结构
.
├── Makefile
├── build
│ ├── main.o
│ ├── my_program
│ └── utils.o
├── include
│ └── utils.h
└── src
├── main.c
└── utils.c
C 文件和头文件内容
src/main.c
#include <stdio.h>
#include "utils.h"
int main() {
printf("Hello, World!\n");
#ifdef VERBOSE
printf("Verbose mode is enabled.\n");
#endif
int result = add(5, 3);
printf("The result of 5 + 3 is: %d\n", result);
return 0;
}
src/utils.c
#include "utils.h"
int add(int a, int b) {
return a + b;
}
include/utils.h
#ifndef UTILS_H
#define UTILS_H
int add(int a, int b);
#endif // UTILS_H
优化后的 Makefile
# Makefile
# Compiler and directories
CC = gcc
CFLAGS = -Wall -O2 -Iinclude # Add include directory to search path
SRCS = src/main.c src/utils.c
OBJS = $(patsubst src/%.c, build/%.o, $(SRCS)) # Output object files to build directory
TARGET = build/my_program # Output binary to build directory
# Conditional flags
ifeq ($(DEBUG), 1)
CFLAGS += -g
VERBOSE = 1
endif
ifeq ($(VERBOSE), 1)
VFLAGS = -DVERBOSE
else
VFLAGS =
endif
all: $(TARGET)
$(TARGET): $(OBJS)
@echo "Linking..."
$(CC) -o $@ $^ $(VFLAGS)
build/%.o: src/%.c | build # Rule to specify the output directory for object files
@echo "Compiling $< with flags: $(CFLAGS)"
$(CC) $(CFLAGS) $(VFLAGS) -c $< -o $@
build: # Create the build directory if it doesn't exist
mkdir -p build
clean:
rm -rf build # Remove the entire build directory
.PHONY: all clean build
代码解释
- 头文件目录:
- 引入头文件的路径通过
-Iinclude添加到编译器选项中,确保编译器能找到头文件。
- 引入头文件的路径通过
- 对象文件路径:
- 使用
$(patsubst src/%.c, build/%.o, $(SRCS))将源文件的路径转换为对象文件的路径。
- 使用
- 构建规则:
build/%.o: src/%.c | build声明表示对象文件依赖于build目录,并且从src目录中编译。
- 清理目标:
clean目标会删除整个build目录,而不仅仅是单个生成的文件。
依赖其它目标
允许一个目标依赖于另一个目标的输出,而不仅仅是文件。.PHONY: all clean
all: build/my_program
build/my_program: build/first_target build/second_target
@echo "Linking my_program..."
build/first_target:
@echo "Building first target..."
build/second_target:
@echo "Building second target..."
在执行 make命令后,我们可以得到如下的输出
Building first target...
Building second target...
Linking my_program...
执行shell命令
在 Makefile 中,你可以通过定义目标来执行 shell 命令。这允许你在构建过程中运行各种 shell 命令。你可以使用 $(shell ...) 或者在规则的命令部分直接写上 shell 命令。
方法 1: 使用 $(shell ...)
$(shell ...) 用于在变量定义中执行 shell 命令。例如,你可以获取当前的 git 版本号,或动态生成文件列表。
以下是一个示例:
# Makefile
# 获取当前 git 版本
VERSION = $(shell git describe --tags)
# 目标
all:
@echo "Current version is $(VERSION)"
方法 2: 在规则中直接执行
你可以在 Makefile 的命令部分直接写 shell 命令。在这种情况下,你不需要使用 $(shell ...),你可以直接在目标的命令部分写具体的 shell 命令。
以下是一个示例:
# Makefile
# 目标
run:
@echo "Running my command..."
./my_script.sh # 执行一个 shell 脚本
构建多语言工程
当然可以!让我们创建一个项目,其中 Python 脚本调用一个 C 程序。我们将使用 Makefile 来管理构建过程。以下是详细的步骤和相关的代码示例。项目结构
./
├── Makefile
├── build/
├── src/
│ ├── main.c
│ └── utils.c
└── scripts/
└── run.py
C 代码示例
src/main.c
#include <stdio.h>
#include "utils.h"
int main() {
printf("C program running...\n");
int result = add(5, 3);
printf("The result of 5 + 3 is: %d\n", result);
return result;
}
src/utils.c
#include "utils.h"
int add(int a, int b) {
return a + b;
}
include/utils.h
#ifndef UTILS_H
#define UTILS_H
int add(int a, int b);
#endif // UTILS_H
Python 代码示例
scripts/run.py
import subprocess
def main():
try:
# 调用C程序
result = subprocess.run(["./build/my_program"], check=True)
print(f"C program exited with code: {result.returncode}")
except subprocess.CalledProcessError as e:
print(f"The C program failed with exit code: {e.returncode}")
if __name__ == "__main__":
main()
Makefile 示例
下面是 Makefile 来构建上述项目:# Makefile
# Compiler
CC = gcc
CFLAGS = -Wall -O2 -Iinclude
# Source files and target executable
C_SRCS = src/main.c src/utils.c
C_OBJS = $(patsubst src/%.c, build/%.o, $(C_SRCS))
TARGET = build/my_program
# Create build directory
build:
mkdir -p build
# Compile C files
build/%.o: src/%.c | build
@echo "Compiling C source: $<"
$(CC) $(CFLAGS) -c $< -o $@
# Link the C program
$(TARGET): $(C_OBJS)
@echo "Linking..."
$(CC) -o $@ $^
# Run the Python script
run: $(TARGET)
@echo "Running Python script..."
python3 scripts/run.py
# Clean up build directory
clean:
rm -rf build
.PHONY: all clean build run
# Default target
all: $(TARGET) run
详细说明
- C 代码:
- C 代码中的
main.c文件包含一个简单的程序,仅计算 5 和 3 的和,并返回结果。 utils.c定义了add函数,并在utils.h中进行了声明。
- C 代码中的
- Python 代码:
- Python 脚本通过
subprocess.run()调用编译后的 C 程序。它还捕获程序的返回代码。
- Python 脚本通过
- Makefile:
- 包含编译和链接 C 程序的指令,还定义了一个
run目标,运行 Python 脚本。 build目标用于创建输出目录。clean目标用于清理构建文件。
- 包含编译和链接 C 程序的指令,还定义了一个
使用方法
- 创建上述目录结构,并将 C 代码、Python 代码和 Makefile 放入相应位置。
- 在终端中,将当前目录切换到项目根目录。
- 运行
make命令来编译 C 程序并运行 Python 脚本:
make all
这将输出如下内容(具体输出可能会有所不同,具体取决于编译环境):
mkdir -p build
Compiling C source: src/main.c
gcc -Wall -O2 -Iinclude -c src/main.c -o build/main.o
Compiling C source: src/utils.c
gcc -Wall -O2 -Iinclude -c src/utils.c -o build/utils.o
Linking...
gcc -o build/my_program build/main.o build/utils.o
Running Python script...
python3 scripts/run.py
Hello, World!
The result of 5 + 3 is: 8
C program exited with code: 0
- 如果只想运行 Python 脚本,可以使用:
make run
- 如果需要清理构建产生的文件,运行:
make clean