☰
从零掌握Makefile:依赖规则、增量构建与多目录实战避坑指南
2026/10/19 3:50:23 网站建设 项目流程

第一次接触Makefile的时候,我其实是拒绝的。当时手头的模拟项目X已经开始膨胀,二十几个源文件分了好几个目录,每次改完代码,我都是靠一条手打的gcc命令去重新编译,直到某次不小心把路径写错、把旧的.o文件和新的头文件混在一起,调了半天才意识到,这种“裸编译”的流程早晚要出事。后来硬着头皮把Makefile系统地过了一遍,回头再看才发现,这玩意儿并没有传说中那么玄:核心逻辑一句话就能讲清楚——告诉make工具“什么东西需要生成、它依赖哪些文件、用什么命令生成”,剩下的事情,make会根据文件的时间戳帮你决定哪些命令需要重新执行。

这篇内容就是我边做边记的Makefile理论与实践学习记录,从最基本的规则语法讲到多目录实战,最后梳理了一批我实际踩过的坑和排查方法。不管你是刚写完第一个多文件C程序的新手,还是在嵌入式或者后端项目里被构建流程折磨过的人,这篇文章应该都能让你少折腾几天。

1. Makefile到底解决了什么问题

1.1 从一次“手动编译”说起

想象一个非常普通的场景:你的项目里有main.c、utils.c、parser.c三个源文件,分别依赖对应的头文件。新手阶段最直接的做法是:

gcc -o app main.c utils.c parser.c -Iinclude

这条命令没有任何问题,但当文件数量变成三十个、五十个,每次编译都要敲一长串文件名,而且其中大部分文件根本没有改动,全部重新编译一遍就是在浪费时间。一个中型C项目的全量编译可能耗时几十秒到几分钟,如果只改了一个源文件,理想情况下只需要重新编译那一个文件,再链接一次就好。

Makefile就是为这个“理想情况”而生的。它本质上是一份给make工具看的“说明书”,里面一条条规则说明了:

  • 我想生成什么(目标文件)
  • 生成它需要哪些东西(依赖文件)
  • 一旦依赖变了,用什么命令生成它(命令/配方)

make工具读了这份说明书之后,会自己去比较目标文件和依赖文件的修改时间。如果某个依赖比目标更新,就说明目标已经过期了,需要重新执行对应的命令。这套“时间戳驱动”的增量构建机制,是Makefile所有复杂语法背后最核心的引擎。

1.2 一套规则就足够理解核心

Makefile里最基本的单元是“规则”,写法非常直白:

target: prerequisites recipe

意思是:要生成target这个文件,先得有prerequisites这些文件;当prerequisites中有任何文件比target新时,就执行recipe里的命令。

举个例子:

app: main.o utils.o gcc -o app main.o utils.o main.o: main.c main.h utils.h gcc -c main.c -o main.o utils.o: utils.c utils.h gcc -c utils.c -o utils.o

当你执行make app时,make会先检查app是否依赖了main.o和utils.o;如果这两个.o不存在,它就去看看有没有能生成它们的规则;找到了就先生成它们,最后再执行gcc -o app ...。如果你只改了main.c,make会发现main.o依赖的main.c变了,于是重建main.o,但utils.o没有变化,不需要重新编译,直接跳过。

这其实就是Makefile最大的价值:只做必须做的事,跳过没变的东西。理解了这个设计动机,后面所有语法都不难记。

1.3 为什么不直接用Shell脚本

很多人会问,既然Makefile也是一条条命令,那写个Shell脚本不也行吗?Shell脚本当然可以完成构建,但它缺少make的两个核心能力:

一是增量判断。Shell脚本只会从头到尾按顺序执行,你要自己做一堆文件时间戳比较、条件判断,才能实现“跳过没变的文件”。Makefile把这种判断内建了,你只需要声明依赖关系,它自己会判断。

二是并行调度。make -j4可以同时跑多个编译任务,但前提是它知道任务之间的依赖关系,不会出现“还在编译A的时候就去链接B”的竞态。Shell脚本自己写并行调度,难度完全是另一个量级。

当然,现代项目里也有CMake、Ninja、Bazel这些更上层的构建系统,但很多底层库、嵌入式工程、Linux内核模块、老牌开源项目,依然直接使用Makefile。学会Makefile,等于拿到了理解这些项目构建逻辑的钥匙。

