资讯动态

Makefile入门指南:从手动gcc到自动化构建

发布时间:2026/10/9 7:26:49 来源:尧图企业网站定制
手动敲gcc敲到想吐的时候我就知道该聊聊make和makefile了。前面几篇把vim、gcc/g、gdb这些基础开发工具过了一遍这次轮到自动化构建这一环。说白了make/makefile就是帮我们把编译、链接、清理、安装这一整套重复劳动自动化掉的工具。这篇文章适合刚接触Linux下C/C开发的新手也适合已经在写项目但每次改完代码还在手动敲编译命令的人——读完你会明白makefile学的不只是语法而是一套如何让机器替自己干活的思维方式。1. 手动编译的痛苦我替你们再回忆一遍1.1 先说说没有make的时候是怎么过的刚开始写C语言作业的时候大家应该都干过这种事写一个main.c然后gcc main.c -o main跑一下改一下再gcc一次。单文件项目这么干完全没毛病甚至比make还方便。但一旦项目变成三个文件、五个文件、十几个文件画风就变了。比如这样一个典型结构project/ ├── main.c ├── utils.c ├── utils.h ├── calc.c ├── calc.h └── ...每次修改完代码你要把这些文件全部编译一遍再链接gcc -c main.c -o main.o gcc -c utils.c -o utils.o gcc -c calc.c -o calc.o gcc main.o utils.o calc.o -o app三五个文件还能忍到了十几个文件人就开始犯晕是不是漏了某个.c是不是有个.o是旧的为什么我改了main.c还要把utils.c也重新编译一遍更痛苦的是头文件。头文件一改所有包含它的.c文件都得重新编译你不重编运行时就会出现改了头文件但程序行为没变的诡异情况排查半天最后发现是忘了重新编译某个模块。这种浪费的时间加起来比写代码还多。1.2 makefile到底省了什么增量编译的本质make的核心价值总结成四个字增量编译。它不会傻乎乎地把所有文件都重新编一遍而是通过对比目标文件和依赖文件的时间戳只重建那些确实需要重建的东西。打个比方你就懂了。你整理房间不会每次打扫都把衣柜里所有衣服翻出来重洗一遍而是看哪件衣服脏了才洗哪件。makefile里的每条规则就是在告诉make这件衣服什么时候算脏、脏了怎么洗。所以make/makefile解决的三个核心问题自动化一条make命令搞定编译链接清理不用手动敲一堆gcc。增量构建只重编修改过的文件和受影响的文件大项目编译时间从天级降到秒级。依赖管理清楚地记录每个文件由什么生成、依赖什么别人接手项目时看一眼makefile就知道怎么构建。这三点正是后面学cmake、学大型项目构建系统比如Linux内核的Kbuild、Android的Soong的地基。把makefile吃透了再去看那些自动化构建框架就是居高临下地看而不是一头雾水地学。2. makefile核心三要素目标、依赖、命令2.1 一条规则拆开看目标、依赖、命令makefile的基本结构其实一句话就能说完目标: 依赖 命令目标target就是你要生成什么通常是可执行文件或.o文件依赖prerequisites就是生成这个目标需要哪些东西命令recipe就是用什么动作从依赖生成目标。比如最简单的app: main.o utils.o gcc -o app main.o utils.o这条规则的意思要生成app先得有main.o和utils.o然后执行gcc把它们链接起来。但这里有个新手最容易困惑的点make不是从上到下把所有规则都执行一遍而是先找第一个目标把它当成终极目标然后看这个目标的依赖又依赖什么递归地往下找把整条依赖链上的规则都跑一遍。2.2 为什么make只重建需要重建的东西时间戳机制make判断需不需要重建的逻辑非常简单粗暴如果目标文件不存在或者依赖文件的修改时间比目标文件新就执行命令。这就是为什么make能实现增量编译。你改了main.cmain.c的时间戳比main.o新make就重建main.o再链接app但utils.c没动utils.o还是新的就不会重新编译直接拿旧文件用。注意make比较的是修改时间不是内容。哪怕你只touch了一下某个文件只改了时间戳、内容没变make也会认为它变了触发重建。这个机制有个隐藏的大坑如果你的系统时间出了问题比如文件时间戳是未来时间make会一直认为目标文件不够新每次make都全部重新编译。我见过有人虚拟机时间没同步整个项目每次构建都全量重编还以为是makefile写错了。2.3 伪目标和.PHONYclean为什么这么写初学者看网上教程经常看到这样的写法clean: rm -f *.o app然后问题来了如果当前目录下恰好有一个叫clean的文件或文件夹你执行make cleanmake会告诉你clean是最新的然后什么都不干因为clean这个文件存在而且没有依赖make觉得它根本不需要重建。解决办法就是声明伪目标.PHONY: clean clean: rm -f *.o app.PHONY的意思就是告诉make别检查这个目标是不是比依赖新也不要管有没有同名文件只要执行这个目标就把命令跑一遍。我常用的伪目标除了clean还有install、run、test这几个后面写项目的时候你就知道有多方便了。注意只要一个目标的实际作用不是生成一个真实文件统统建议放在.PHONY里这是个好习惯。2.4 不会变量和自动变量makefile就还没入门如果makefile里全是重复的目标名、文件名那它跟硬编码的shell脚本没什么区别。makefile的灵魂在于变量和自动变量。变量很简单就是替换CC gcc CFLAGS -Wall -g OBJ main.o utils.o app: $(OBJ) $(CC) -o app $(OBJ)以后要换编译器改一行CC就行要加编译选项改CFLAGS就行。这才是自动化该有的样子。自动变量才是真正的黑科技。最常用的四个$表示当前规则的目标名$表示第一个依赖文件的名称$^表示所有依赖文件的名称以空格分隔$?表示所有比目标文件新的依赖文件列表配合通配符整个makefile可以被压缩得极短。比如app: main.o utils.o gcc -o $ $^这条命令展开后就是gcc -o app main.o utils.o。$代表app$^代表main.o utils.o。写代码的时候别人问起来能说清楚$和$^是什么才算真正看过makefile而不是复制粘贴跑了就算会。3. 从零写一个能用的makefile从最简单的到多目录工程3.1 最简版本一个单文件程序先来个最简单的情况只有一个main.capp: main.c gcc -o app main.c够简单但这样有个问题每次make app只要main.c有改动就直接编译完全没用到中间产物.o。对于单文件没问题但不利于理解make的增量思想也不利于大项目分模块编译。3.2 多文件版本用变量和自动变量收拾整齐假设项目结构src/ ├── main.c ├── utils.c ├── calc.c └── headers/ ├── utils.h └── calc.h一个还算正规的makefile可以这么写CC gcc CFLAGS -Wall -g -I./headers OBJ main.o utils.o calc.o TARGET app $(TARGET): $(OBJ) $(CC) -o $ $^ %.o: %.c $(CC) $(CFLAGS) -c $ -o $ .PHONY: clean clean: rm -f $(TARGET) $(OBJ)这里有个东西要专门解释%.o: %.c是模式规则是一个通配意思是任何一个.o文件都由同名的.c文件编译出来。比如make在构建app时发现缺main.o就去找main.c套用这条规则。为什么要专门写这一条规则因为gcc虽然有默认规则能直接.c编译到.o但默认规则不带我们自定义的-I./headers头文件路径和编译选项。一旦实战中涉及自定义头文件目录、宏定义、特定优化开关就必须要自己写模式规则。提醒一句编译选项里的-I是指定头文件搜索路径的后面跟目录名告诉编译器去这个目录里找头文件。新手经常漏了它然后面对一堆xxx.h: No such file or directory的报错发懵。3.3 头文件依赖改头文件不重编是最大的坑上面这个makefile有个严重缺陷规则里写的是main.o依赖main.c没写main.o也依赖main.c里include的那些.h。实际场景是这样main.c里面#include utils.h。你改了utils.h但main.c没有改动按上面这个makefilemain.c时间戳没变make不会重建main.o。结果就是头文件改了程序行为没变你还以为是自己代码逻辑写错了折腾一下午。解决这个问题有正规军打法自动生成依赖文件后面进阶部分专门讲。这里先给一个适合小项目的土办法手动把所有头文件写进依赖列表。main.o: main.c utils.h calc.h utils.o: utils.c utils.h calc.o: calc.c calc.h模板规则照旧这样任何头文件改动包含它的.o都会重新编译。缺点得手动维护依赖关系文件多了容易漏。小项目十几二十个文件手写还能忍上百个文件就得交给编译器自动生成。这个思路我们在第5节展开。3.4 多目录嵌套和交叉编译的makefile写法真实项目不可能所有源码堆在一个文件夹里。常见的做法是用子目录makefile或者顶层makefile直接通行通编。先展示一下顶层makefile怎么处理子目录源文件CC gcc CFLAGS -Wall -g -Isrc/headers SRCDIR src OBJDIR obj SRCS $(wildcard $(SRCDIR)/*.c) OBJS $(patsubst $(SRCDIR)/%.c, $(OBJDIR)/%.o, $(SRCS)) TARGET app $(TARGET): $(OBJS) $(CC) -o $ $^ $(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR) $(CC) $(CFLAGS) -c $ -o $ $(OBJDIR): mkdir -p $ .PHONY: clean clean: rm -rf $(TARGET) $(OBJDIR)这里出现了两个新函数$(wildcard ...)用来收集所有.c文件$(patsubst ...)把src/xxx.c转换为obj/xxx.o。这种写法的好处是以后在src目录新增一个.c文件makefile不需要改动自动被通配符收集进去。这是从写死文件列表到自动发现文件列表的分水岭。至于交叉编译场景比如给ARM板子交叉编译只改CC一行就够CC arm-linux-gnueabihf-gcc这也是makefile设计的精髓编译逻辑和工具链解耦。换平台不用改构建规则只换工具链前缀。4. 新手最常踩的make报错逐个拆给你看4.1 make没有指明目标并且找不到makefile是怎么回事这是搜索引擎里高频出现的问题。很多人Linux刚装好随便找个目录敲了个make结果得到make: *** 没有指明目标并且找不到 makefile。停止。这根本不算错误只是make在说你既没指定目标当前目录下也没有makefile我不知道该干什么。make默认查找的文件名是按顺序来的GNUmakefile、makefile、Makefile推荐用Makefile让文件夹排序时它排在前面。碰到这个提示你该做的先ls看看当前目录有没有makefile没有就去源码目录里找或者自己写一个。还有一种常见情况你想编译的项目确实有Makefile但你站在了错误的目录层级比如进入src子目录而Makefile在上级目录。find . -name Makefile查一下就好。4.2 Windows下make无法识别为cmdlet、函数是怎么来的很多人在Windows上装好编辑器想在终端里跑make遇到make : 无法将“make”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这个问题的本质很简单Windows的PowerShell不知道make这个可执行文件装在哪。解决方案看你的工作环境装了WSL路径没问题的情况下直接在WSL的Linux终端里使用make而不是在PowerShell里。装了MinGW/MSYS2确认中的bin目录已加入系统PATH环境变量。VSCode Remote连上远程Linux后再开终端不要用本地Windows终端。初学者最容易掉进去的坑明明vscode显示的是Linux路径本地终端却还是Windows PowerShell。记住一点make是Linux世界的东西让它在Linux环境里跑别在Windows壳里折腾。4.3 Nothing to be done到底是不是坏事遇到make: app 是最新的。这是好事情make检查完时间戳发现不用重新编译。但有个特殊情况会让人困惑你明明改了代码但还是提示最新。这种情况十有八九是系统时间问题比如目标文件的时间戳比源文件还晚。用ls -l看一下文件时间再用touch更新时间戳基本就能解决。还有一种情况你声明了.PHONY伪目标但执行它的时候没反应。检查一下是不是在.PHONY声明和实际规则之间多了别的东西或者缩进用了空格而make规定命令前必须用Tab键。makefile一个经典大坑就是缩进命令行的开头必须是Tab字符用空格会报缺少分隔符或者直接执行异常。在vim里编辑makefile建议开:set list看看隐藏字符确认Tab是Tab。4.4 undefined reference和头文件找不到库路径、头文件路径的问题链接阶段的undefined reference to xxx是所有C/C开发者的老朋友。这个问题根源大部分时候不是代码写错而是链接时缺了库。比如你用了数学库的sqrt()编译时没加-lm就会undefined reference。记住一个口诀编译用-I找头文件链接用-L找库文件用-l指定库名。-l后面跟库名的时候要去掉lib前缀和.so/.a后缀。比如libm.a就写-lmlibpthread.so就写-lpthread。makefile里对应就是LDFLAGS -L./lib -lm不少从GitHub拉下来的项目README里会写需要安装依赖库如果你make时报一堆undefined reference先检查依赖库装了没再去查-L和-l。顺序反了折腾半天还是报错。4.5 从GitHub拉下来的项目无法直接make或提示权限问题拉下一个开源项目make的时候提示找不到文件或者make成功但make install失败这种情况太常见了。先说make install。它通常要把文件拷到系统目录比如/usr/local/bin这需要root权限。正确姿势是在顶层目录里用sudo执行install那一层make sudo make install而不是整条sudo make一把梭。因为某些项目的构建过程中需要生成用户属主的配置或缓存文件全程sudo反而会把文件权限搞乱后续重新构建还得处理权限问题。我在实际编译一些开源库时喜欢用make -j$(nproc)多核并行编译。注意并行编译偶尔会碰上依赖没写全的项目出现莫名其妙的编译失败。这种时候先串行编译一遍确认代码没问题再用并行加快速度。另外如果你只是想在自己的用户目录下安装不污染系统环境可以用DESTDIR或PREFIX重定向make install DESTDIR$HOME/myapp这样装出来的东西都在你自己的目录里删掉也干净不用sudo。5. makefile进阶技巧以及cmake和它的关系5.1 .d文件真正的自动依赖跟踪前面说的手动写头文件依赖在真实项目里撑不了多久。编译器其实自带一个非常实用的能力——自动生成依赖关系。gcc的-MM选项会分析源文件里的#include输出依赖列表。比如gcc -MM main.c输出类似main.o: main.c utils.h calc.h这个功能可以配合makefile写成自动依赖生成DEP $(OBJS:.o.d) %.d: %.c $(CC) -MM $ $ -include $(DEP)流程是这样的每个.c生成对应的.d文件里面记录这个.o依赖哪些.hmake启动时用-include把.d文件内容包含进来就像手动写了完整依赖规则一样。以后你再也不用管头文件依赖了加头文件改头文件make都能自动感知。注意这里用的是-include而不是include区别在于include如果文件不存在会报错退出-include只是静默跳过。首次build时.d文件还没生成如果用include就会直接失败。5.2 VPATH和模式规则把makefile写得更像模板当源文件和目标文件在不同目录时除了第3节的写死路径方法make还提供VPATH机制。一行VPATH srcmake就会自动去src目录找.c文件。VPATH src OBJ main.o utils.o app: $(OBJ) $(CC) -o $ $^ %.o: %.c $(CC) $(CFLAGS) -c $ -o $配合多目录结构可以用vpath小写精确指定哪些后缀去哪找比如vpath %.c src、vpath %.h include。关于模式规则还有个细节值得注意%.o: %.c和具体的main.o: main.c同时存在的时候make会用更具体的那条规则。这点被很多人忽略但实战中利用它可以做一些特殊文件特殊处理、其他文件走通用的精细控制。5.3 为什么现在项目更喜欢cmakemakefile还要不要学热搜词里cmake和makefile区别的热度常年居高不下。简单说清楚它们的关系cmake本身不是构建工具它是元构建系统它的作用是根据CMakeLists.txt生成一套makefile或者ninja构建文件然后实际干活还是make在干。cmake的优势集中在三点跨平台自动适配Windows/Linux/macOS的编译器差异、自动探测系统依赖库、以及更友好的语法。一个几十行的CMakeLists.txt如果纯手写makefile可能要折腾几百行。那makefile还要不要学要而且必须学。理由是真实环境里太多项目直接以makefile交付老一点的项目和嵌入式开发makefile仍然是主流。你至少要看得懂、能改、能修这是基本功cmake是加分项是现代化协作的必备技能。两者关系有点像makefile是螺丝刀cmake是电动螺丝刀——你当然可以直接用电动的但没有手动螺丝刀的基础遇到特殊螺丝还是会抓瞎。我个人给学习路线的建议先用makefile手工构建几个小项目把编译、链接、依赖、变量这些概念彻底吃透然后再切换到cmake你会发现cmake的一切设计你都能快速理解因为它的本质就是在帮你生成makefile。跳过makefile直接学cmake的人遇到底层的链接错误往往一脸懵因为缺少构建过程是怎么一步步发生的这一层心智模型。最后聊点实际的我这几年帮人排查构建问题一大半的根源其实都不在makefile语法本身而是对编译流程缺乏整体认识预处理、编译、汇编、链接四个阶段分别干什么.o文件是什么符号解析发生哪个阶段-I和-L分别解决哪一侧的问题。makefile语法再熟练不理解这些出问题照样两眼一抹黑。所以给看完这篇的朋友一个行动建议找个3个文件以上的小项目自己从头写一份makefile先用最原始的写法再逐步换成变量模式规则自动依赖的写法最后跑一下make clean和make -j。这套流程走完你对自动化构建这件事的理解会扎实很多。后面再碰cmake或者大型构建系统你会觉得它们都只是在makefile思想之上盖的高楼而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