makefile编写旋风实践

今天把make和写makefile的一些非常常用的方法和技巧整理了一下,算是一个旋风式入门了吧,自己的一些小项目按这个方法写makefile是很容易的,make也算是一个学完可以一劳永逸的好工具了。

旋风入门

基础的makefile例子:

main.c是主程序文件,里面的功能是由stack.c和maze.c两个程序中的函数组成,

main.h中定义了几个常量和一个类型,main.c,stack.c,maze.c都要用到。

1
2
3
4
5
6
7
8
9
10
11
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
main.o: main.c main.h stack.h maze.h
gcc -c main.c
stack.o: stack.c stack.h main.h
gcc -c stack.c
maze.o: maze.c maze.h main.h
gcc -c maze.c

每条格式如下:

1
2
3
target ... :prerequisites(先决条件) ...
command1
command2

目标和先决条件的关系是:欲更新目标,必须先更新它的所有条件;所有条件中只要有一个条件被更新了,目标也必须随之被更新。

注意:命令列表中的每条命令必须以一个Tab开头,注意不能用空格代替这个Tab。对于Makefile中每个以Tab开头的命令,make会启动一个Shell进程去执行它。

make 会自动选择那些受影响的源文件重新编译,不受影响的源文件不会重新编译。

clean例子:

1
2
3
4
clean:
@echo "cleanning begin"
-rm main *.o
@echo "clean completed"

使用:

1
make clean

附注:

如果文件夹下存在clean的文件,则会扰乱使用make clean,想不被文件名所扰乱make命令的使用,可以将clean声明成一个伪目标:

1
.PHONY: clean

这样,完整clean例子:

1
2
3
4
5
6
clean:
@echo "cleanning begin"
-rm main *.o
@echo "clean completed"
.PHONY: clean

不会被同名文件夹所干扰。

PS:clean用来清除目标是一个约定俗称的名字,在所有软件项目中makefile类似的约定俗称的名字还有:

  • all, 执行主要编译工作,通常作为缺省目标
  • install,执行编译后的安装工作,把可执行文件,配置文件,文档等分别复制到不同安装目录。

升级提高

Makefile有很多灵活的写法,可以写得更简洁,同时减少人为出错的可能。

几种常用写法认识

前提知识:

  • 1
    2
    main.o: main.c main.h stack.h maze.h
    gcc -c main.c

    可以拆成两句:

    1
    2
    3
    4
    main.o: main.h stack.h maze.h
    main.o: main.c main.h stack.h maze.h
    gcc -c main.c

    规定:一个目标拆成多条,但只能有其中一条允许带命令列表。

我们可以将前面的简单例子写成以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
main.o: main.h stack.h maze.h
stack.o: stack.h main.h
maze.o: maze.h main.h
main.o: main.c
gcc -c main.c
stack.o: stack.c
gcc -c stack.c
maze.o: maze.c
gcc -c maze.c
clean:
@echo "cleanning begin"
-rm main *.o
@echo "clean completed"
.PHONY: clean

并且我们可以将提出来的几条规则删去,写成以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
main.o: main.h stack.h maze.h
stack.o: stack.h main.h
maze.o: maze.h main.h
clean:
@echo "cleanning begin"
-rm main *.o
@echo "clean completed"
.PHONY: clean

为什么main.o,stack.o,maze.o三个目标编译命令都删去了,怎么还能编译呢?

因为利用了make的机制,make会尝试在内建的隐含规则数据库中查找合适的规则。(具体隐含规则不展开聊了,可以通过make -p查看,个人建议,还是能省的不要省,要不错了都不知道为什么)

这个例子比较常用,也很方便写,所以举出来,建议使用。

另外一种写法

之前makefile都是以目标为中心写依赖逻辑的,可以换种写法,以条件为中心,makefile只要写明白依赖关系既可,所以上面的例子还可以这么写:

1
2
3
4
5
6
7
8
9
10
11
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
main.o stack.o maze.o: main.h
main.o stack.o: stack.h
main.o maze.o: maze.h
clean:
-rm main *.o
.PHONY: clean

加入变量

一个简单例子了解变量:

1
2
3
4
all:
@echo $(foo)
foo = $(bar)
bar = Huh?

执行make命令会打印出Huh?

