一份快速入门的 Makefile 教程

一份快速入门的 Makefile 教程

最近被正在初学 Linux 的朋友问起 Makefile 的事情。有朋友想知道:

  • 什么是 Makefile?能做些什么呢?
  • Makefile 该怎么写?如何自定义编译规则呢?
  • 我想创建一个 C 项目,我把文件保存为 makefile.c,为什么无法编译呢?

我作为一个苦 Bee 大学牲 + 只会无脑写 Python 的数据分析人士,被问起这些问题确实比较尴尬。不过我还是决定斗胆来写一份教程吧 ~

关于 Makefile,你应该知道的一些事情

什么是 Makefile?

Makefile 是一种定义了软件项目中文件依赖关系和构建规则的文本文件。通过 Makefile,我们可以使用 make 工具自动执行编译、链接等操作,从而简化软件项目的构建过程。

说白了,Makefile 就是要告诉 make 命令:我要什么,怎么编译。 具体的实施过程 make 命令会为你全盘接手,让你以逸待劳,坐享其成。

实际上很多 Winodws 的程序员都不知道这个东西,因为那些 Windows 上常见的 IDE 都为你做了这个工作。

但是 IDE 代理完成工作,往往意味着更低的定制性和更有限的自由度。如果你想对自己的项目编译过程进行一定程度上的自定义,就应当对 Makefile 的工作原理有所了解。

Makefile 能做什么?

Makefile 最初被发明出来,是为了解决 C 语言的编译的难题。 但在现在,Makefile 的功能早已不在局限于编译 C 项目。它能作为一般 shell 脚本的一种扩充,帮你管理一系列的命令规则。

最近大家都开始学习了如何在 Linux 上编译 C 语言的项目。这令我可以很容易地 拿 C 语言项目作为例子。当没有 Makefile 的情况下,编译 C 项目可能会变得非常冗长和折磨人。假设我们有一个包含多个源文件和头文件的项目,编译过程中需要链接外部库,那么编译命令可能会变得非常复杂。

首先,一个 C 语言项目通常最起码包含如下的文件夹:

  • src 用于存放项目的源代码
  • obj 用于存放项目的链接文件
  • bin 用于存放最终输出的二进制文件

在这种情况下,以下是一个示例,假设我们有两个源文件 main.chelper.c,以及对应的头文件 helper.h。同时,我们需要链接名为 libexample.a 的外部库。在没有 Makefile 的情况下,我们可能需要执行以下一系列冗长的命令来完成编译:

gcc -c ./src/main.c -o ./obj/main.o
gcc -c ./src/helper.c -o ./obj/helper.o
gcc ./obj/main.o ./obj/helper.o -L. -lexample -o ./bin/myprogram

在这个示例中,我们首先分别编译每个源文件生成对应的目标文件(.o 文件),然后再将它们链接在一起并指定外部库的位置和名称。如果项目规模更大、依赖关系更复杂,那么这些命令将会变得更加冗长和难以管理。

而通过 Makefile,你只需要简单地定义编译规则和依赖关系,然后执行 make 命令即可自动完成整个编译过程。这种自动化构建过程极大地简化了开发者的工作,并且减少了出错的可能性。

接下来我想举另一个更加实用的例子,也是我每天都在用的例子:

你可以用 Makefile 来写论文。

是的,你没听错。就是写论文。

下图展示了一篇日常作业论文,是基于 Makefile 定义的规则和 pandoc 文档转换工具生成的。

image

我在书写这篇论文的时候,只需要简单地写下其对应的 Markdown 文本,pandoc 就会为我自动管理 Word 文档的排版格式。

相较于繁杂的 Word 排版,Markdown 文本只有简单的 9 个用于注明文字排版格式的标识符。比如标题可以简单地用一个 # 来表示;数学公式可以用 $$ 包裹起来,然后用 LaTeX \LaTeX LATEX 代码书写。pandoc 软件会将其自动转化为公式对象并插入 Word 文档。这种简便性大大减少了我花费在作业上的时间。

