一份快速入门的 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 文档转换工具生成的。

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

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

但是,假如我没有 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 软件的安装和用法,因为超出本文的范畴,故不做赘述了。

一份快速入门的 Makefile 教程的更多相关文章

  1. Spring Boot 2.0 的快速入门(图文教程)

    摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! Spring Boot 2.0 的快速入门(图文教程) 大家都 ...

  2. MyBatis学习总结-MyBatis快速入门的系列教程

    MyBatis学习总结-MyBatis快速入门的系列教程 [MyBatis]MyBatis 使用教程 [MyBatis]MyBatis XML配置 [MyBatis]MyBatis XML映射文件 [ ...

  3. 零基础快速入门SpringBoot2.0教程 (三)

    一.SpringBoot Starter讲解 简介:介绍什么是SpringBoot Starter和主要作用 1.官网地址:https://docs.spring.io/spring-boot/doc ...

  4. 零基础快速入门SpringBoot2.0教程 (二)

    一.SpringBoot2.x使用Dev-tool热部署 简介:介绍什么是热部署,使用springboot结合dev-tool工具,快速加载启动应用 官方地址:https://docs.spring. ...

  5. html快速入门(基础教程+资源推荐)

    1.html究竟是什么? 从字面上理解,html是超文本标记语言hyper text mark-up language的首字母缩写,指的是一种通用web页面描述语言,是用来描述我们打开浏览器就能看到的 ...

  6. 零基础快速入门SpringBoot2.0教程 (四)

    一.JMS介绍和使用场景及基础编程模型 简介:讲解什么是小写队列,JMS的基础知识和使用场景 1.什么是JMS: Java消息服务(Java Message Service),Java平台中关于面向消 ...

  7. Redis之快速入门与应用[教程/总结]

    内容概要 因为项目中用户注册发送验证码,需要学习redis内存数据库,故而下午花了些时间进行初步学习.本博文性质属于对今日redis学习内容的小结.在看本博文前或者看完后,可以反问自己三个问题:Red ...

  8. Golang快速入门

    Go语言简介: Golang 简称 Go,是一个开源的编程语言,Go是从2007年末由 Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian L ...

  9. 专为设计师而写的GitHub快速入门教程

    专为设计师而写的GitHub快速入门教程 来源: 伯乐在线 作者:Kevin Li     原文出处: Kevin Li 在互联网行业工作的想必都多多少少听说过GitHub的大名,除了是最大的开源项目 ...

  10. CMake快速入门教程-实战

    http://www.ibm.com/developerworks/cn/linux/l-cn-cmake/ http://blog.csdn.net/dbzhang800/article/detai ...

随机推荐

  1. 实验1 在MAX10 FPGA上实现组合逻辑

    实验1 在MAX10 FPGA上实现组合逻辑 实验前的准备工作:参照讲义步骤安装Quartus,Modelsim和System Builder.阅读材料:1)推荐的文件组织形式:2)Verilog 1 ...

  2. Quartus prime 的安装步骤:

  3. vue+vant实现省市联动(van-area)组件(包含比较全面的全国省市数组数据)

    组件库太香了,人家nb,自己写的都是** 效果: 1.安装vant库以及main.js的配置 2.一般结合van-popup组件 </template> <van-popup v-m ...

  4. 都2024年了,你还不知道git worktree么?

    三年前 python 大佬吉多·范罗苏姆(为 Python 程序设计语言的最初设计者及主要架构师)才知道 git worktree ,我现在才知道,我觉得没啥丢人的. 应用场景 如果你正在 featu ...

  5. AnaTraf 网络万用表流量分析教程系列 | AnaTraf 网络万用表 B 站频道

    为了更好的向大家分享如何使用 AnaTraf 网络万用表进行网络流量分析.网络故障排除,AnaTraf 开通了 B 站频道. 在 B 站上,将以视频的形式向大家介绍如何使用 AnaTraf 网络万用表 ...

  6. Vue3 项目

    创建 Vue3 项目的步骤如下: 安装 Node.js Vue3 需要依赖 Node.js 环境,因此需要先安装 Node.js.可以从官网下载 Node.js 的安装包并安装,也可以使用包管理器安装 ...

  7. IPv6 — 移动性

    目录 文章目录 目录 前文列表 IPv6 的移动性 移动操作 路由优化 前文列表 <IPv6 - 网际协议第 6 版> <IPv6 - 地址格式与寻址模式> <IPv6 ...

  8. 有隙可乘 - Android 序列化漏洞分析实战

    作者:vivo 互联网大前端团队 - Ma Lian 本文主要描述了FileProvider,startAnyWhere实现,Parcel不对称漏洞以及这三者结合产生的漏洞利用实战,另外阐述了漏洞利用 ...

  9. 用pageOffice控件实现 office word文档 强制留痕编辑Word

    OA办公中,业务需要多人编辑word文档,需要强制留痕功能,用来查看文档编辑过程中的具体修改痕迹. 怎么实现word文档的强制留痕呢? 1 实现方法 通过pageOffice实现简单的在线打开编辑wo ...

  10. grafana模板参考

    空的,把面板都删除了 { "__inputs": [ { "name": "DS_PROMETHEUS", "label" ...