一、Makefile和Make的作用

什么是makefile?或许很多使用IDE(集成开发环境)开发者可能都不知道这个东西,因为IDE都为开发者做了这个工作。但是在Unix下的软件编译,你就不能不自己写makefile了,尽管Linux桌面环境下也有很多IDE,但是在嵌入式开发中经常需要使用中终端进行开发,不能直接使用IDE;同时,编写makefile,可以对项目中模块的依赖关系有更清晰的认识,有利于提高自己完成大型工程的能力。

1、引入

从一个简单例子开始,首先我们需要准备5个文件。这5个文件分别代表主程序和工具函数的实现与声明。

  1. #ifndef HELLOMAKE_H_
  2. #define HELLOMAKE_H_
  3. void myPrintHelloMake();
  4. #endif /* HELLOMAKE_H_ */
  1. #include <stdio.h>
  2. #include "hellomake.h"
  3. void myPrintHelloMake()
  4. {
  5. printf("Hello, makefiles!\n");
  6. return;
  7. }
#ifndef SRC_TOOLKITS_H_
#define SRC_TOOLKITS_H_

void PrintName(char *name);

#endif /*
#include <stdio.h>
#include "toolkits.h"

void PrintName(char *name)
{
    printf("Hello, %s!\n", name);
    return;
}
#include <stdio.h>
#include "toolkits.h"

void PrintName(char *name)
{
    printf("Hello, %s!\n", name);
    return;
}

image.png
有了这三个文件,我们可以用下面的指令来编译

gcc -I./ main.c hellomake.c toolkits.c -o learn_make

这条指令编译两个 c 文件,并且生成可执行文件 hellomake。-I.参数告诉工 gcc 从当前目录寻找头文件 hellomake.h。我们在测试 / 修改 / 调试代码的时候一般可以在 terminal 上用上下键切换编译指令,这样就不用每编译一次都要敲一次编译指令了。
但是不幸的是,这种方法有两个缺点:第一:如果你不小心丢掉了编译指令(比如不小心关掉了 terminal)或者换了一个电脑,你就得重新敲一遍编译指令;第二:就算你只修改了某一个 c 文件,你也必须把所有的源文件全部重新编译一次,这个是非常耗时间的,也是完全没有必要的。

2、makefile的作用

这两个问题都可以通过写一个 makefile 来解决。
makefile中包含了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile指定了目标文件一系列的依赖关系,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
makefile带来的好处就是—“自动编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。

3、make的作用

makefile中指定了目标文件的依赖文件目标文件生成的指令,那么谁来执行这些依赖规则和指令呢?那就是make了。
make是一个工具,是一个解释makefile中指令和规则的工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译规则。

总之, make是一种工具,用于控制从程序的源文件生成程序的可执行文件和其他非源文件。

GNU Make is a tool which controls the generation of executables and other non-source files of a program from the program’s source files. Make gets its knowledge of how to build your program from a file called the makefile, which lists each of the non-source files and how to compute it from other files. When you write a program, you should write a makefile for it, so that it is possible to use Make to build and install the program. The make utility automatically determines which pieces of a large program need to berecompiled, and issues(调用) commands to recompile them.

二、简单makefile编写

给第一部分中的5个文件,编写makefile。本节1-6部分为递进关系,由简单到抽象。

1、最简单的makefile

lean_make: main.c hellomake.c toolkits.c                    # 指定依赖
    # 默认输出可执行文件的名称为a.out,使用-o参数指定输出文件名称:learn_make
    # 命令前面是tab,不是空格
    gcc -I./ main.c hellomake.c toolkits.c -o learn_make

把这两行写入到一个名为 Makefile 或 makefile 的文件中,然后在 terminal 输入make,系统就会按照 makefile 文件中定义的规则编译你的代码。
image.png
注make命令不带参数的话,会默认执行 makefile 文件中的第一条规则。此外,目标文件依赖的文件列表放在“:”之后的第一行,make 就知道如果其中的任何文件发生更改,就需要执行 gcc编译命令。这样就解决了第一个问题,不需要在 terminal 中上下翻找最后一个编译命令了。但是这样写,make 仍然不能有效的只编译更改源文件。
有一点需要特别注意的是,在 gcc 命令之前有一个 tab 字符。事实上,在任何指令之前,都必须加一个 tab 字符,这是 make 的要求,不加 tab。

根据makefile内容可以看出,编写makefile是需要gcc常用参数 shell常用指令的使用基础的。连接如下
GCC 参数详解
Linux基本操作

2、仅编译更改的文件(make工作原理)

lean_make: main.o hellomake.o toolkits.o                #指定依赖关系
    # 命令前面是tab,不是空格
    # 默认输出可执行文件的名称为a.out,使用-o参数指定输出文件名称:learn_make
    gcc  main.o hellomake.o toolkits.o -o learn_make    

