如果你在Linux下写完第一段C代码兴致勃勃地敲下make回车屏幕上却弹出一句“make: *** 没有指明目标并且找不到makefile。停止。”——恭喜你这是很多程序员第一次和Makefile打交道时都会撞上的墙。其实make并不是没装好也不是命令输入错了而是它默认要找一份叫Makefile的“说明书”没有这份说明书它根本不知道该干什么。Makefile本质上是一份依赖关系清单和一套执行流程。你告诉make我要生成什么它依赖哪些文件以及用哪条命令从依赖生成目标。make拿到这份说明书后会自己检查文件的时间戳决定哪些步骤需要重新执行。这篇文章里我会从零开始把Makefile中最核心、最常用的知识串成一条“够用就能上”的路线也把几个高频报错逐一拆开尤其是那句看着像系统故障的“没有指明目标并且找不到makefile”其实背后全是小问题一文值回票价。1. 先搞懂Makefile在干什么目标和依赖是灵魂1.1 Makefile到底在解决什么问题设想一个简单的C项目只有main.c和hello.c两个文件。不用Makefile时首次编译你需要敲gcc -Wall -Wextra -O2 -g -c main.c -o main.o gcc -Wall -Wextra -O2 -g -c hello.c -o hello.o gcc -o app main.o hello.o第二次编译时你只改了hello.c理论上只需要重新编译hello.c再链接一次。但手工操作时绝大多数人会选择把三行命令全部重敲一遍或者更糟糕靠“上箭头找历史命令”碰运气。Makefile解决的就是这个痛点。它通过文件之间的依赖关系和时间戳来判断“谁需要重新生成”。只要目标和依赖之间存在“目标不存在或者依赖比目标更新”的情况make就执行对应的命令。说白了make是一个懂得看“保质期”的自动化工匠只有食材比半成品还新鲜时它才肯重新下锅。1.2 三要素目标、依赖、配方Makefile的基本语法非常短短到可以用一个Hello级例子讲完hello: hello.c gcc -o hello hello.c这里有三样东西hello是目标target也就是你想最终生成的东西可以是一个可执行文件、一个.o文件也可以是一个动作标签比如clean。hello.c是依赖prerequisite目标能否成立取决于这些文件是否存在、是否更新。第二行以Tab键开头的不是普通缩进而是配方recipe也就是当目标需要重建时实际执行的命令。这里我把缩进写成了真正的Tab字符你复制后不要用空格替换否则make会直接报错。make判断规则很简单如果hello不存在或者hello.c的修改时间比hello更晚就执行第二行的gcc命令。如果hello已经存在且hello.c更老make就会告诉你nothing to be done。1.3 用“做菜”打比方你会秒懂把Makefile想成一张菜谱。目标是“端出一盘鱼香肉丝”依赖是“肉丝、木耳、豆瓣酱这些食材”配方就是“下锅翻炒”这个动作。如果你从来没做过这道菜那肯定要动手炒一盘。如果冰箱里的食材刚刚买回来比你上次炒好的那份菜还新鲜你觉得应该重新炒一份。但如果食材放了三天比你昨天炒的那盘菜还老再炒一份也是浪费。make做的事情就是在每次启动前挨个摸一摸食材的“新鲜度”该动手时才动手。这个心理模型能帮你理解后面几乎所有规则。什么时候重新编译什么时候跳过什么时候执行清理本质上都是“比较新旧”的问题。2. 从零手写Makefile新手最容易走的弯路2.1 最笨但能用的版本先别急着用网上那些几十行的“万能模板”咱们从最朴素、最呆的版本开始。假设项目里有两个源文件app: main.c hello.c gcc -o app main.c hello.c这个版本能编译能运行但它有两个隐藏问题。第一哪怕只改了hello.c一个文件make也会把两个源文件全部重新编译并链接。因为规则里写的是“app依赖main.c和hello.c”而make只知道目标app是否比这两个文件新并不知道main.o和hello.o之间谁该被单独重建。第二如果新增了一个foo.c你必须手动把它加到依赖列表里不然链接时会报undefined reference头大得很。最笨版本的意义不是让你用它而是让你看清Makefile的成长路径一开始靠“直接列文件”中间靠“变量和自动变量”最后靠“模式规则和自动依赖”。2.2 引入变量和自动变量告别重复劳动C程序员写代码都知道用宏或者常量Makefile里一样有“全局配置”。常用的变量包括CC编译器默认是cc一般写成gcc或clang。CFLAGS编译选项比如-Wall -Wextra -O2 -g。LDFLAGS链接选项比如需要链接数学库时加-lm。OBJS目标文件列表。TARGET最后生成的可执行文件名。写进Makefile后长这样TARGET : app CC : gcc CFLAGS : -Wall -Wextra -O2 -g OBJS : main.o hello.o $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $ $^ main.o: main.c hello.h $(CC) $(CFLAGS) -c main.c -o main.o hello.o: hello.c hello.h $(CC) $(CFLAGS) -c hello.c -o hello.o这里出现了$和$^两个自动变量。$代表当前目标名也就是main.o或app$^代表这条规则的所有依赖也就是冒号后面的那串文件。自动变量的意义是让规则尽量“少写具体名字”改起来只用改上面的变量定义。你还会发现我用了:而不是。:是立即展开定义时就把右边内容算好是递归展开使用时才展开。写普通变量时两者差别不大但涉及wildcard这类函数时:更可控遇到循环引用也不会把自己绕死。2.3 模式规则登场二十个文件也不怕上面的写法还有一个问题每增加一个.c文件你就得多写一条“xxx.o: xxx.c xxx.h”规则。如果项目有二十个文件光复制粘贴就够烦了。模式规则就是用来干掉这种重复的OBJS : main.o hello.o $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $ $^ %.o: %.c $(CC) $(CFLAGS) -c $ -o $%.o: %.c表示任何.o文件都去找对应的.c文件。假设make需要生成hello.o它看到这条模式规则就会去检查hello.c是否存在存在就用后面的配方编译。$在这里代表第一个依赖也就是对应的.c文件。配合make提供的wildcard函数甚至可以让Makefile自动收集当前目录下所有C源文件SRCS : $(wildcard *.c) OBJS : $(SRCS:.c.o)第一行把*.c匹配到的所有文件名存进SRCS第二行利用后缀替换语法把列表里的.c全部换成.o。这样以后在目录里新建一个foo.c重新跑make它会自动加入构建流程你一行都不用改。不过这条“自动收集”有个要注意的坑如果你在空目录里跑makeSRCS和OBJS会变成空列表最后链接命令变成“gcc -o app”必然报错。真实项目里通常会有至少一个源文件所以问题不大但心里得有数。3. 核心实操从源码到可执行文件的自动化流程3.1 一个能跑起来的示例项目废话不多说直接建一个练手项目。目录里放三个文件hello.h声明一个hello函数。hello.c实现hello函数。main.c调用hello。文件内容大致是/* hello.h */ #ifndef HELLO_H #define HELLO_H void hello(void); #endif/* hello.c */ #include stdio.h #include hello.h void hello(void) { printf(Hello from Makefile!\n); }/* main.c */ #include hello.h int main(void) { hello(); return 0; }这个例子很小但足够把编译、链接、头文件依赖、清理这些环节全部串起来。3.2 写出带自动依赖的完整Makefile如果只写“够用版”上面的模式规则已经够了。但真实项目中藏着一个大坑只改头文件不重编。假设hello.h声明变了main.c和hello.c都必须重新编译。但你的Makefile里只有“main.o: main.c”“hello.o: hello.c”这种规则make根本不知道hello.h和.o文件之间有什么关系。你改了hello.h再跑make它看一眼main.o比main.c新理都不理你。这种“编译成功但没有生效”的坑非常隐蔽网上很多人骂“make不重新编译”其实不是make蠢是依赖没写全。解决办法是让编译器帮我们生成依赖文件。GCC有一个选项-MMD在编译时顺手生成一个.d文件里面的内容就是“main.o: main.c hello.h”这种规则。Makefile再用-include把这些.d文件吃进来依赖关系就自动齐全了。完整版本如下TARGET : app CC : gcc CFLAGS : -Wall -Wextra -O2 -g -MMD SRCS : $(wildcard *.c) OBJS : $(SRCS:.c.o) DEPS : $(OBJS:.o.d) $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $ $^ %.o: %.c $(CC) $(CFLAGS) -c $ -o $ -include $(DEPS) .PHONY: clean clean: rm -f $(TARGET) $(OBJS) $(DEPS)一行行拆开讲-MMD编译每个.c文件时生成对应的.d依赖文件但.d里不包含系统头文件依赖只包含项目自己的头文件。DEPS : $(OBJS:.o.d)把OBJS里的.o后缀替换成.d也就是算出所有依赖文件列表。-include $(DEPS)include前面加个减号意思是“如果文件不存在也别报警继续往下走”。第一次编译时.d文件还不存在全靠这个减号容错。之后每次编译.d内容被读进来make就明白了所有头文件依赖。这套写法是很多中型C项目的基础模板实测下来非常稳。你可以在命令行里试一下先正常make再touch hello.h然后make你会发现这次make老老实实地重新编译了main.o和hello.o而不是无动于衷。3.3 高频操作命令一览Makefile写好了常用的调用方式值得单独列一遍make # 构建默认目标也就是第一个目标 make clean # 执行清理 make -j4 # 用4个并行任务加速构建 make -n # 只打印将要执行的命令不真正执行 make CFLAGS-O0 -g # 命令行覆盖CFLAGS变量 make help # 如果Makefile里定义了help目标的话-j4并行构建非常香尤其是项目文件多的时候编译速度能翻几倍。但注意如果Makefile里的依赖关系写得不严谨并行构建很容易出现“文件还没生成就去找它”的诡异报错。遇到这种情况先用make单线程跑一次能过的话多半是缺依赖导致。命令行覆盖变量这个功能很实用我经常用它临时调试“make CFLAGS-O0 -g -fsanitizeaddress”跑完测试再恢复正常优化级别全程不用改文件。3.4 .PHONY与伪目标为什么clean必须特殊处理在完整版Makefile里clean前面专门写了一行.PHONY: clean。为什么要这么干因为clean不是真实存在的文件它只是一个“动作标签”。正常情况下make看到一个目标时会先检查磁盘上是否存在同名文件再比较时间戳。如果当前目录恰好有一个名叫clean的普通文件而你没有声明.PHONYmake会认为“clean已经存在并且挺新的”然后告诉你make: clean is up to date清理命令压根不执行。这是很多人第一次写clean时踩过的经典坑。.PHONY的作用就是告诉make“这个目标不是文件名别拿文件系统里的东西来比每次执行规则就好。”凡是这种“动作型”目标比如all、clean、install、test最好都加到.PHONY声明里。有多个伪目标时也能写在一行.PHONY: all clean install test4. 高频报错速查表尤其是“找不到makefile”之谜4.1 “make没有指明目标并且找不到makefile”的完整排查这句报错是许多人的第一个make阴影。它翻译过来其实是两件事叠加在一起make没能找到一个可用的Makefile同时你也没在命令行里指定目标名于是它不知道干什么。常见原因就下面这几种。第一种当前目录确实没有Makefile。这是最普遍的。make默认按顺序找三个文件名GNUmakefile、makefile、Makefile。Linux下文件名大小写敏感如果你创建的是MakeFile或者MAKEFILEmake是认不出来的。遇到这种情况先ls -la看看目录里到底有什么。如果文件名不对用mv MakeFile Makefile改回来。第二种你站在了错误的目录里。项目在/home/me/myproj你却在/home/me直接敲make它当然找不到。解决方法是cd回项目根目录。如果连项目根目录在哪都不确定可以用find . -maxdepth 3 -name Makefile搜一下。第三种Makefile存在但内容里没有“可构建的目标”。比如文件里只有一堆变量定义或者只写了模式规则没有写出第一个具体目标make照样不知从何下手。可以执行cat -n Makefile | head -30检查内容。只要有一个形如“app: ...”的规则或者用make app直接指定要构建的名字问题就解决了。第四种你用了非标准文件名又没告诉make。如果你把构建文件命名为build.mk那必须用make -f build.mk或者make -f build.mk app来调用。-f就是“file”的意思用来显式指定Makefile路径。还有一个容易忽略的点很多人在Windows下用VS Code的终端跑make结果发现系统里根本装的不是GNU Make而是别的同名命令。Linux下直接make通常没问题Windows下用MinGW或MSYS2环境时命令可能是mingw32-make输入前先make -v确认一下版本和路径。4.2 其他高频错误速查表除了那句“找不到makefile”平时遇到最多的报错还有这些整理成一张速查表方便你对照报错信息出现的典型场景真正原因解决方法Makefile:2: *** missing separator. Stop.把配方命令顶格写或用了空格缩进配方前必须用Tab字符make只认Tab在该行行首敲一个Tab不要用空格No rule to make target foo.o, needed by app. Stop.链接时找不到某个.o文件要么没有对应.c文件要么依赖里漏了生成规则检查foo.c是否存在于当前目录检查OBJS列表nothing to be done for all明明改了源码make却不干活依赖关系缺失比如头文件没进依赖使用-MMD生成.d依赖并-includeundefined reference to xxx链接阶段报符号找不到缺少库、缺少源文件或声明和实现不匹配在OBJS中补.o或加LDFLAGS -lxxxmultiple definition of xxx链接阶段报重复定义同一个函数在多个源文件里定义或重复链接检查全局变量/函数定义避免头文件定义变量*** file Makefile has modification time in the future系统时间不对或文件从别处拷贝文件时间戳比当前时间还晚make认为目标一直过时用touch Makefile更新时间或同步系统时间这些报错我全都在实际项目里遇到过。尤其是missing separator新手期几乎天天见。它背后的原理很简单make使用“Tab 缩进”来区分“配方命令”和“普通文本”这是1970年代就定下的老规矩一直没改过。很多现代编辑器默认用空格缩进如果你把Makefile代码从某个网页直接复制下来缩进悄悄变成空格make就会在那一行报missing separator。4.3 排查时最实用的几个手段遇到复杂依赖问题我常用的三招是make -n、make -d和$(info)。make -n就是“演习模式”它只打印每个目标准备执行的命令不真正执行。比如你想确认clean会删哪些文件先跑一次make -n clean心里有底了再真删。make -d是debug模式会输出大量细节包括make比较了哪些时间戳、哪个文件更新、哪个目标被跳过了。输出确实很啰嗦但当你百思不得其解“为什么这个文件没有重新编译”时它能给你答案。$(info ...)可以在Makefile里直接打印变量值。比如在OBJS : $(SRCS:.c.o)后面加一行$(info OBJS $(OBJS))执行make时就能看到OBJS到底变成了什么。这个方法排查变量为空、路径不对特别管用比自己猜快得多。5. 进阶但不困难自动生成依赖与Makefile的几种姿势5.1 用gcc自带能力生成头文件依赖前面提到的-MMD已经算自动生成依赖了不过你也可以单独执行命令查看依赖内容gcc -MM main.c hello.c输出结果大致是main.o: main.c hello.h hello.o: hello.c hello.h-MM表示“生成依赖列表但不编译并且不包含系统头文件”。如果你想连系统头文件一起列出来用-M而不是-MM。在写Makefile时-MM更常用因为你并不想让stdio.h的修改触发项目重编。理解了这套机制后你就明白为什么很多Makefile里的.d文件是一大坨“xxx.o: /usr/include/...”了那多半是用了-M生成的。实际工程里没必要把系统头文件纳入依赖用-MM或者-MMD更干净。5.2 用CMake、autotools间接生成Makefile现在很多项目已经不再直接手写Makefile而是用CMake生成。CMake本身并不是编译器也不是make的替代品它更像一个“编制说明书的人”读完后产出Makefile或者Ninja等构建文件然后你再执行make或ninja完成构建。一个极简的CMakeLists.txt只需要十几行cmake_minimum_required(VERSION 3.10) project(makefile_tutorial) add_executable(app main.c hello.c)然后在项目目录里执行mkdir build cd build cmake .. make你会看到CMake自动生成了一份密密麻麻的Makefile接着make正常执行。这里要提醒一句自动生成的Makefile虽然长但它内部依然严格遵守“目标-依赖-配方”这套规则。只不过变量和规则名全部由CMake按模式生成。那既然有CMake为什么还要手写Makefile我的看法是Makefile是“基础能力”CMake是“工程化工具”。你会用CMake不代表你应该不懂Makefile因为很多老牌开源仓库、嵌入式SDK、内核模块仍然直接使用Makefile邮件列表里也有大量关于Makefile的讨论。没有Makefile底子打开一个几百行的构建文件时真的会觉得无从下手。5.3 什么时候手写什么时候生成我个人的经验可以参考这个边界项目只有几个文件或者构建逻辑很特殊比如需要生成配置头文件、做代码打包、调用特定交叉编译工具链手写Makefile反而更清晰。它只有几十行写完自己心里门清。项目超过几百个文件有复杂的平台分支、测试目标、打包分发需求或者需要跨平台给其他工程师复用用CMake更合适。CMake能自动处理编译选项探测、安装路径、多平台兼容等问题。嵌入式交叉编译场景手写Makefile依然常见因为目标平台和工具链非常固定CMake虽然也能通过toolchain文件支持但学习成本更高。值得一提的是网上有些“自动生成makefile”的小工具和脚本本质就是往模板里填文件名和编译选项。这种工具适合临时应急但不建议作为长期方案。真要自动化优先选CMake这类有完整生态的方案而不是来路不明的脚本。最后分享一点个人心得体会写了这么多年Makefile我越来越觉得它不需要多华丽但必须“一眼能看懂”。我给自己定了几条规矩所有变量集中放在文件顶部每个目标上方加一行注释说明它是干嘛的清理目标一律声明.PHONY依赖关系交给编译器自动生成。做到这四点一个只有二十行的Makefile也能撑起一个中型C项目。还有个特别实用的小习惯偶尔跑一次make -n看看make打算重新编译哪些文件。如果发现某个文件不该重编却每次都重编多半是依赖写得太粗或者.d文件过期被保留了。这个习惯能帮你提前发现很多隐藏的构建问题比等到CI上挂了再排查省心得多。一直以来我的Makefile都是“长”出来的不是一次“背”出来的希望这篇精华版教程也能让你少走几步弯路。
阅读完成 · 觉得有帮助?