「软件工程」 makefile

Posted by Dawn-K's Blog on March 4, 2021

Makefile

概述

Makefile 主要是为了解决构建问题。将编译,链接,清理自动化。

配置

make 在 Windows 上体验不太好。 主要是 Windows 的 Powershell 和 cmd 的命令并不兼容,而且相比 Linux, 功能还是弱了一些。 而且一定要注意,命令前面的分隔符是 tab 而不是空格

提前准备了若干文件,将这几个文件和 Makefile 放在同一层文件夹下。

1
2
3
// functions.h
void print_hello();
int factorial(int n);
1
2
3
4
5
6
7
8
9
// function1.cpp
#include "functions.h"
int factorial(int n) {
    if (n != 1) {
        return n * factorial(n - 1);
    } else {
        return 1;
    }
}
1
2
3
4
5
6
// function2.cpp
#include <iostream>

#include "functions.h"

void print_hello() { std::cout << "Hello World" << std::endl; }
1
2
3
4
5
6
7
8
9
10
11
// main.cpp
#include <iostream>

#include "functions.h"

int main() {
    print_hello();
    std::cout << "this is main" << std::endl;
    std::cout << "The factorial of 5 is " << factorial(5) << std::endl;
    return 0;
}

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
all: hello
hello: main.o function1.o function2.o
        g++ main.o function1.o function2.o -o hello
main.o: main.cpp
        g++ -c main.cpp
function1.o: function1.cpp
        g++ -c function1.cpp
function2.o: function2.cpp
        g++ -c function2.cpp

clean:
        rm -rf *.o hello

这个就基本展现了 Makefile 的大体功能。

1
2
<target>:<dependencies> 
    <command>

target 表示 目标,它可以是文件,也可以是单纯的一个符号(比如 allclean ).

dependencies 是若干个(或者没有)依赖目标,也就是当前的目标需要在其依赖构建之后才能构建,同时 make 可以判断依赖与目标的新旧程度,尽量少进行构建。比如如果自从上次构建后,依赖的目标没有再更新过,那么就不再重新构建 target . 若有更新,也仅仅重新构建更新的依赖。

command 就是构建 target 的语句。可视为 OS 的脚本,因 OS 的不同而不同。

每个文件的第一个 target 就是默认的目标,直接执行 make 就会尝试构建,也可以手动指定,如 make main.o . 构建前先检查依赖,如果依赖不满足,那么还需要继续递归,直到构建完成。上文中的 clean 可以声明为 伪指令,也就是

1
2
3
.PHONY: clean
clean:
		rm -rf *.o hello

这样即使存在着一个文件叫 clean, 也不会对 make clean 造成影响,都会执行下面的删除

进阶用法

虽然上述用法节省了大量的体力,但是如果考虑到一个巨大的系统,其 Makefile 的编写和维护也是困难的。

变量

可以指定变量,让修改更方便。比如上文的 g++ -c 等。

1
2
3
4
5
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
#   上文的 g++ -c -Wall main.cpp 可以改写为
# 	$(CC) $(CFLAGS) main.cpp 

另外,观察构建过程,发现很多 target 的构建就是编译其所有依赖。故引入如下三个变量(内置的)

1
2
3
$@ # 指代 all ,即 target
$< # 指代 library.cpp, 即第一个 dependency
$^ # 指代 library.cpp 和 main.cpp,即所有的 dependencies

运用好如上的变量,可以将 makefile 修改如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall

all: hello
hello: main.o function1.o function2.o
		$(CC) $(LFLAGS) $^ -o $@
main.o: main.cpp
		$(CC) $(CFLAGS) $< 
function1.o: function1.cpp
		$(CC) $(CFLAGS) $< 
function2.o: function2.cpp
		$(CC) $(CFLAGS) $< 

clean:
		rm -rf *.o hello

自动检测

make 提供了 wildcard 命令以获取符合特定规则的文件名

比如

1
2
3
4
SOURCE_DIR = .
SOURCE_FILE = ${wildcard $(SOURCE_DIR)/*.cpp}
target:
	@echo $(SOURCE_FILE)

就可以打印出当前目录下所有以 .cpp 结尾的文件

@echo 是表示打印时不显示 echo 此句,而仅仅显示要打印的内容。

make 还有个功能是 patsubst , 是用以创建同名但不同后缀名的文件的。

两个功能可以进行组合,如下的代码可以打印源代码对应的重定向文件。

1
2
3
4
5
6
SOURCE_DIR = .
SOURCE_FILE = ${wildcard $(SOURCE_DIR)/*.cpp}
OBJS = $(patsubst %.cpp, %.o, $(SOURCE_FILE))
target:
	@echo $(SOURCE_FILE)
	@echo $(OBJS)

综上, makefile 可以修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
SOURCE_DIR = .
SOURCE_FILE = ${wildcard $(SOURCE_DIR)/*.cpp}
OBJS = $(patsubst %.cpp, %.o, $(SOURCE_FILE))

all: hello
hello: $(OBJS) 
		$(CC) $(LFLAGS) $^ -o $@
main.o: main.cpp
		$(CC) $(CFLAGS) $< 
function1.o: function1.cpp
		$(CC) $(CFLAGS) $< 
function2.o: function2.cpp
		$(CC) $(CFLAGS) $< 
.PHONY: clean
clean:
		rm -rf *.o hello

Static Pattern Rule

虽然上述的功能已经非常简洁,但是我们还发现,对于 main, function1, function2 其实还有重复的代码,所以我们将其合并,也就是让 make 自动生成对应的 target 而不用一个个手写了。

1
2
3
4
# 将 target:dependencies 修改如下形式
targets: target-pattern: prereq-patterns
# 如下文就是表示,将 所有目标文件都指向其。cpp 结尾的依赖
$(OBJS):%.o:%.cpp

综上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
SOURCE_DIR = .
SOURCE_FILE = ${wildcard $(SOURCE_DIR)/*.cpp}
OBJS = $(patsubst %.cpp, %.o, $(SOURCE_FILE))

all: hello
hello: $(OBJS) 
		$(CC) $(LFLAGS) $^ -o $@
$(OBJS): %.o: %.cpp
		$(CC) $(CFLAGS) $< 
.PHONY: clean
clean:
		rm -rf *.o hello

参考资料

跟我一起写 Makefile Makefile 入门