Makefile中的高级技巧

269 阅读6分钟

大家好!😄

感谢大家的时间来阅读此文,如果您对以下内容感兴趣,欢迎关注我的公众号《叨叨叨的成长记录》,这里你可以收获以下内容:

  1. 专业的IT内容分享
  2. 前沿LLM技术和论文分享
  3. 个人对行业的思考
  4. 投资理财的经验和笔记

如果您也对这些感兴趣,欢迎在后台留言,大家多多交流!

前序文章


本篇文章,我们会介绍一些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/

在这个例子中,无论文件是否存在,cleaninstall 目标都将被执行。

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/

在这个例子中,cleaninstall 被声明为虚拟目标,确保它们总是被执行,而不管文件的存在与否。

总结

.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

代码解释

  1. 头文件目录:
    • 引入头文件的路径通过 -Iinclude 添加到编译器选项中,确保编译器能找到头文件。
  2. 对象文件路径:
    • 使用 $(patsubst src/%.c, build/%.o, $(SRCS)) 将源文件的路径转换为对象文件的路径。
  3. 构建规则:
    • build/%.o: src/%.c | build 声明表示对象文件依赖于 build 目录,并且从 src 目录中编译。
  4. 清理目标:
    • 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

详细说明

  1. C 代码:
    • C 代码中的 main.c 文件包含一个简单的程序,仅计算 5 和 3 的和,并返回结果。
    • utils.c 定义了 add 函数,并在 utils.h 中进行了声明。
  2. Python 代码:
    • Python 脚本通过 subprocess.run() 调用编译后的 C 程序。它还捕获程序的返回代码。
  3. Makefile:
    • 包含编译和链接 C 程序的指令,还定义了一个 run 目标,运行 Python 脚本。
    • build 目标用于创建输出目录。
    • clean 目标用于清理构建文件。

使用方法

  1. 创建上述目录结构,并将 C 代码、Python 代码和 Makefile 放入相应位置。
  2. 在终端中,将当前目录切换到项目根目录。
  3. 运行 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
  1. 如果只想运行 Python 脚本,可以使用:
make run
  1. 如果需要清理构建产生的文件,运行:
make clean