一份快速入门的 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.c
和 helper.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) 定义变量
首先我们定义 CC
和 CFLAGS
这两个变量,用来指定编译时使用的编译器和编译参数。这里的变量定义和赋值的规则和 shell 脚本是一样的。
CC = gcc # 选择 C 语言编译器为 GCC
CFLAGS = -Wall -Wextra -g # 设置编译参数
-Wall
:启用所有警告信息。这会让编译器输出所有可能的警告,帮助开发者尽早发现潜在的问题。-Wextra
:启用额外的警告信息。类似于-Wall
,但会输出更多额外的警告信息,帮助进一步提高代码质量。-g
:在可执行文件中嵌入调试信息。这样做可以让程序在调试时能够提供更多有用的信息,例如变量名、行号等,方便开发者进行调试和定位问题。
问:这里的变量命名需要使用相同的
CC
和CFLAGS
这两个名字吗?能不能改成自己喜欢的别的什么名字
答:当然可以!这只是变量。这两个变量在代码的下文如何体现出自身的作用都是人为定义的,所以你可以选择你喜欢的变量命名方式。但是我不建议这样做,因为在变量命名的时候遵循约定俗成的规律,以便于代码的维护、管理和读写。如果一定要改,最起码要让自己能看得懂,比如:
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 $@
只要你们还记得我们之前是怎么给变量赋值的,我就可以像下面这样尽可能通俗地解释,保证你听得懂:
all: $(EXECUTABLE)
:这一行表示告诉make
命令,如果我要生成所有东西(make all
),我想要得到$(EXECUTABLE)
这个文件。$(EXECUTABLE): $(OBJECTS)
:这一行表示告诉make
命令,要生成$(EXECUTABLE)
这个文件,需要先生成$(OBJECTS)
中定义的所有文件。$(CC) $(CFLAGS) $^ -o $@
:这一行是告诉make
命令,如何把$(EXECUTABLE)
这个文件生成出来。$^
表示所有的需要生成的文件(也就是目标文件),而$@
表示当前要生成的目标(也就是可执行文件)。整个命令使用了变量CC
来表示编译器,以及变量CFLAGS
来表示编译参数。
这个时候我们输入命令 make
和命令 make all
效果是一样的。因为我们并没有定义更加复杂的编译逻辑。但是我打个比方:假如我们要编译三四个可执行文件 a.out
、b.out
、c.out
,我们就可能定义好几个 make
规则,make a
,make b
,make 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
中列出一些不产生对应输出文件的目标,例如 clean
、all
等。这样做的好处是告诉 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 教程的更多相关文章
- Spring Boot 2.0 的快速入门(图文教程)
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! Spring Boot 2.0 的快速入门(图文教程) 大家都 ...
- MyBatis学习总结-MyBatis快速入门的系列教程
MyBatis学习总结-MyBatis快速入门的系列教程 [MyBatis]MyBatis 使用教程 [MyBatis]MyBatis XML配置 [MyBatis]MyBatis XML映射文件 [ ...
- 零基础快速入门SpringBoot2.0教程 (三)
一.SpringBoot Starter讲解 简介:介绍什么是SpringBoot Starter和主要作用 1.官网地址:https://docs.spring.io/spring-boot/doc ...
- 零基础快速入门SpringBoot2.0教程 (二)
一.SpringBoot2.x使用Dev-tool热部署 简介:介绍什么是热部署,使用springboot结合dev-tool工具,快速加载启动应用 官方地址:https://docs.spring. ...
- html快速入门(基础教程+资源推荐)
1.html究竟是什么? 从字面上理解,html是超文本标记语言hyper text mark-up language的首字母缩写,指的是一种通用web页面描述语言,是用来描述我们打开浏览器就能看到的 ...
- 零基础快速入门SpringBoot2.0教程 (四)
一.JMS介绍和使用场景及基础编程模型 简介:讲解什么是小写队列,JMS的基础知识和使用场景 1.什么是JMS: Java消息服务(Java Message Service),Java平台中关于面向消 ...
- Redis之快速入门与应用[教程/总结]
内容概要 因为项目中用户注册发送验证码,需要学习redis内存数据库,故而下午花了些时间进行初步学习.本博文性质属于对今日redis学习内容的小结.在看本博文前或者看完后,可以反问自己三个问题:Red ...
- Golang快速入门
Go语言简介: Golang 简称 Go,是一个开源的编程语言,Go是从2007年末由 Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian L ...
- 专为设计师而写的GitHub快速入门教程
专为设计师而写的GitHub快速入门教程 来源: 伯乐在线 作者:Kevin Li 原文出处: Kevin Li 在互联网行业工作的想必都多多少少听说过GitHub的大名,除了是最大的开源项目 ...
- CMake快速入门教程-实战
http://www.ibm.com/developerworks/cn/linux/l-cn-cmake/ http://blog.csdn.net/dbzhang800/article/detai ...
随机推荐
- 实验1 在MAX10 FPGA上实现组合逻辑
实验1 在MAX10 FPGA上实现组合逻辑 实验前的准备工作:参照讲义步骤安装Quartus,Modelsim和System Builder.阅读材料:1)推荐的文件组织形式:2)Verilog 1 ...
- Quartus prime 的安装步骤:
- vue+vant实现省市联动(van-area)组件(包含比较全面的全国省市数组数据)
组件库太香了,人家nb,自己写的都是** 效果: 1.安装vant库以及main.js的配置 2.一般结合van-popup组件 </template> <van-popup v-m ...
- 都2024年了,你还不知道git worktree么?
三年前 python 大佬吉多·范罗苏姆(为 Python 程序设计语言的最初设计者及主要架构师)才知道 git worktree ,我现在才知道,我觉得没啥丢人的. 应用场景 如果你正在 featu ...
- AnaTraf 网络万用表流量分析教程系列 | AnaTraf 网络万用表 B 站频道
为了更好的向大家分享如何使用 AnaTraf 网络万用表进行网络流量分析.网络故障排除,AnaTraf 开通了 B 站频道. 在 B 站上,将以视频的形式向大家介绍如何使用 AnaTraf 网络万用表 ...
- Vue3 项目
创建 Vue3 项目的步骤如下: 安装 Node.js Vue3 需要依赖 Node.js 环境,因此需要先安装 Node.js.可以从官网下载 Node.js 的安装包并安装,也可以使用包管理器安装 ...
- IPv6 — 移动性
目录 文章目录 目录 前文列表 IPv6 的移动性 移动操作 路由优化 前文列表 <IPv6 - 网际协议第 6 版> <IPv6 - 地址格式与寻址模式> <IPv6 ...
- 有隙可乘 - Android 序列化漏洞分析实战
作者:vivo 互联网大前端团队 - Ma Lian 本文主要描述了FileProvider,startAnyWhere实现,Parcel不对称漏洞以及这三者结合产生的漏洞利用实战,另外阐述了漏洞利用 ...
- 用pageOffice控件实现 office word文档 强制留痕编辑Word
OA办公中,业务需要多人编辑word文档,需要强制留痕功能,用来查看文档编辑过程中的具体修改痕迹. 怎么实现word文档的强制留痕呢? 1 实现方法 通过pageOffice实现简单的在线打开编辑wo ...
- grafana模板参考
空的,把面板都删除了 { "__inputs": [ { "name": "DS_PROMETHEUS", "label" ...