前言
如果您有多个 c、c++ 和其他语言的文件,并且想通过终端命令编译它们,我们该如何编译他们呢?为了解决这类问题,Makefile就出现了。Makefile在编译大型项目的过程中,可以一次性编写大量的源文件以及需要链接器标志。废话少说咱们直接开始今天的正文!
什么是Makefile
Makefile是一种用于简化或组织编译代码的工具,是一组具有变量名称和目标的命令(类似于终端命令),用于创建和删除目标文件的工具。在单个 make 文件中,我们可以创建多个目标来编译和删除对象、二进制文件。您可以使用Makefile多次编译您的项目(程序)。
让我们通过一个例子来理解:
假设我们有 3 个文件main.c(主源文件)、 misc.c(包含函数定义的源文件)、misc.h(包含函数声明)。在这里,我们将声明和定义一个名为myFunc()的函数来打印一些东西——这个函数将分别在misc.c和misc.h中定义和声明。
misc.c
#include#include "misc.h" /*function definition*/ void myFunc(void) { printf("Body of myFunc function. "); }
misc.h
#ifndef MISC_H #define MISC_H /*function declaration.*/ void myFunc(void); #endif
main.c
#include#include "misc.h" int main() { printf("Hello, World. "); myFunc(); fflush(stdout); return 0; }
上面这个场景是非常常见也是最简单的一个多文件系统了,我们想要编译他,并将他们链接在一起该如何做呢?显然仅仅使用gcc等这些简单的编译器是不够的,此时我们就需要用到Makefile了。
下面将内容放在一个名为Makefile的文件中,注意Makefile文件的名字只能是这几个字,而且区分大小写。
Makefile
#make file - this is a comment section all: #target name gcc main.c misc.c -o main
保存名为Makefile。
插入注释,后跟#字符。
all是一个目标名称,在目标名称之后插入:。
gcc是编译器名称,main.c,misc.c源文件名,-o是链接器标志,main是二进制文件名。
“
注意: Makefile必须使用 TAB 而不是空格缩进,否则make会失败。
”
我们写好Makefile后怎么进行编译呢?下面是代码的编译过程:
没有目标名称:
make
带有目标名称:
make all
输出:
sh-4.3$ make gcc main.c misc.c -o main sh-4.3$ ./main Hello, World. Body of myFunc function. sh-4.3$
此时我们就可以看到对应文件夹里已经生成了对应的可执行文件了!这就是Makefile的作用!
为什么会存在 Makefile?
Makefile 用于帮助决定大型程序的哪些部分需要重新编译。在绝大多数情况下,编译 C 或 C++ 文件。其他语言通常有自己的工具,其用途与 Make 相似。当您需要一系列指令来运行取决于哪些文件已更改时,Make 也可以在编译之外使用。本教程将重点介绍 C/C++ 编译用例。
这是您可以使用 Make 构建的示例依赖关系图。如果任何文件的依赖项发生更改,则该文件将被重新编译:
Makefile的语法
一个 Makefile 由一组规则组成。规则通常如下所示:
targets: prerequisites command command command
targets:是文件名,以空格分隔。通常,每条规则只有一个。
command:是通常用于制作目标的一系列步骤。这些需要以制表符开头,而不是空格。
prerequisites:先决条件也是文件名,以空格分隔。这些文件需要在运行目标命令之前存在。这些也称为依赖项
Makefile的精髓
让我们从一个 hello world 示例开始:
hello: echo "Hello, World" echo "This line will always print, because the file hello does not exist."
然后我们将运行make hello,只要hello文件不存在,命令就会运行。如果hello存在,则不会运行任何命令。
重要的是要意识到我说hello的是target和file,那是因为两者是直接联系在一起的。通常,当运行目标时(也就是运行目标的命令时),这些命令将创建一个与目标同名的文件。在这种情况下,hello 目标不会创建hello 文件。
那么我们怎么样才能让程序全部重新生成呢?这就要用到清理目标文件的语句了,下面我们一起看一下如何清理已生成的目标文件。
清理生成的目标文件
我们还可以使用 Makefile 中的变量来概括Makefile。在此示例中,我们使用变量和干净的目标名称编写 Makefile 以删除所有对象(.o 扩展文件)和二进制文件(主文件)。
#make file - this is a comment section CC=gcc #compiler TARGET=main #target file name all: $(CC) main.c misc.c -o $(TARGET) clean: rm $(TARGET)
编译:
make
此时我们想要的目标文件以及.o文件已经出现在对应的文件夹中,那我们如何删除编译出来的文件呢?是不是要使用rm语句一个一个的删除呢?其实大可不必,而且在大的工程中你也不可能一个一个的删除,所以这时候make clean就出现了,他能通过一条语句就删除刚才编译出来的所有文件,下面我们来看一下应该如何操作!
直接在Makefile对应的文件夹先输入一下命令,就会发现刚才生成的文件已经消失了。
make clean
当我们有多个文件时,我们可以在 Makefile 中编写命令来为每个源文件创建目标文件。如果你这样做 只有那些被修改的文件将被编译。
如果我们想要全部重新编译只需要先执行make clean 语句在执行make即可。
Makefile中如何使用变量
在上面的示例中,大多数目标值和先决条件值都是硬编码的,但在实际项目中,这些值被替换为变量和模式。
在 Makefile 中定义变量的最简单方法是使用=运算符。例如,要将命令分配给gcc变量CC:
CC = gcc
这也称为递归扩展变量,它用于如下所示的规则中:
hello: hello.c ${CC} hello.c -o hello
那么实际在终端中执行的语句是下面的:
gcc hello.c -o hello
两者${CC}和$(CC)都是对 gcc的有效引用。但是如果想将一个变量重新分配给它自己,它将导致一个无限循环。让我们验证一下:
CC = gcc CC = ${CC} all: @echo ${CC}
运行make将导致下面的错误:
$ make Makefile:8: *** Recursive variable 'CC' references itself (eventually). Stop.
为了避免这种情况,我们可以使用:=运算符(这也称为简单扩展变量)。我们运行下面的makefile应就不会出现上面的问题了:
CC := gcc CC := ${CC} all: @echo ${CC}
举个例子
下面我们通过一个实际的例子来体会一下上面讲的知识点。以下 makefile 使用了变量、模式和函数编译所有 C 程序。
# Usage: # make # compile all binary # make clean # remove ALL binaries and objects .PHONY = all clean CC = gcc # compiler to use LINKERFLAG = -lm SRCS := $(wildcard *.c) BINS := $(SRCS:%.c=%) all: ${BINS} %: %.o @echo "Checking.." ${CC} ${LINKERFLAG} $< -o $@ %.o: %.c @echo "Creating object.." ${CC} -c $< clean: @echo "Cleaning up..." rm -rvf *.o ${BINS}
#注释整行。
Line.PHONY = all clean定义虚假目标all和clean.
变量LINKERFLAG定义要在gcc中使用的标志。
SRCS := $(wildcard *.c):$(wildcard pattern)是文件名的功能之一。在这种情况下,所有带有.c扩展名的文件都将存储在一个变量SRCS中。
BINS := $(SRCS:%.c=%): 这称为替代参考。在这种情况下,如果SRCS有值'foo.c bar.c',BINS就会有'foo bar'。
Line all: ${BINS}:虚假目标all将值${BINS}作为单独的目标调用。
让我们看一个例子来理解这个规则。假设foo是中的值之一${BINS}。然后%将匹配foo(%可以匹配任何目标名称)。以下是扩展形式的规则:
foo: foo.o @ echo "Checking.." gcc -lm foo.o -o foo
如上所示,%替换为foo。%.o替换为foo.o。%.o被模式化以匹配先决条件,并将%匹配为目标。
下面是对上述makefile的重写,并将它被放置在具有单个文件的foo.c中:
# Usage: # make # compile all binary # make clean # remove ALL binaries and objects .PHONY = all clean CC = gcc # compiler to use LINKERFLAG = -lm SRCS := foo.c BINS := foo all: foo foo: foo.o @echo "Checking.." gcc -lm foo.o -o foo foo.o: foo.c @echo "Creating object.." gcc -c foo.c clean: @echo "Cleaning up..." rm -rvf foo.o foo
这样我们就可以使用一条语句make 完成整个程序的编译了,如果想删除编译中生成的文件,可以使用make clean
多目标Makefile
制作多个目标并且您希望所有目标都运行?做一个all目标。make由于这是列出的第一条规则,如果在没有指定目标的情况下调用它,它将默认运行。
all: one two three one: touch one two: touch two three: touch three clean: rm -f one two three
自动变量和通配符
* 通配符*和%在 Make 中都称为通配符,但它们的含义完全不同。*在您的文件系统中搜索匹配的文件名。
# Print out file information about every .c file print: $(wildcard *.c) ls -la $?
*可以在目标、先决条件或wildcard函数中使用。
*不能在变量定义中直接使用
当*没有匹配到文件时,保持原样(除非在wildcard函数中运行)
% 通配符%确实很有用,但是由于可以使用的情况多种多样,因此有些混乱。
在matching模式下使用时,它匹配字符串中的一个或多个字符。
在replacing模式下使用时,它采用匹配的词干并替换字符串中的词干。
%最常用于规则定义和某些特定功能中。
结语
最后让我们通过一个非常多汁的 Make 示例来结束本文,它适用于中型项目。
这个 makefile 的巧妙之处在于它会自动为您确定依赖关系。您所要做的就是将您的 C/C++ 文件放入该src/文件夹中。
TARGET_EXEC := final_program BUILD_DIR := ./build SRC_DIRS := ./src # Find all the C and C++ files we want to compile # Note the single quotes around the * expressions. Make will incorrectly expand these otherwise. SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s') # String substitution for every C/C++ file. # As an example, hello.cpp turns into ./build/hello.cpp.o OBJS := $(SRCS:%=$(BUILD_DIR)/%.o) # String substitution (suffix version without %). # As an example, ./build/hello.cpp.o turns into ./build/hello.cpp.d DEPS := $(OBJS:.o=.d) # Every folder in ./src will need to be passed to GCC so that it can find header files INC_DIRS := $(shell find $(SRC_DIRS) -type d) # Add a prefix to INC_DIRS. So moduleA would become -ImoduleA. GCC understands this -I flag INC_FLAGS := $(addprefix -I,$(INC_DIRS)) # The -MMD and -MP flags together generate Makefiles for us! # These files will have .d instead of .o as the output. CPPFLAGS := $(INC_FLAGS) -MMD -MP # The final build step. $(BUILD_DIR)/$(TARGET_EXEC): $(OBJS) $(CXX) $(OBJS) -o $@ $(LDFLAGS) # Build step for C source $(BUILD_DIR)/%.c.o: %.c mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Build step for C++ source $(BUILD_DIR)/%.cpp.o: %.cpp mkdir -p $(dir $@) $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ .PHONY: clean clean: rm -r $(BUILD_DIR) # Include the .d makefiles. The - at the front suppresses the errors of missing # Makefiles. Initially, all the .d files will be missing, and we don't want those # errors to show up. -include $(DEPS)
审核编辑:汤梓红
评论
查看更多