ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Makefile从入门到实战:增量编译、依赖管理与自动化构建核心解析

Makefile从入门到实战:增量编译、依赖管理与自动化构建核心解析 在Linux下折腾C/C项目的朋友迟早都会碰到Makefile这个文件。有人把它当成看不懂的天书有人只会敲make然后祈祷编译通过更多的人则是手动执行gcc命令一条命令一行代码改一个文件就要去翻历史记录找之前敲过的命令。这篇文章想聊聊我理解中的Makefile——它到底在解决什么问题应该怎么一步一步写出来以及那些真正踩过的坑。接下来的内容基于一个多文件C项目的完整实战从手工编译开始一步步把它“工厂化”。整个过程中我会解释最关键的设计思路和底层逻辑而不是给一份可以直接复制的模板就完事。目标是让看完这篇文章的你既能独立写出工程上能用的Makefile也能在未来看到别人的构建脚本时不再发怵。无论你是只写课堂作业的学生、刚接触Linux不久的新手还是在嵌入式板子上玩交叉编译的开发者这篇文章都适用。1. 内容整体设计与思路拆解1.1 手工小作坊的痛点到底在哪里想象你刚接手一个C语言项目文件不多就三四个。第一次编译你执行了gcc main.c calc.c utils.c -o app成功了。你改了一处代码又执行一次还是成功。到了第十次你开始觉得这样有点蠢于是按方向键上回车继续用。直到有一天项目加了一个模块你需要链接数学库gcc main.c calc.c utils.c sort.c -lm -o app命令变长了一点但还能忍。再后来你在嵌入式板子上做开发编译命令变成了arm-none-eabi-gcc -mcpucortex-a7 -I./include -I./bsp/include -DDEBUG_LEVEL2 main.c bsp.c uart.c timer.c sort.c -L./lib -lm -o app.bin这个长度已经超过了一家手工小作坊能承受的极限。但问题不只是命令太长还有几个更深的痛点。第一个痛点改了一个头文件你不知道哪些.c文件需要重新编译。头文件通常被多个源文件include比如calc.h可能同时被main.c和calc.c引用。你改了calc.h最稳妥的做法是把所有文件全部重新编译一遍。项目小的时候不觉得等源码量到了几十个文件一次全量编译吃掉你十几分钟这就是成本。第二个痛点编译参数全靠“人传人”。别人的项目换到你电脑上头文件路径、宏开关、链接库的位置全靠口头交代或者写在README里。环境稍微有点差异编译就挂。而这个问题恰恰是Makefile最擅长解决的——把编译规则固化进文件里谁拿到都能一键构建。第三个痛点条件编译和平台配置根本没法用手工命令维护。同一个项目要同时适配Ubuntu、ARM开发板、可能还要适配一个Darwin环境每条命令的参数都不一样手工敲必然乱套。1.2 Makefile是如何实现“自动化工厂”的Makefile本质上是一张“施工图”你告诉make最终产品是什么它由哪些零件组成每个零件又由哪些原料加工而来。make拿着这张图对比各文件的修改时间决定哪些零件需要重新加工哪些可以直接用仓库里的存货然后一条一条执行加工指令。生活化类比一下手工小作坊的老板接到订单自己去仓库翻料、自己车零件、自己组装订单一多就手忙脚乱。工厂流水线不一样每个工位只管自己那道工序物料一到就开工没有新物料到就不动弹最后统一组装出货。Makefile就是把“流水线规则”写下来make就是那个天天盯着时间戳、按规则推进的车间主任。这个类比基本就是Makefile的设计哲学。很多初学者会把注意力放在语法上纠结变量赋值方式、函数参数怎么写。我的建议是反过来先把“目标、依赖、时间戳”这三个词刻进脑子里。后面所有技巧——变量、函数、模式规则——本质上都是在为这三个词服务。1.3 make其实是一个“时间戳判断器”make的核心调度逻辑其实很简单对每一条规则先看目标文件是否存在如果存在就把目标文件和所有依赖文件的时间戳做比较只要任何一个依赖比目标新就执行规则里的命令重新生成目标否则跳过这条规则。这就是增量编译的原理。所以理解Makefile千万不要把它当成一门编程语言来学它更像一个带表达能力的资源调度器。它的语法只是为了让你把“目标—依赖—命令”三元组写得更紧凑、更易维护。后面我讲到的每个进阶写法都在往这个内核上靠不是为了炫技而是为了让“按需重建”这件事更灵活。顺带提醒一句make程序本身和Makefile不是一个东西。make是Unix上一个有几十年历史的构建工具Makefile是它的配置文件。GNU make是目前最常见的一个实现本文所有命令和语法都基于GNU make。2. 核心语法拆解先把地基打好2.1 目标、依赖、命令Makefile的最小骨架Makefile最基本的规则长这样target: prerequisites command注意command前面必须是一个tab字符这是新手最容易踩的坑后面我会专门讲。target是这条规则的目标通常是一个文件名比如appprerequisites是生成这个目标需要的原料可以是.c、.h也可以是其他目标文件command是真正生成目标的shell命令make会把它交给/bin/sh执行。举一个最简单但五脏俱全的例子app: main.o calc.o utils.o gcc -o app main.o calc.o utils.o main.o: main.c calc.h gcc -c main.c -o main.o calc.o: calc.c calc.h gcc -c calc.c -o calc.o utils.o: utils.c utils.h gcc -c utils.c -o utils.o clean: rm -f app *.o这个示例把每条生产链路都交代清楚了。你只需要敲一次make它会自动判断执行顺序目标app依赖三个.o文件而每个.o文件又依赖对应的.c和.h于是make会先去处理每条.o规则最后执行链接命令。我可以明确告诉你如果第一次接触Makefile你不需要看懂所有细节只要先抓住这个骨架里的对应关系就行最终产品是谁、中间产品是谁、原料是谁、怎么从原料加工成产品。后面所有灵活性都建立在这个基础上。这里特别提醒一下clean它不是一个真实存在的文件而是一个“伪目标”冒号右边没有依赖。如果你不声明.PHONY: clean一旦目录里恰好出现了一个名叫clean的文件make会发现clean已经存在且没有任何依赖需要更新于是rm命令永远不会执行。这就是工程里几乎都会把clean、install、test这类操作声明为.PHONY的原因。2.2 变量系统工程化改造的第一步真实工程里没有人会把编译器和参数一遍遍写进命令。把这些不变的东西抽成变量是Makefile工程化的第一课CC : gcc CFLAGS : -Wall -Wextra -O2 -g TARGET : app OBJS : main.o calc.o utils.o $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $ $^ %.o: %.c $(CC) $(CFLAGS) -c $ -o $ clean: rm -f $(TARGET) $(OBJS) .PHONY: clean这样改的好处是换编译器时只改CC这一处调试版本和发布版本可以通过覆盖CFLAGS来切换不同目标平台之间复制构建脚本的成本也大幅降低。但变量的赋值方式有讲究常用的四种、:、?、。最坑的是和:的区别。是递归展开式赋值变量在展开时才解析它的值。:是简单展开式赋值相当于立即求值。看这个例子A B C $(A)1 A D all: echo $(C)用时C最终展开为D1因为等到使用C时才现查A的值如果把改成:C在赋值瞬间就已经变成B1后面A怎么变都不影响C。在简单工程里很少出错但一旦你的Makefile开始include其他文件、通过命令行传变量、做多层级目录构建的递归展开时机就会带来各种诡异问题。我的建议是对于固定的配置值一律用:对于需要在多个目标之间共享的累加值用对于命令行允许覆盖的变量用?。2.3 自动变量与模式规则写一次复用一百次手动为每个文件写编译规则实在太啰嗦了。Makefile提供了一套自动变量来偷懒$ 表示当前规则的目标文件名$ 表示第一个依赖文件$^ 表示所有依赖文件已去重$* 表示去掉后缀的主文件名利用自动变量再配合%模式规则可以把每个.c文件的编译规则压缩成一行%.o: %.c $(CC) $(CFLAGS) -c $ -o $这条规则的意思是任何.o文件只要对应的.c存在就套用同一条编译命令。$自动替换成具体的.c文件$自动替换成具体的.o文件。有了这条上面main.o、calc.o、utils.o那三条独立规则就可以删掉了。这就是“写一次复用一百次”的含义是Makefile从手工走向自动化的标志性特征。这里一定要理解%和的区别。%是模式规则里的“通配符”它匹配的部分可以跨目录而是shell的通配符在Makefile里使用时要交给shell展开两者不要混用。我曾经见过有人在依赖列表里写$(wildcard *.c)又在规则头部写%.o: %.c结果两个功能交叉在一起构建行为变得非常难排查。2.4 常用函数wildcard、patsubst、notdir当源文件越来越多手动列出所有.c文件也是一件麻烦事。Makefile自带的文本函数可以自动收集SRCS : $(wildcard src/*.c) OBJS : $(patsubst %.c,%.o,$(SRCS))wildcard把src目录下所有.c文件展开成列表patsubst做模式替换把.c后缀改成.o。两个函数组合起来以后src目录里新增任何.c源文件Makefile一行都不用改。如果目录层次复杂还可以配合notdir提取文件名用dir获取目录名。这些函数本质上就是字符串处理工具。Makefile的函数返回值都是空格分隔的列表使用时经常需要和foreach、filter、sort等组合。工程越大这套“自动收集—自动替换”的价值越明显。以我的经验很多从CMake转过来的人会觉得Makefile的函数语法很难看比如$(subst a,b,$(VAR))这种写法。但实际用久了会发现它无非就是“在字符串上做替换、过滤、拼接”核心逻辑并不复杂。理解成“Makefile里的awk”就行。3. 从零构建一个正经项目多文件C工程Makefile实战3.1 案例背景一个不算玩具的C项目环境为了讲清楚细节我构造一个多文件项目结构如下project/ ├── include/ │ ├── calc.h │ └── utils.h ├── src/ │ ├── main.c │ ├── calc.c │ └── utils.c └── Makefilemain.c会调用calc和utils两个模块的功能calc.c依赖calc.hutils.c依赖utils.hmain.c同时依赖两个头文件。编译目标是生成一个名为app的可执行文件。需求这里列一下支持增量编译头文件变动时所有引用它的源文件都要重新编译支持clean能方便切换编译器和编译参数。这四条听起来很少但能同时做到的Makefile就已经具备真实工程的基础能力了。3.2 第一版Makefile先把构建跑通按我们前面讲的变量和模式规则第一版可以这样写CC : gcc CFLAGS : -Wall -Wextra -Iinclude TARGET : app OBJS : src/main.o src/calc.o src/utils.o $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $ $^ %.o: %.c $(CC) $(CFLAGS) -c $ -o $ clean: rm -f $(TARGET) $(OBJS) .PHONY: clean执行make输出大概是gcc -Wall -Wextra -Iinclude -c src/main.c -o src/main.o gcc -Wall -Wextra -Iinclude -c src/calc.c -o src/calc.o gcc -Wall -Wextra -Iinclude -c src/utils.c -o src/utils.o gcc -Wall -Wextra -Iinclude -o app src/main.o src/calc.o src/utils.o注意OBJS里我手动写了三个路径。其实到这里还不够“自动化”但只要这版能编出来说明规则骨架没毛病。接下来就可以做增量编译和头文件依赖了。3.3 增量编译与头文件依赖自动化的灵魂所在现在试一个场景把include/calc.h里的一个函数返回值从int改成void然后重新执行make。结果会是什么你会发现app被重新链接了甚至main.o、calc.o、utils.o也全部重新编译了。等等为什么改了calc.h连不直接引用它的utils.o也重编了答案很简单因为你使用了%.o: %.c模式规则make只知道每个.o依赖对应的.c并不知道.c里到底include了哪些头文件。所以当.h变化时make根本不该感知到。但你又确实看到了utils.o被重建这通常是因为你执行了make clean后重新make或者文件本身的mtime已经被改动。真正的问题在于make并没有自动管理头文件依赖的能力。如果你在Makefile里不写main.o对calc.h的依赖然后在保持main.c不变、只改calc.h的情况下运行make你会看到make说“make: app is up to date”——明明头文件改了可执行文件却不会更新。这正是“自动化工厂”和“半自动流水线”的分水岭。解决办法是利用gcc的一个隐藏功能-MMD -MP。编译时gcc会顺带生成一个.d文件里面记录了当前.c文件include过的所有头文件路径。然后Makefile用include指令把这些.d文件读进来make就拥有了完整的依赖信息。改过的Makefile这样写CC : gcc CFLAGS : -Wall -Wextra -Iinclude -MMD -MP SRCS : $(wildcard src/*.c) OBJS : $(SRCS:.c.o) DEPS : $(OBJS:.o.d) $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $ $^ %.o: %.c $(CC) $(CFLAGS) -c $ -o $ -include $(DEPS) clean: rm -f $(TARGET) $(OBJS) $(DEPS) .PHONY: clean编译一次之后src目录下会出现main.d、calc.d、utils.d。以main.d为例内容大概是src/main.o: src/main.c include/calc.h include/utils.hmake读到这条之后就等价于在依赖关系里补上了一行main.o依赖calc.h和utils.h。一旦这些头文件的时间戳比main.o新make就会重新编译main.o再触发链接。而且每个.d文件都是独立生成的互不干扰。这里还有个细节为什么是-include $(DEPS)而不是include $(DEPS)因为第一次构建时目录里还没有任何.d文件普通的include会直接报错“文件不存在”。前缀减号的意思是“读取失败也别抱怨”安静跳过就行。等第二次构建时.d文件已经存在依赖关系自然就被加载了。3.4 引入第三方库头文件路径、库路径和链接顺序真实项目很少只由自研代码组成大概率要链接zlib、ssl或者项目自己的静态库。这时候Makefile里需要区分三类参数头文件搜索路径用-I指定属于编译期参数写在CFLAGS里。库文件搜索路径用-L指定属于链接期参数写在LDFLAGS里。具体链接哪个库用-l指定写在LDLIBS里。一个带第三方库的链接规则示例CFLAGS : -Iinclude -Ithird_party/include -MMD -MP LDFLAGS : -Lthird_party/lib LDLIBS : -lssl -lcrypto -lm $(TARGET): $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) -o $ $^ $(LDLIBS)关于静态链接库有一个很多人栽过的坑库的位置必须在目标文件之后。比如gcc main.o -lm -o app通常没问题但如果你写成gcc -lm main.o -o app在部分较旧工具链上就可能出现“undefined reference to ‘sqrt’”这种怪事。因为链接器是从左往右扫描目标文件和库的先遇到-lm时它还不知道后面main.o需要数学库符号等它看到main.o再回头找时候已经晚了。所以我的习惯是把$(LDLIBS)放在命令行的最末尾永远不让它出现在$^前面。这一点经验是从一次凌晨排查bug中得来的教训写下来希望你不必再踩。3.5 交叉编译嵌入式平台上的Makefile到底要改哪里很多嵌入式开发者第一次接触Makefile就是在某一款开发板的SDK里。我手头做过一块以RV1106为核心的板子厂商给的BSP里就有大量Makefile模板。交叉编译和本机编译的区别其实就是工具链、头文件和库文件都换了一套。工具链通常体现为一个带前缀的编译器名比如arm-none-eabi-gcc、arm-linux-gnueabihf-gcc、aarch64-linux-gnu-gcc。前缀一换整个GCC家族都被替换。所以工程里应该把交叉编译器前缀抽成变量CROSS_COMPILE ? CC : $(CROSS_COMPILE)gcc CFLAGS : -Iinclude -MMD -MP LDFLAGS : -Llib -Wl,-rpath,lib LDLIBS : -lm # 使用原生工具链编译时不用传参 # 交叉编译时执行 # make CROSS_COMPILEarm-linux-gnueabihf- clean all这样设计的好处是本机调试用默认值交叉编译时直接在命令行传CROSS_COMPILE参数同一份Makefile不用维护两个副本。注意这里?的语义如果用户在命令行传入了CROSS_COMPILEmake会优先采用用户的值如果没有传才使用空默认值。嵌入式场景还有一个常见需求头文件路径往往会带上芯片厂商提供的库目录这时-I参数的数量会比较多编译警告选项可能也要收敛以免厂商代码刷出大量警告干扰判断。多调试几次你就能体会到把“可变的东西抽成变量”这句话的真实价值。4. 常见问题与排查技巧实录4.1 “没有指明目标并且找不到makefile”到底怎么回事完整的报错长这样make: *** No targets specified and no makefile found. Stop.这条报错几乎每个用make的人都会遇到。原因多数是以下几类第一文件名不对。make默认按GNUmakefile、makefile、Makefile这个顺序查找构建文件。如果你的文件叫build.mk直接敲make当然找不到。有人会问“Makefile和makefile有啥区别”答案是Linux系统里区别很大文件名区分大小写Makefile是惯例makefile也能用GNUmakefile则是GNU make特有的。我一般只写Makefile不用makefile避免跨系统时踩坑。第二你在错误的目录里执行了make。比如项目在/home/user/project你却在/home/user下敲make自然找不到。先pwd确认一下再看Makefile是不是就在当前目录。第三文件从Windows拷贝过来换行符变成了\r\n。make解析时会认为这一行不是合法规则出现missing separator之类报错。用file命令看文件类型如果显示CRLF line terminators把它转成LF即可。排查顺序我建议是先ls看文件名再file看换行符最后cat -n看行内容。三次内基本能找到问题。不要在没确认文件名的情况下直接怀疑Makefile语法写错。4.2 头文件路径和库路径报错-I、-L、-l的故事编译阶段最常见的报错是fatal error: calc.h: No such file or directory如果你在源文件里写的是#include calc.h编译器会按默认目录加-I指定的目录去找如果你写的是#include calc.h编译器还会先看当前文件所在目录。很多人把-I忘了或者路径写错成一个相对路径在make执行目录和编译命令工作目录不一致时就报遍了。快速解法给CFLAGS加上-Iinclude再检查路径前缀是否拼对了。如果头文件在third_party/include/openssl/下代码里写的是#include openssl/ssl.h那-I就应该指向third_party/include而不是更细的openssl目录。链接阶段的报错和编译阶段不同典型表现是cannot find -lssl这说明-L指定的路径里没有libssl.a或libssl.so。检查一下库文件是否真的在那个目录版本命名是否一致。这里有个小技巧运行make -n打印出实际要执行的命令再把这条命令里的—L参数换成ls手动去看很多问题一眼就能定位。4.3 我改了代码make却告诉我一切正常这种情况比报错更让人抓狂。原因基本是时间戳判断出了问题至少我遇到过的场景都逃不出这几类。最常见的改动了一个头文件但Makefile里压根没有这个头文件参与任何依赖规则。这就是我们在3.3里反复强调的“用-MMD -MP自动生成.d文件”如果Makefile里没有依赖信息make当然认为app不需要更新。第二类代码文件确实改了但修改后的时间戳比目标文件还旧。这种情况在从压缩包解压、用scp复制文件时经常出现——复制过来的文件mtime是当前时间比你编译出来的二进制还新所以make会跳过重建。手动执行touch src/main.c把mtime刷新一下问题就解决了。第三类你用了并行编译make -j16但.d文件的生成和读取有一定的先后顺序问题。第一遍编译时.d还不存在不管构建顺序如何都可能漏掉依赖。建议第一次构建不要带-j等.d生成之后再并行。排查这类问题的通用办法是执行make --debugbasic它会打印每一次规则判断结果。看到一个文件因为“up to date”被跳过你就知道是时间戳比对了。看到“Prerequisite xxx is newer”你就知道是谁触发了重建。搞清楚这两行一半的“为什么不重编”问题都能解开。4.4 tab键还是空格键一个让人崩溃的“隐形字符”新手最常见也最崩溃的报错是这个Makefile:2: *** missing separator. Stop.原因很简单你的编辑器把tab键自动转换成了4个空格。make规定每条命令必须以tab字符开头而空格不是tab。报错信息里的“missing separator”说的就是这个分隔符没了。处理方式很简单在vim里执行:set noexpandtab然后确认当前行确实以tab开头如果你用VS Code在右下角把缩进类型从Spaces切换成Tabs。用cat -A Makefile看一下tab会显示为^I空格则显示为行首空格。一眼就能区分。这里我要说一句大实话因为没有把tab和空格区分开而崩溃的下午我经历过不只一次。现在我的习惯是一进新项目第一件事就是打开编辑器配置检查是不是“expandtab”免得写两行就语法错误。4.5 高频问题速查表报错 / 现象常见原因快速处理No targets specified and no makefile found文件名不对、目录不对、换行符异常ls查看文件名file查换行符cd到项目根目录missing separator命令前用了空格而不是tabcat -A确认tab改编辑器缩进设置fatal error: xxx.h: No such file or directory头文件路径未用-I指定检查-I路径确认include写法cannot find -lxxx库路径未用-L指定或库文件不存在检查-L路径确认库文件名和-l参数Undefined reference to ...链接顺序问题库放在目标文件之前把LDLIBS放到命令最后make说up to date但代码改了缺少头文件依赖或mtime异常加-MMD -MP生成.dtouch源码刷新时间戳Multiple target patternsMakefile里%和*混用确认模式规则中%的位置不要和wildcard混用这张表是我平时排查构建脚本问题时最常对照的思维导图整理出来希望对你有用。每个问题其实都不复杂复杂的是把它们串联起来之后的整条构建链路。5. Makefile和CMake到底先学哪个5.1 换一个角度理解现代构建体系打开搜索引擎输入“cmake和makefile区别”跳出来的讨论通常都旗帜鲜明地站队。但我想换个角度说Makefile和CMake根本不是同一个层次的东西。Makefile是面向文件依赖的构建脚本它直接把规则写给make执行。CMake则是一个更高层的“工程描述语言”它不直接构建而是根据你的描述生成对应构建系统需要的文件。最常见的生成目标就是Makefile在Linux上CMake生成的就是一套Makefile。换句话说你用CMake底层跑的还是make逻辑。这样理解之后就不会纠结“学了Makefile是不是白学”这种问题了。学Makefile是在学习构建系统的核心心智模型学CMake是在学习一种更高效的工程描述语法。两者不冲突反而是递进关系。5.2 实际选型什么场景用哪个更顺完全抛开“谁好谁坏”的争论聊聊我的选型标准单个子项目、中小规模代码、核心目标明确直接用Makefile。比如我在个人的工具库项目里就是一份200行不到的Makefile简洁直接改起来快效率极高。需要跨平台支持、自动检测系统库、生成安装包、管理多个子目录用CMake更合适。CMake在Windows上还能转成Visual Studio工程在macOS上能转Xcode工程这是原生Makefile做不到的。嵌入式裸机项目的BSP包多数直接提供Makefile模板或者至少是Makefile风格。你拿到一块开发板第一步大概率是打开官方SDK里的Makefile看变量怎么配所以不懂Makefile在这里完全走不动。大型开源项目现在主流是CMake但它的构建日志里还是会大量出现gcc命令。遇到链接问题你还是得回到CC、CFLAGS、LDFLAGS、LDLIBS这套变量上来思考。扎实的Makefile功底能让你在排查这类问题时更快锁死根因。结论很简单不要跳过Makefile直接上CMake。先把“目标—依赖—时间戳”这个模型吃透再去看CMake的高层描述认知成本会低非常多。5.3 一个CMake对照小例子一眼看出差别同样上面这个calc项目用CMake描述只需要几行cmake_minimum_required(VERSION 3.10) project(calc C) add_executable(app src/main.c src/calc.c src/utils.c ) target_include_directories(app PRIVATE include)在命令行执行cmake -S . -B build然后cmake --build buildCMake会自己生成Makefile并调用make完成编译。头文件依赖的.d文件生成CMake内部处理掉了多目标并行CMake帮你组合了跨平台配置CMake有全套语法。那是不是说CMake完胜也未必。CMake生成的Makefile通常非常长可读性差调试起来反而很难直接改。你做一个20行的测试工程CMake那套目录结构可能比你的源码还大。这就是为什么我一直建议个人小项目直接Makefile多人协作、需要发布到不同平台的大工程再上CMake。另外再提一个容易混淆的点CMake生成的目标文件可能是Makefile也可能是Ninja的.build文件取决于你配置的生成器。Ninja的定位和make类似都是一个执行构建的“车间主任”只是它设计得更现代、并行效率更高。理解了这一层你会对所有构建工具有一个更清晰的坐标感。最后分享一个我自己的习惯拿到任何一个新的工程构建出错时第一件事不是改代码而是运行make --debugbasic看看它的判断逻辑再用make -n看看实际会执行的命令。这两条命令是排查构建问题最好的两个侦察兵。把Makefile跑通之后一定要留一个习惯就是每次执行clean之前想一下这层增量构建关系别让习惯性的rm -rf把所有时间戳信息都清光。构建系统这门手艺说到底就是“把规则讲清楚让机器帮你按需干活”。你愿意多花一点时间把Makefile写利索后面省下的时间会远超你的投入。
返回列表