一份快速入门的 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. [FE] 实时视频流库 hls.js 重载切换资源的方式

    hls 播放需要先 attachMedia,然后 loadSource. 如果切换 resource,需要先执行 hls.destroy(),否则会出现混乱. destroy 之后再依次进行 hls ...

  2. UOS 开启 VisualStudio 远程调试 .NET 应用之旅

    本文记录的是在 Windows 系统里面,使用 VisualStudio 2022 远程调试运行在 UOS 里面 dotnet 应用的配置方法 本文写于 2024.03.19 如果你阅读本文的时间距离 ...

  3. dotnet 6 在 System.Text.Json 使用 source generation 源代码生成提升 JSON 序列化性能

    这是一个在 dotnet 6 早就引入的功能,此功能的使用方法能简单,提升的效果也很棒.使用的时候需要将 Json 序列化工具类换成 dotnet 运行时自带的 System.Text.Json 进行 ...

  4. 9.prometheus监控--监控springboot2.x(Java)

    一.环境部署 yum search java | grep jdk yum install -y java-11-openjdk-devel 二.监控java应用(tomcat/jar) JMX ex ...

  5. WebGL实现简易的局部“马赛克”

    前言 接触过Canvas的小伙伴应该都知道,在Canvas2D中我们要加载一个图片很简单,通过调用drawImage API就能将图像绘制到画布上,当然在WebGL中我们也可以绘制图像,在绘制时我们需 ...

  6. Dinky实时计算平台

    前言:Apache Flink 作为新一代的实时计算框架已经被应用到各个行业与领域,其岂存在着应用的痛点比如 FlinkSQL 在线IDE.作业提交不友好.作业无监控报警等.很大程度上说,FlinkS ...

  7. 四、【转】基于知识图谱的推荐系统(KGRS)综述

    以下文章来源于AI自然语言处理与知识图谱 ,作者Elesdspline 导语 本文是2020年针对知识图谱作为辅助信息用于推荐系统的一篇综述.知识图谱对于推荐系统不仅能够进行更精确的个性化推荐,而且对 ...

  8. MYSQL CONVERT、JSON_EXTRACT函数的使用总结

    一.CONVERT.CONCAT.COUNT函数联合查询 CONVERT()函数用于将值从一种数据类型转换为表达式中指定的另一种数据类型. MySQL还允许它将指定的值从一个字符集转换为另一个字符集. ...

  9. C#/.NET/.NET Core优秀项目和框架2024年4月简报

    前言 公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(每周至少会推荐两个优秀的项目和框架当然节假日除外),公众号推文中有项目和框架的介绍.功能特点.使用方式以及部分功能截图 ...

  10. android中Room数据库的基本使用

    简介: 还在使用原生的sqllite?有这么清爽且稳如狗的room为啥不用呢? Room是Google官方推荐使用的数据库,相比较某些优秀数据库框架来说,不用过于担心某天库会停止维护,且访问数据库非常 ...