变量在实际运用中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Compiler is G++
CXX = g++
LIBDIR = .
SOCKETLIBDIR = $(LIBDIR)/SocketLib
BASICLIBDIR = $(LIBDIR)/BasicLib
# flags
CFLAGS = -I$(LIBDIR)
link: $(wildcard *.cpp)
$(CXX) $(CFLAGS) $(SOCKETLIBDIR)/*.cpp -c;
$(CXX) $(CFLAGS) $(BASICLIBDIR)/*.cpp -c;
$(CXX) $(CFLAGS) -o chatroomserver *.cpp *.o
clean:
@echo "cleanning begin"
-rm chatroomserver *.o
@echo "cleanning end"
.PHONY: clean

CFLAGES常用来定义一些编译选项,例如-o,-g等。

CPPFLAGES 常用来定义一些预处理选项,如-D,-I。

1
2
>g++ -I.
>

>

用来标志包含目录位置,非常有用!

关于预处理中-D:

源代码里面如果这样是定义的:
#ifdef MACRONAME
//可选代码
#endif

那在makefile里面
gcc -D MACRONAME=MACRODEF
或者
gcc -D MACRONAME

这样就定义了预处理宏,编译的时候可选代码就会被编译进去了。

对于GCC编译器,有如下选项:
​ -D macro=string,等价于在头文件中定义:#define macro string。例如:-D TRUE=true,等价于:#define TRUE true
​ -D macro,等价于在头文件中定义:#define macro 1,实际上也达到了定义:#define macro的目的。例如:-D LINUX,等价于:#define LINUX 1(与#define LINUX作用类似)。
​ –define-macro macro=string与-D macro=string作用相同。

自动变量

常用的自动变量有:

  • $@:表示规则中的目标
  • $<:表示规则中第一个条件
  • $?:表示规则中所有比目标新的条件,组成一个列表,以空格分隔。
  • $^:表示规则中的所有条件,组成一个列表,以空格分隔,如果这个列表中有重复的项则将其消除。

例如:

1
2
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main

可以改写成:

1
2
main: main.o stack.o maze.o
gcc $^ -o $@

自动处理头文件的依赖关系

现在我们的makefile可以写成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
all:main
main: main.o stack.o maze.o
gcc $^ -o $@
main.o stack.o maze.o: main.h
main.o stack.o: stack.h
main.o maze.o: maze.h
clean:
-rm main *.o
.PHONY: clean

按照惯例,用all做缺省目标。

现在唯一的麻烦就是,写目标依赖关系的时候还要查看源代码,去一个个分析它们的依赖于哪些头文件,这个过程很容易出错,一是因为有的头文件包含在另一个头文件中,在写规则时很容易遗漏,二是如果以后修改源代码改变了依赖关系,很可能忘记修改Makefile规则。

因为我们知道源代码中包含了目标文件和头文件之间的依赖关系,这种依赖关系是以#include形式描述的,我们只要想办法把源代码中的依赖关系信息抽取出来自动转换成makefile中的规则即可。

第一步,用gcc的-M选项自动分析目标文件和源文件的依赖关系,输出:

(以一个工程的例子为输出结果,主要看实现过程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ubuntu-ysbbs:~/yscode/Chatroom$ g++ -M -I . ./chatroomserver.cpp
chatroomserver.o: chatroomserver.cpp /usr/include/stdc-predef.h \
SocketLib/SocketLib.h SocketLib/SocketLibTypes.h \
/usr/include/x86_64-linux-gnu/sys/types.h /usr/include/features.h \
/usr/include/x86_64-linux-gnu/sys/cdefs.h \
/usr/include/x86_64-linux-gnu/bits/wordsize.h \
/usr/include/x86_64-linux-gnu/gnu/stubs.h \
/usr/include/x86_64-linux-gnu/gnu/stubs-64.h \
/usr/include/x86_64-linux-gnu/bits/types.h \
/usr/include/x86_64-linux-gnu/bits/typesizes.h /usr/include/time.h \
/usr/lib/gcc/x86_64-linux-gnu/5/include/stddef.h /usr/include/endian.h \
/usr/include/x86_64-linux-gnu/bits/endian.h \
/usr/include/x86_64-linux-gnu/bits/byteswap.h \
/usr/include/x86_64-linux-gnu/bits/byteswap-16.h \
/usr/include/x86_64-linux-gnu/sys/select.h \
/usr/include/x86_64-linux-gnu/bits/select.h \
/usr/include/x86_64-linux-gnu/bits/sigset.h \
/usr/include/x86_64-linux-gnu/bits/time.h \
/usr/include/x86_64-linux-gnu/sys/sysmacros.h \
/usr/include/x86_64-linux-gnu/bits/pthreadtypes.h \
/usr/include/x86_64-linux-gnu/sys/socket.h \
/usr/include/x86_64-linux-gnu/sys/uio.h \
/usr/include/x86_64-linux-gnu/bits/uio.h \
/usr/include/x86_64-linux-gnu/bits/socket.h \
/usr/include/x86_64-linux-gnu/bits/socket_type.h \
/usr/include/x86_64-linux-gnu/bits/sockaddr.h \
/usr/include/x86_64-linux-gnu/asm/socket.h \
/usr/include/asm-generic/socket.h \
/usr/include/x86_64-linux-gnu/asm/sockios.h \
...

输出不仅包含我们编写的文件,还包含了用到的系统头文件一大堆,系统头文件不需要和我们程序一起维护,我们可以用-MM选项,输出只包含我们自己编写的头文件。

1
2
3
4
5
6
7
8
9
ubuntu-ysbbs:~/yscode/Chatroom$ g++ -MM -I . ./chatroomserver.cpp
chatroomserver.o: chatroomserver.cpp SocketLib/SocketLib.h \
SocketLib/SocketLibTypes.h SocketLib/SocketLibSystem.h \
SocketLib/SocketLibSocket.h BasicLib/BasicLib.h BasicLib/BasicLibTypes.h \
BasicLib/BasicLibTime.h BasicLib/BasicLibString.h BasicLib/minilogger.h \
SocketLib/SocketLibErrors.h SocketLib/Connection.h \
SocketLib/ConnectionManager.h SocketLib/SocketSet.h \
SocketLib/ListeningManager.h SocketLib/ConnectionHandler.h \
SocketLib/Telnet.h SCUserDB.h SCLogon.h SCChat.h

这样就可以方便知道文件的依赖关系而不用去一个个代码查看了。

其它make常用命令

如果某次编译想加入调试选项而不是每次都要加入-g,可以在命令行中定义CFLAGS变量而不需要修改makefile:

1
$make CFLAGS=-g

本文标题:makefile编写旋风实践

文章作者:Yang Shuai

发布时间:2018年06月21日 - 22:06

最后更新:2019年04月24日 - 10:04

原始链接:https://ysbbswork.github.io/2018/06/21/makefile编写旋风实践/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!