想学Linux你就必须要学会Makefile

嵌入式技术

1362人已加入

描述

前言

如果您有多个 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 构建的示例依赖关系图。如果任何文件的依赖项发生更改,则该文件将被重新编译:
C++

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)

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分