2. 语法地基:变量、函数、规则一次讲透

2.1 规则与那个要命的Tab字符

这是所有Makefile新手第一个拦路虎。规则里的“配方”(recipe)每一行的开头,必须是一个Tab字符,不能是空格。你换成四个空格,make会直接报错:

Makefile:2: *** missing separator. Stop.

所以“missing separator”的意思多半就是:你该用Tab的地方用成了空格。这不是make不讲道理,而是它的语法就是这么定义的。我的习惯是:在编辑器里把Tab键显示出来,或者干脆在写完Makefile后执行一次cat -A Makefile,Tab在输出里显示为^I,空格则原样可见,一眼就能排查。

规则本身可以有很多变体:一个目标可以有多条规则(内容会合并)、没有配方只有依赖的规则、多目标共用一套配方等等。但核心永远是“目标: 依赖 + 配方”的三段式。

2.2 变量:四种赋值方式别搞混

Makefile里的变量用起来很像C语言里的宏,引用方式是$(变量名)。定义一个变量很简单:

CC = gcc CFLAGS = -Wall -O2

但真正让人踩坑的是赋值符号的区别:

赋值符号含义展开时机
=递归展开赋值变量被引用时才进行展开,可能产生“延迟求值”
:=立即展开赋值定义时右侧立即计算,结果固定
?=条件赋值只有当变量未被定义时才赋值
+=追加赋值在原有值后面追加内容

=和:=的差异最容易被忽略。看个例子:

A = hello B = $(A) world A = hi

如果使用=,最后B的值是hi world,因为在引用B时才去展开$(A),而那时A已经被改成hi了。如果使用:=,B会立即展开成hello world,之后A再变也影响不到它。

我在实战中的经验是:依赖文件列表、源文件列表这类希望“定死”的值,尽量用:=;只有在确有必要利用“延迟求值”时才用=。这样生成的Makefile行为更可预测,排查问题时也不必追着变量的“当前值”和“引用值”绕来绕去。

2.3 自动变量:写规则的偷懒利器

每次在配方里重复写目标名、依赖名不仅繁琐,还容易出错。make内置了一组“自动变量”,在配方里可以直接使用:

  • $@:当前规则的目标文件名
  • $^:当前规则的所有依赖文件列表(去重)
  • $<:当前规则的第一个依赖文件
  • $?:比目标更新的一组依赖文件
  • $*:模式规则中匹配到的那部分“主干”名称

用自动变量重写上面的例子:

app: main.o utils.o gcc -o $@ $^ %.o: %.c gcc -c $< -o $@

这样以后就算目标列表再多,配方也不用改。$@和$^一定是我写任何规则的“默认开头模板”。

这里有个小坑:自动变量只在配方上下文中有效,如果你在普通的“变量名 = 值”的地方写$@,它会被当作$加一个字符处理,结果完全不是你想的那样。所以自动变量不要拿到规则外面去用。

2.4 常用内置函数:wildcard、patsubst、shell

Makefile本身是一门“迷你语言”,内置函数可以帮你动态获取文件列表、做字符串处理。最常用的几个:

wildcard:按通配符模式获取文件列表。