image

但是,假如我没有 Makefile 文件,想要将这份 Markdown 文档转换为 Word 格式的作业论文,我需要输入如下命令:

pandoc ./main.md\
-f markdown -t docx\
-o ./release/output.docx\
--resource-path=./image\
-s\
--toc\
--mathjax\
--highlight-style=pygments\
--reference-doc=./lib/\
--cite --bibliography=./lib/Refe.bib

这原本是一行命令,但是因为实在是太长了,所以不得不换行处理。显然,这是很麻烦很折磨的。但是我们可以将上述的编译规则定义在 Makefile 里面,然后一次性管理所有的编译规则。这样一来,我只需要输入命令:

make

就可以直接拿到我想要的 Word 文档。

Makefile 是可以复用的。下回当我想要再写另一篇科学论文的时候,直接把上回的 Makefile 复制黏贴过来就好了。

Makefile 怎么写?

说了这么多,大家一定想知道 Makefile 到底应该怎么写。那还能怎么写,这种简单又繁琐的工作当然是让 ChatGPT 帮你写了!一句话下单 30 秒就给你写好了…… 实际上 Makefile 的教程在网上已经很多了。你可以很快搜索到教程。不过我在这里还是简单地写一份教程比较好。

Makefile 与 make

想要理解 Makefile 的意义,就不得不提到 make 命令。

make 命令是一个用于自动化构建程序的工具,它通过读取一个名为 Makefile 的文件来执行一系列指定的操作。也就是说,如果你发现一个项目文件夹(可能是你爸微信上转给你的文件夹压缩包,或者你从 Github 上拉取下来的,随便)下面有名为 Makefile 或者 makefile 的文件,那么你就可以直接使用命令:

make

来编译这个项目。

一个 C 语言项目

在开始写 Makefile 之前,我们首先要新建一个项目文件夹。

touch ./myproject

这一步很重要:通过创建项目文件夹,可以把写代码的工作空间和外部乱七八糟的文件隔离开来,方便代码的维护管理。比如你外公突然让你把昨天的代码发给他,你手忙脚乱的打包代码的说明书和源文件就很麻烦;如果有一个独立的文件夹,就可以直接把整个文件夹压缩了发给人家,省时省力。

接下来,我们进入文件夹,创建项目源代码文件夹 src、链接文件文件夹 obj 和最终生成的二进制执行文件文件夹 bin

# 创建项目文件夹
cd ./myproject

# 创建子文件夹
mkdir ./src
mkdir ./obj
mkdir ./bin

接下来,首先在项目根目录下创建一个名为 Makefile 的文件。名字就是 Makefile,没有后缀名。

缩进和 Tabs

这里要说明一个问题:Makefile 中出现代码缩进的部分,也就是定义编译命令的部分,在代码的开头插入的是 Tabs,不是四个空格也不是六个空格!

这一点很重要。尽管空格和 Tabs 都是看不见的无颜色的字符,但是如果不用 Tabs 的话,Makefile 就会报错。(Python 程序员应该深表同情)

不赘述了。看别人的文章:

关于为什么会有 Tabs:

关于 Makefile 怎么缩进的细节:

如果你在使用 Vim:

手把手教你写一个 Makefile

很好,这部分内容终于开始了。

一个简单的 Makefile 包括以下几个部分:

(1) 定义变量

首先我们定义 CCCFLAGS 这两个变量,用来指定编译时使用的编译器和编译参数。这里的变量定义和赋值的规则和 shell 脚本是一样的。

CC = gcc # 选择 C 语言编译器为 GCC
CFLAGS = -Wall -Wextra -g # 设置编译参数
  • -Wall:启用所有警告信息。这会让编译器输出所有可能的警告,帮助开发者尽早发现潜在的问题。

  • -Wextra:启用额外的警告信息。类似于 -Wall,但会输出更多额外的警告信息,帮助进一步提高代码质量。

  • -g:在可执行文件中嵌入调试信息。这样做可以让程序在调试时能够提供更多有用的信息,例如变量名、行号等,方便开发者进行调试和定位问题。