main.o: main.c hellomake.h toolkits.h
    # -c 只允许执行到汇编步骤,不允许链接。
    gcc -I./ -c main.c -o main.o
hellomake.o: hellomake.c toolkits.h
    gcc -I./ -c hellomake.c -o hellomake.o 
toolkits.o: toolkits.c toolkits.h
    gcc -I./ -c toolkits.c -o toolkits.o

下图可以看到编译的顺序,同时可以看到中间生成的未链接的二进制代码(*.o文件)。
image.png

3、添加编译时的参数

添加编译参数和清理命令

lean_make: main.o hellomake.o toolkits.o
    gcc  main.o hellomake.o toolkits.o -o learn_make    

main.o: main.c hellomake.h toolkits.h
    # -Wall可以看到所有的警告
    # -g 包含调试内容    
    gcc -I./ -Wall -g -c main.c -o main.o
hellomake.o: hellomake.c toolkits.h
    gcc -I./ -Wall -g -c hellomake.c -o hellomake.o 
toolkits.o: toolkits.c toolkits.h
    gcc -I./ -Wall -g -c toolkits.c -o toolkits.o

#清理命令    
clean:
    rm -rf *.o lean_make

image.png

4、利用变量简化makefile

# OBJS        代表    依赖文件
# CC        代表    gcc指令
# CFLAGS    代表    编译选项
# INC        代表    头文件目录
# L
# LIBS        代表    依赖库    
TARGET=learn_make
OBJS=main.o hellomake.o toolkits.o
LIBS=
INC=-I./
CFLAGS=-Wall -g
RM= rm -rf
CC=gcc

$(TARGET): $(OBJS)
    $(CC) $(OBJS) -o $(TARGET)        
main.o: main.c hellomake.h toolkits.h
    $(CC) $(INC) $(CFLAGS) -c main.c -o main.o
hellomake.o: hellomake.c toolkits.h
    $(CC) $(INC) $(CFLAGS) -c hellomake.c -o hellomake.o 
toolkits.o: toolkits.c toolkits.h
    $(CC) $(INC) $(CFLAGS) -c toolkits.c -o toolkits.o

clean:
    $(RM) *.o $(TARGET)

5、利用默认变简化makefile

# $<    代表    代表上面规则中的第一个依赖文件
# $^    代表    代表上面规则中的所有依赖文件
# $@    代表    代表上面规则中的目标文件
TARGET=learn_make
OBJS=main.o hellomake.o toolkits.o
LIBS=
INC=-I./
CFLAGS=-Wall -g
RM= rm -rf
CC=gcc

lean_make: $(OBJS)
    $(CC) $^ -o $@        
main.o: main.c hellomake.h toolkits.h
    $(CC) $(INC) $(CFLAGS) -c $< -o $@
hellomake.o: hellomake.c toolkits.h
    $(CC) $(INC) $(CFLAGS) -c $< -o $@ 
toolkits.o: toolkits.c toolkits.h
    $(CC) $(INC) $(CFLAGS) -c $< -o $@

clean:
    $(RM) *.o $(TARGET)

6、利用通配符简化makefile

# %.o:%.c
#     $(CC) $^ $(CFLAGS) -o $@
# 百分号是一个通配符
TARGET=learn_make
OBJS=main.o hellomake.o toolkits.o
LIBS=
INC=-I./
CFLAGS=-Wall -g
RM= rm -rf
CC=gcc

$(TARGET): $(OBJS)
    $(CC) $^ -o $@        
%.o: %.c 
    $(CC) $(INC) $(CFLAGS) -c $< -o $@

clean:
    $(RM) *.o $(TARGET)

7、make工作原理

Makefile和Make总结 - 图5
make工作原理

  • 首先make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
  • 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“lean_make”这个文件,并把这个文件作为最终的目标文件。
  • 如果lean_make文件不存在,或是lean_make所依赖的后面的 .o 文件的文件修改时间要比lean_make这个文件新,那么,他就会执行后面所定义的命令来生成edit这个文件。
  • 如果lean_make所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
  • 当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件声明make的终极目标任务,也就是执行文件lean_make了

    三、makefile文件详解

    参考资料
    https://www.gnu.org/software/make/manual/make.html
    https://blog.csdn.net/weixin_38391755/article/details/80380786

    1、makefile的内容

    Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义文件指示和注释。

  • 显式规则:显式规则说明了,如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出要生成的目标文件目标文件的依赖文件目标生成的指令

  • 隐晦规则:由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。
  • 变量的定义:在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
  • 文件指示:其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
  • 注释:Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用“#”字符,这个就像C/C++中的“//”一样。如果你要在你的Makefile中使用“#”字符,可以用反斜框进行转义,如:“#”。

2、

参考资料

Makefile傻瓜教程-菜鸟笔记