SRCS := $(wildcard src/*.c)

这条命令会把src目录下所有.c文件展开成完整列表。不加wildcard,src/*.c在Makefile里就是一个朴素的字符串,不会自动做文件名展开——这是新手特别容易踩的点。

patsubst:模式化字符串替换。

OBJS := $(patsubst %.c,%.o,$(SRCS))

把SRCS里所有.c后缀的文件名都换成.o。等价写法是OBJS := $(SRCS:.c=.o),后者短一些,含义相同。这一步的意义在于:源文件列表和目标文件列表往往是一一对应的,我们只需要维护“源文件列表”这一个变量,后面自动推导出中间产物。

notdir:去掉路径前缀,只保留文件名。

$(notdir src/main.c) # 结果:main.c

shell:执行一条Shell命令并把输出结果作为变量值。

DATE := $(shell date +%Y%m%d)

用shell函数时要小心性能问题:它会在Makefile解析阶段执行,如果命令很耗时,每次解析makefile都会被拖慢。

foreach:遍历列表,对每一项做处理。

DIRS := a b c PATHS := $(foreach dir,$(DIRS),build/$(dir))

这三个函数配合起来,足以应付90%的路径处理需求。

2.5 .PHONY:声明假目标

如果一个目标不对应任何实际文件,它就叫“假目标”。最经典的例子是clean:

clean: rm -f *.o app

问题在于:如果当前目录下恰好有一个名为clean的文件,make会认为clean已经是“最新”的,什么都不做。你执行make clean,得到一句“make: 'clean' is up to date.”,气得人血压升高。

解决办法就是声明它是假目标:

.PHONY: clean clean: rm -f *.o app

.PHONY告诉make:这个目标不要检查文件是否存在,也不要比较时间戳,每次执行make clean都老老实实跑配方。我习惯把所有“动作类”目标(clean、install、test、distclean)全部放进.PHONY,宁多勿漏。

3. 从零开始:一步步写出工程级Makefile

3.1 模拟项目X的场景设定

为了不让语法停留在纸面上,我们构造一个完整的模拟项目X。目录结构如下:

project_x/ ├── Makefile ├── include/ │ └── utils.h └── src/ ├── main.c ├── utils.c └── parser.c

目标很朴素:生成一个可执行文件build/app,同时支持一次全部重编、清理、自动生成头文件依赖。我故意把头文件(.h)和源文件(.c)分放在不同目录,因为路径处理是很多人一开始就绕晕的地方。

3.2 第一版:先有能跑的

第一版先不考虑简洁,把每个文件的规则老老实实列出来:

TARGET = build/app $(TARGET): main.o utils.o parser.o gcc -o $(TARGET) main.o utils.o parser.o @mkdir -p build main.o: src/main.c include/utils.h gcc -c src/main.c -o main.o utils.o: src/utils.c include/utils.h gcc -c src/utils.c -o utils.o parser.o: src/parser.c include/utils.h gcc -c src/parser.c -o parser.o

这个版本能跑,但问题很明显:每新增一个.c文件,就要手写一条新规则;头文件依赖靠人肉维护;对象文件全部散落在根目录;mkdir -p build放在配方里也只是临时补救。但它有教学价值:规则就是这么简单,复杂化都是在“减少重复”这条路上自然长出来的。

3.3 第二版:引入变量和自动变量

手动维护文件列表非常痛苦,先用变量把“变化的部分”抽出来:

CC = gcc CFLAGS = -Wall -Wextra -Iinclude TARGET := build/app SRCS := $(wildcard src/*.c) OBJS := $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SRCS))) BUILD_DIR := build

这一步的写法值得解释一下。wildcard负责把src/*.c展开成真实文件列表,notdir把src/main.c变成main.c,patsubst再把它转换成build/main.o。这样我只维护SRCS一个变量,所有中间对象文件路径都自动推导出来。

再把规则改成自动变量:

$(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $@ $^ $(BUILD_DIR)/%.o: src/%.c $(CC) $(CFLAGS) -c $< -o $@

$(BUILD_DIR)/%.o: src/%.c是一条模式规则,表示“任何build/xxx.o都可以由src/xxx.c生成”。这样就不需要为每个源文件写单独规则了。新增文件时,只要文件是.c后缀、在src目录下,就自动纳入构建。

这个阶段,Makefile已经从“能跑”变成了“基本能用”。

3.4 自动生成依赖:把头文件关系交给编译器

第二版还藏着一个致命问题:对象文件规则里没有声明头文件依赖。如果include/utils.h变了,但src/utils.c没变,build/utils.o不会重建。于是链接时就会用旧对象文件,出现极度令人困惑的“我改了头文件,但运行结果没变化”的情况。

手动维护build/utils.o: src/utils.c include/utils.h不现实,因为头文件可能再包含别的头文件,依赖树的规模会失控。正确的做法是让编译器自己生成依赖文件:

GCC和Clang都支持-MMD和-MP选项:

  • -MMD:在编译源文件的同时生成一个.d文件,里面写好了这个.o依赖哪些文件;
  • -MP:为每个依赖的头文件生成一个空目标,避免头文件被删除后make因找不到目标而报错。

具体操作是:修改编译规则,在配方里生成.d文件,然后在Makefile末尾把它include进来:

DEP_FILES := $(OBJS:.o=.d) $(BUILD_DIR)/%.o: src/%.c $(CC) $(CFLAGS) -MMD -MP -c $< -o $@ -include $(DEP_FILES)

注意-include前面的减号:它的意思是“如果这些文件不存在,也不要报错”。第一次全量编译时.d文件还没生成,如果没有这个减号,make会在解析阶段就抛错退出。这是整个“自动依赖”方案里最容易翻车的地方。

有了.d文件后,头文件的任何变化都会被make感知到,增量编译才是真正可靠的。这一步做完,这个Makefile就具备了工程级构建系统的核心能力。

3.5 完整示例:一套可以直接套用的模板

综合上面所有改进,我给模拟项目X整理了一份完整模板,平时自己新开项目时也基本是这个结构:

# ========= 配置区 ========= CC := gcc CFLAGS := -Wall -Wextra -O2 -Iinclude LDFLAGS := BUILD_DIR := build SRC_DIR := src TARGET := build/app # ========= 文件列表 ========= SRCS := $(wildcard $(SRC_DIR)/*.c) OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS)) DEPS := $(OBJS:.o=.d) # ========= 默认目标 ========= all: $(TARGET) $(TARGET): $(OBJS) @mkdir -p $(BUILD_DIR) # 确保目标目录存在 $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ # ========= 模式规则 ========= $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c @mkdir -p $(BUILD_DIR) $(CC) $(CFLAGS) -MMD -MP -c $< -o $@ # ========= 依赖文件 ========= -include $(DEPS) # ========= 其他目标 ========= .PHONY: all clean distclean test clean: rm -rf $(BUILD_DIR) distclean: clean rm -f *.log test: $(TARGET) ./$(TARGET) --test

我说几个这段内容里的细节:

@mkdir -p前面的@表示执行命令时不要打印命令本身,否则每次make都会吐一行mkdir -p build,输出会很啰嗦。all作为第一个目标,是make在没有指定目标时的默认选择。test依赖$(TARGET),意味着测试前会自动重新构建,避免了“忘了编译就去跑测试”的尴尬。clean直接删整个build目录,省去维护一长串中间文件的烦恼,代价是下次构建全量编译,这在绝大多数开发场景里都是划算的。

4. 多目录、递归make与并行构建

4.1 单Makefile还是递归Makefile

当项目继续长大,src下面按模块拆成了好几个子目录,比如src/net、src/cli、src/fs,这时面临一个架构选择:

单Makefile方案:继续在一个Makefile里用wildcard扫描所有子目录的源文件。它的优点是依赖关系全局可见、并行构建安全、变量集中管理;缺点是当模块边界非常清晰,每个目录都由不同人长期维护时,一个巨大的Makefile修改起来容易互相影响。

递归Makefile方案:每个子目录放一个自己的Makefile,顶层Makefile用$(MAKE) -C src/net这种方式去逐个调用。优点是模块自治、各目录构建逻辑隔离;缺点是跨目录依赖(比如A模块的头文件生成后B模块才能编译)很难表达,并行构建时尤其容易出竞态。

业界对递归make早有争议,“递归make被视为有害”是一篇流传很广的文章。我的建议是:除非你有强理由(比如各模块完全独立、由不同团队维护、甚至使用不同编译器),否则优先单Makefile。我自己在模拟项目X这种规模的项目里,单Makefile完全够用,而且省了很多跨目录沟通成本。

4.2 如果非要递归make,写法要注意什么

假设你决定使用子Makefile,顶层Makefile常见写法是:

SUBDIRS := src/net src/cli all: @for d in $(SUBDIRS); do \ $(MAKE) -C $$d; \ done

注意循环里用的是$(MAKE)而不是make,这样make的-j等命令行参数、以及顶层设置的变量,才能正确传递到子make进程。如果直接写make,遇到make -j8时子进程不会继承并行参数,整个构建反倒变成串行,性能反而下降。

子Makefile里的变量如果需要被父Makefile覆盖,建议用$(MAKE) -C $d VAR=value显式传入,或者把变量放进export列表。递归Makefile最坑的地方就是“变量传不传、传给谁”的隐式行为,调试起来特别费神。

4.3 并行构建:-j的真正威力与隐藏风险

make -j$(nproc)可以大幅缩短构建时间,但它依赖一个前提:Makefile里的依赖关系必须完整正确。make只会根据规则声明的依赖来决定调度顺序,它并不能“看懂”你的配方在做什么。

一个极端例子:

all: a b a: echo hello > a b: cat a

这条规则完全没有声明b依赖a。串行执行没问题,但用make -j2时,make可能同时启动两个配方,b去读a时文件还不存在,构建直接失败。这类问题在复杂项目里的表现是“偶现失败、同一个命令有时成功有时失败”,极难排查。

如果某条规则确实需要一个文件“已经存在”,但又不想因为它的时间戳变化就触发自己的重建,可以用order-only依赖,语法是管道符|:

output.txt: input.txt | configs/ generate --config configs/ > $@

configs/目录不是“内容依赖”,只是“顺序依赖”:目录存在或者比目标旧不会导致重建,但make会保证在生成目标前先准备好目录。这个语法在需要先建目录、先生成头文件、再编译的场景里特别实用。

4.4 条件判断与常用目标规范

Makefile里也有条件指令,最常见的是ifeq、ifneq、ifdef:

ifeq ($(BUILD_TYPE),debug) CFLAGS += -g -O0 else CFLAGS += -O2 endif

用变量覆盖机制,命令行执行:

make BUILD_TYPE=debug

就能快速切换构建类型,不用改文件。这也解释了为什么很多开源项目会约定CFLAGS、prefix、DESTDIR这类变量——它们的设计初衷就是允许用户在命令行覆盖默认配置。

另外,一套成熟的Makefile通常会约定这些目标:

目标作用
all默认构建入口
clean清理中间文件
distclean清理所有生成产物
install安装到系统路径
test运行测试
uninstall卸载安装的产物

我写install时会配合变量PREFIX ?= /usr/local,允许用户通过make install PREFIX=$HOME/.local自定义安装位置。这种约定俗成的接口,让我的Makefile可以被任何熟悉make的人直接使用。

5. 常见问题排查与避坑实录

5.1 高频报错速查表

以下是我实践中被问过最多、也自己踩过最多的Makefile报错,整理成一张速查表:

报错信息真正原因解法
missing separator. Stop.配方行用了空格而不是Tab;或变量赋值写成但少了冒号检查对应行首字符,统一换Tab
No rule to make target 'xxx.o'没有能生成该文件的规则,或源文件路径不对用make -p查看make数据库,确认规则存在;检查通配符是否被正确展开
recipe commences before first target文件开头出现非规则内容,最常见是Tab开头/变量定义前有多余字符检查第一行是否有乱字符
No targets specified and no makefile found当前目录下根本没有Makefile,或文件不是默认名显式执行make -f 你的文件
multiple target patterns模式规则里出现了多个%一条模式规则只允许一个%
*** missing separator与提示夹杂乱码多半是Windows下编辑导致的CRLF行尾问题在编辑器里把换行符设为LF,或执行sed -i 's/\r$//' Makefile

第三条值得多说一句:如果你用Windows编辑器编辑过Makefile,最好检查一下换行符。make对\r这种回车符非常敏感,它会混进行尾导致各种幽灵般的报错。我处理这类问题的统一操作是:

file Makefile cat -A Makefile | head -20

file能看到行尾格式,cat -A能显示所有隐藏字符,Tab显示为^I、行尾的CR显示为^M,一看便知。

5.2 变量调试:让make把“心里话”说出来

Makefile的变量展开经常让人意外,因为你看到的变量值未必是它被展开后的真实值。排查技巧是用信息函数直接把变量打印出来:

$(info SRC_DIR=$(SRC_DIR)) $(info SRCS=$(SRCS)) $(info OBJS=$(OBJS))

$(info ...)会在Makefile解析阶段就打印内容,比在配方里写echo更早暴露问题。如果某个变量的值不符合预期,顺着它的赋值链往回查即可。

更进一步,make -n可以“演练”而不真正执行命令,专门用来确认make到底打算跑哪些命令。配合make --debug=能打印目标解析、依赖检查、更新决策的详细过程,只是输出比较啰嗦,适合在小项目上做“解剖实验”。

还有两个不那么常用但特别有效的调试技巧:

$(warning 开始解析到此处,TARGET=$(TARGET)) $(error 这里主动中断构建)

$(warning)会在标准错误输出打印,不会被重定向丢掉;$(error)则直接停止解析,适合在写条件分支时强制验证“代码走到哪条路径”。我调过一份变量死活不对的Makefile,就是靠在几个分支里插入$(error)才确定走了哪个ifeq分支——这个方法比纯看代码快多了。

5.3 依赖缺失、时序与并行:那些“偶发”的坑

有一个场景我印象特别深:某次给一个多模块项目加-j8并行编译,偶尔会冒出奇怪的“头文件找不到”,单独重跑一次又好了。当时排查了很久,最后发现是从属模块的头文件生成规则没有写入依赖关系,导致并行时部分模块尚未生成头文件,而依赖它的模块已经开始编译。这类问题单靠加锁或者“碰运气”根本解决不了,最终还是要补全依赖声明。建议大家在排查阶段按这个顺序走:

  1. 先关掉并行:make -j1,如果问题不再出现,说明是依赖或时序问题;
  2. 再执行make -n查看命令执行顺序,检查逻辑漏洞;
  3. 最后审视依赖声明是否完整,必要时引入order-only依赖来保证“先建目录再编译”这类顺序。

如果“偶现”问题在串行下也出现,那多半不是时序,而是某个文件被外部修改、或者环境变量残留。此时需要清环境、从头全量构建验证结论。

5.4 跨平台与不同make版本

很多人会默认所有Linux/Unix系统上的make都一样,其实不然。Linux上通常是GNU Make,而macOS系统自带的make是BSD Make的变体,两者在函数支持、内置变量、行为细节上有差异。一个典型的例子是$(shell ...)函数的兼容性、--output-sync等参数在旧版本上的支持情况。

我自己的做法是:在一个统一的基础镜像或CI环境里固定make版本,如果必须在不同机器上手工构建,就在项目文档里注明推荐GNU Make版本,或者干脆用gmake命令名。这样至少能保证本地和CI行为一致,减少“在我机器上明明可以”这类问题的出现概率。

5.5 三条减少Makefile报错的日常习惯

最后分享几个我从大量踩坑中沉淀下来的习惯,谈不上高端,但确实有效:

第一,Makefile里的路径尽量从同一个变量出发推导,例如从SRC_DIR推SRCS、从SRCS推OBJS、再从OBJS推DEPS。不要在不同位置硬编码路径,否则改一个目录结构,就要全局搜索替换,极易漏改。

第二,把“死命令”和“活参数”分开。gcc编译器名、-Wall这类警告选项放入变量,方便覆盖;-MMD这类辅助选项也放变量,但要记得在文档里说明“不要轻易关掉”。

第三,每个Makefile都加少量注释。make的语法可读性本身一般,三个月后回来看,如果里面没有注释,很多变量和规则的用意只能靠猜。注释不用多,每个区块一两句就足够。

6. 写在最后的个人经验

学Makefile最忌讳的事情,是一上来就纠结“高级技巧”,比如函数库、内嵌Perl、各种奇技淫巧。我见过太多人对着几十行复杂的Makefile头晕,最后得出结论“Makefile太难看懂了”。其实把它拆开看,每一行都逃不脱“目标、依赖、配方”这三个要素,复杂只是这三要素在大量重复时的自然堆叠。

我自己的学习路径很简单:先带着一个真实的小项目(模拟项目X),从头写一份能用的Makefile;然后用make -n看它到底要干什么,用$(info ...)打印变量确认自己的理解;最后再一点一点加自动依赖、多目录、并行构建。每改一处,就重新构建一遍,确认行为符合预期。这套“小步演进”的方法,比任何教程都管用。

关于Makefile还有一个容易被忽略的价值:它把“构建过程”变成了可复现的、机器可执行的文本。你的项目换了新人来接手,跑一下make就能得到和文档描述一致的产物;你的CI流程里只需要执行一条make test。这种“把构建过程固化成版本化文件”的思路,是比具体语法更值得学习的工程习惯。

如果你后面需要更现代化的构建体验,可以再去了解CMake和Ninja,它们底层依然能看到make的影子,但无论如何,理解Makefile的“时间戳+依赖+规则”这套底层逻辑,都会让你使用任何构建工具时都更快上手。

最后还是那句话:别怕Makefile,先跑起来,再去改造它。你踩的第一个坑,大概率就是Tab和空格的区别,而踩过这个坑之后,后面的路都会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询