问:这里的变量命名需要使用相同的 CCCFLAGS 这两个名字吗?能不能改成自己喜欢的别的什么名字

答:当然可以!这只是变量。这两个变量在代码的下文如何体现出自身的作用都是人为定义的,所以你可以选择你喜欢的变量命名方式。但是我不建议这样做,因为在变量命名的时候遵循约定俗成的规律,以便于代码的维护、管理和读写。如果一定要改,最起码要让自己能看得懂,比如:

BIAN_YI_QI = gcc
CAN_SHU = -Wall -Wextra -g

然后我们还要指定项目文件夹。

SRCDIR = src
OBJDIR = obj
BINDIR = bin

(2) 定义规则

首先,我们利用已经定义好的变量,获取我们要编译的代码文件,并规定编译要生成的可执行程序的路径和名称。

SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES))
EXECUTABLE := $(BINDIR)/output

让我来解释一下每一行代码的含义:

首先,SOURCES := $(wildcard $(SRCDIR)/*.c):这一行的作用是使用 wildcard 函数来获取源代码文件的列表。

$(SRCDIR) 是引用我们之前定义的变量,表示源代码存放的目录

*.c 表示所有以 .c 结尾的文件。(* 是 shell 脚本中的“通配符” )

这样就会将所有的 .c 文件列在 SOURCES 变量中。

OBJECTS := $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES)):这一行使用了两个函数。

首先是 patsubst 函数,它用来进行模式替换。

在这里,它的作用是将源代码文件列表中的每个 .c 文件路径替换成对应的目标文件路径,并将结果保存在 OBJECTS 变量中。其中,$(SRCDIR)/%.c 表示源代码文件路径的模式,而 $(OBJDIR)/%.o 则表示目标文件路径的模式。

举个例子:假如此时在 src 文件夹下面有如下的文件:

  • main.c
  • cat.c
  • dog.c
  • fish.c

这个时候,变量 OBJECTS 就会保存如下的文件名:

  • main.o
  • cat.o
  • dog.o
  • fish.o

EXECUTABLE := $(BINDIR)/output:这一行是定义了可执行文件的名称,并将其保存在 EXECUTABLE 变量中。其中,$(BINDIR) 变量是我们之前定义的可执行文件存放的目录,而 output 则是可执行文件的名称。

(2) 定义文件编译规则和依赖关系

all: $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
	$(CC) $(CFLAGS) $^ -o $@

$(OBJDIR)/%.o: $(SRCDIR)/%.c
	$(CC) $(CFLAGS) -c $< -o $@

只要你们还记得我们之前是怎么给变量赋值的,我就可以像下面这样尽可能通俗地解释,保证你听得懂:

  1. all: $(EXECUTABLE):这一行表示告诉 make 命令,如果我要生成所有东西(make all),我想要得到 $(EXECUTABLE) 这个文件。

  2. $(EXECUTABLE): $(OBJECTS):这一行表示告诉 make 命令,要生成 $(EXECUTABLE) 这个文件,需要先生成 $(OBJECTS) 中定义的所有文件。

  3. $(CC) $(CFLAGS) $^ -o $@:这一行是告诉 make 命令,如何把 $(EXECUTABLE) 这个文件生成出来。$^ 表示所有的需要生成的文件(也就是目标文件),而 $@ 表示当前要生成的目标(也就是可执行文件)。整个命令使用了变量 CC 来表示编译器,以及变量 CFLAGS 来表示编译参数。

这个时候我们输入命令 make 和命令 make all 效果是一样的。因为我们并没有定义更加复杂的编译逻辑。但是我打个比方:假如我们要编译三四个可执行文件 a.outb.outc.out,我们就可能定义好几个 make 规则,make amake bmake c 和一次性编译三个文件的规则 make all

(4) 清理规则

定义下面的规则:

clean:
	rm -f $(EXECUTABLE) $(OBJECTS)

用来删除输出的文件。打个比方说,如果你修改了代码,想要看看修改之后编译出来的程序是什么样子的,你就在项目根目录下面执行:

make clean

原本编译好的程序就会被删除。然后你重新执行:

make

就可以在 bin 文件夹下面看到新的程序了。

完整的 Makefile

完整的 Makefile 如下。你们理论上可以直接把下面的内容复制然后拿去用的。

CC = gcc
CFLAGS = -Wall -Wextra -g

SRCDIR = src
OBJDIR = obj
BINDIR = bin

SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES))
EXECUTABLE := $(BINDIR)/printAcat

all: $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)
	$(CC) $(CFLAGS) $^ -o $@

$(OBJDIR)/%.o: $(SRCDIR)/%.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f $(EXECUTABLE) $(OBJECTS)

.PHONY: all clean

在 Makefile 中,.PHONY 是用来声明一个伪目标(phony target)的。伪目标是指在 Makefile 中定义的一个名字,它并不代表一个真实的文件名,而是用来执行一系列命令或者作为其他目标的依赖。

通常情况下,我们会在 .PHONY 中列出一些不产生对应输出文件的目标,例如 cleanall 等。这样做的好处是告诉 Make 工具,即使有一个同名的文件存在,也要执行这个目标所定义的命令。否则,如果存在一个同名文件,Make 工具会认为该文件是最新的,从而不会执行对应的命令。

示例:

.PHONY: clean

clean:
    rm -f *.o

在上面的例子中,.PHONY: clean 声明了 clean 是一个伪目标。这样无论是否存在名为 clean 的文件,执行 make clean 都会执行 rm -f *.o 命令来清理工作目录。

其他附件

懒狗の shell 脚本

有人想说,即使已经有了 Makefile 自动管理编译器编译的过程,创建项目给人感觉还是很繁琐。能不能进一步简化?

答:可以的。高级的办法当然就是一些 Cmake 之类的环境,而低级的最简单的方法就是自己写一个 shell 脚本,把上述的操作过程都封装进去。这样下一次要创建一个 C 语言的项目的时候,只需要执行一下脚本,就能自动完成操作。通过对这个脚本进行修改来根据个人喜好进行定制。与此同时,网上也已经有了相当多类似的项目,你可以直接下载别人写好的脚本拿来用。

比如我这里就已经给你们写好一份了。你们可以直接复制,然后拿去用。

#!/bin/bash

# 一个字符画
# 没有什么用,只是很帅

echo "  _____  ___             _         __    "
echo " / ___/ / _ \\_______    (_)__ ____/ /_   "
echo "/ /__  / ___/ __/ _ \\  / / -_) __/ __/   "
echo "\\___/_/_/  /_/_ \\___/_/ /\\__/\\__/\\__/    "
echo "  / _ |__ __/ /____|___/(_)__  (_) /_    "
echo " / __ / // / __/ _ \\   / / _ \\/ / __/    "
echo "/_/ |_\\_,_/\\__/\\___/__/_/_//_/_/\\__/     "
echo "                  /___/                  "
echo ""
echo "正在自动初始化一个 C 语言项目的主目录..."

# 项目应该有个说明书
touch ./README.md

# 创建项目文件夹
mkdir ./src
mkdir ./obj
mkdir ./lib
mkdir ./doc

# 创建基本的源文件 main.c
touch ./src/main.c

# 创建 Makefile
touch ./Makefile

# 将 Makefile 里的基本内容写入 Makefile
echo "CC = gcc" >> ./Makefile
echo "CFLAGS = -Wall -Wextra -g" >> ./Makefile
echo "" >> ./Makefile
echo "SRCDIR = src" >> ./Makefile
echo "OBJDIR = obj" >> ./Makefile
echo "BINDIR = bin" >> ./Makefile
echo "" >> ./Makefile
echo "SOURCES := \$(wildcard \$(SRCDIR)/*.c)" >> ./Makefile
echo "OBJECTS := \$(patsubst \$(SRCDIR)/%.c,\$(OBJDIR)/%.o,\$(SOURCES))" >> ./Makefile
echo "EXECUTABLE := \$(BINDIR)/printAcat" >> ./Makefile
echo "" >> ./Makefile
echo "all: \$(EXECUTABLE)" >> ./Makefile
echo "" >> ./Makefile
echo "\$(EXECUTABLE): \$(OBJECTS)" >> ./Makefile
echo "	\$(CC) \$(CFLAGS) \$^ -o \$@" >> ./Makefile
echo "" >> ./Makefile
echo "\$(OBJDIR)/%.o: \$(SRCDIR)/%.c" >> ./Makefile
echo "	\$(CC) \$(CFLAGS) -c \$< -o \$@" >> ./Makefile
echo "" >> ./Makefile
echo "clean:" >> ./Makefile
echo "	rm -f \$(EXECUTABLE) \$(OBJECTS)" >> ./Makefile
echo "" >> ./Makefile
echo ".PHONY: clean all" >> ./Makefile

echo "... 创建好了。"
echo "现在当前目录下有以下的文件:"

ls

执行效果如下:

$ auto-C-proj.sh
  _____  ___             _         __
 / ___/ / _ \_______    (_)__ ____/ /_
/ /__  / ___/ __/ _ \  / / -_) __/ __/
\___/_/_/  /_/_ \___/_/ /\__/\__/\__/
  / _ |__ __/ /____|___/(_)__  (_) /_
 / __ / // / __/ _ \   / / _ \/ / __/
/_/ |_\_,_/\__/\___/__/_/_//_/_/\__/
                  /___/

正在自动初始化一个 C 语言项目的主目录...
... 创建好了。
现在当前目录下有以下的文件:
auto-C-proj.sh  doc  lib  Makefile  obj  README.md  src

那个能够编译排版论文的 Makefile

我猜你们会想要这个的。使用这个脚本的时候,首先要确定电脑上已经正确安装并配置了 pandoc 文档转换软件,且命令行下能够正常使用。

项目包含以下几个文件夹:

  • libs 存放了论文的排版样式模板、参考文献表的文件夹(参考样式可以用 make reference 来生成。参考文献需要写成 .bib 引用格式)
  • release 存放了输出的文档的文件夹
  • image 存放了论文图片的文件夹

论文文件 main.md 放在当前目录的主文件夹下面。

SRC_DIR := .
OUTPUT_DIR := release
REFERENCE_DIR := reference
MD_FILE := report.md
DOCX_FILE := $(OUTPUT_DIR)/report.docx
REFERENCE_FILE := $(REFERENCE_DIR)/custom-reference.docx 
BIB_FILE := $(REFERENCE_DIR)/Refe.bib
IMAGE_DIR := image
PANDOC_OPTIONS = \
--toc\
--mathjax\
--highlight-style=pygments\
--reference-doc=$(REFERENCE_FILE)\
--cite --bibliography=$(BIB_FILE)

.PHONY: all clean reference

all: $(DOCX_FILE)

$(DOCX_FILE): $(SRC_DIR)/$(MD_FILE)
	pandoc $< -o $@ -s $(PANDOC_OPTIONS) --resource-path=$(IMAGE_DIR) 

reference:
	pandoc -o ./reference/custom-reference.docx --print-default-data-file reference.docx

clean:
	rm -rf $(OUTPUT_DIR)/*

这里仅作一示例。至于 Pandoc 软件的安装和用法,因为超出本文的范畴,故不做赘述了。

  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值