news 2026/9/11 5:56:32

makefile完全指南:从目标依赖到自动化构建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
makefile完全指南:从目标依赖到自动化构建

1. 为什么"编译一堆文件"值得一个专门的工具来管

我第一次认真接触Linux下的C项目时,老师只丢给我一句话:"在终端敲make就行。"我照做了,项目编译通过,几秒钟出结果。当时觉得神奇,因为在此之前,我习惯的方式是自己在终端里一行行敲gcc命令,比如:

gcc -o app main.c utils.c logger.c config.c -I./include -lm

刚开始文件少,这条命令还能凑合。等文件一多,问题就全来了:命令越来越长,敲完容易出错;改了一个.c文件,想把整个工程重新编译一遍,但每次都要把所有文件重新编译,几十秒起步;更麻烦的是,如果某个头文件变了,我根本意识不到哪些.c文件依赖它,经常改了头文件之后忘记重新编译对应的.o,导致程序跑的还是老代码。

说白了,"编译"本身不是问题,问题出在编译的管理上:哪些文件需要重新编译、哪些不用动、以什么顺序编译、头文件依赖怎么追踪。这些事如果全靠人脑去记,项目规模一大必然翻车。

make干的事情,就是把这套编译规则写进一个文件里——这个文件就是makefile。它读起来像一份菜谱:告诉make"我想做出什么东西(目标),需要哪些原料(依赖),以及怎么做(命令)"。make拿到这份菜谱后,会检查目标和依赖的时间戳,判断哪道菜需要回锅加热,哪道菜可以直接端上桌。

网上很多教程会说make是"自动化构建工具",这个定义没错,但对新手来说容易产生一个错觉:觉得make是某种编译器。其实make本身不编译任何东西,它只是根据规则帮你把gcc、ld这些编译命令组织起来、按需执行。真正的编译工作还是由编译器完成的。

这篇文章适合谁?想搞懂make和makefile是什么的Linux初学者,面试前想临时补一下构建知识点的求职者,以及对STM32、uboot、内核这些嵌入式项目里那堆makefile感到头痛的人。我会把自己实际用到的写法、踩过的坑都写在里面,尽量让这份笔记不只是"概念介绍",而是能直接拿过来用的工具手册。

2. makefile的最小骨架:目标、依赖与规则的三角关系

2.1 从三行代码理解makefile

先看一个最简makefile长什么样:

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

这短短两行,已经包含了makefile最核心的三要素:

  • 目标(target):想生成的东西,这里就是app这个可执行文件。
  • 依赖(prerequisites):生成目标需要哪些材料,这里是main.cutils.c两个源文件。
  • 规则(recipe/command):用依赖生成目标的具体命令,第二行那个以Tab键开头的gcc命令就是规则。

make执行的时候,逻辑是这样的:先看目标文件app存不存在。如果不存在,直接执行规则命令;如果存在,再比较目标和所有依赖的时间戳——只要有任何一个依赖比目标新,说明源代码新改了,目标过期了,需要重新执行命令;如果所有依赖都没有目标新,说明目标是最新的,make就说"没问题,啥也不用干"。

注意一个细节:规则命令前面必须是Tab键,不能是空格。这是新手遇到最多的报错之一(后面我会专门讲怎么排查)。你可以理解为make的硬性规矩:冒号下面跟的、以Tab开头的内容,才被当作要执行的命令,否则会直接报missing separator

2.2 第一个目标是默认目标,这坑了多少人

make在没有任何参数时运行,它会去找当前目录下的makefile文件,默认执行第一个目标。很多人写makefile习惯先写clean,比如:

clean: rm -f *.o app app: main.c utils.c gcc -o app main.c utils.c

这种情况下,你在终端敲make,它执行的是clean,瞬间把你之前编译出来的.o文件和app全删了。我当年就这么吃亏过一次,看着终端里一堆rm输出,人都是懵的。

所以约定俗成的做法是:把all作为第一个目标,后面跟整个工程真正要生成的东西:

all: app app: main.c utils.c gcc -o app main.c utils.c clean: rm -f app

这样make默认构建appmake clean负责清理。

2.3 伪目标:让clean这种"假目标"真正可靠

刚才的clean有个隐患。万一你的目录里恰好有一个叫clean的文件,make检查clean这个目标时,发现它已经存在,而且没有依赖,就会说:

make: 'clean' is up to date.

然后什么都不做。明明你想清理,它却告诉你"已经是最新的了",就很无语。

解决办法是用.PHONYclean声明成伪目标:

.PHONY: clean

伪目标的意思是:这个目标不代表任何真实文件,它只是一个动作的名字,不要拿它和磁盘上的文件做时间戳比较。声明以后,不管目录里有没有clean这个文件,执行make clean都会老老实实跑命令。

我第一次看到.PHONY时心想这东西有什么用啊,直到真的被同名文件坑过一次才长记性。建议所有不生成文件的目标——cleaninstalltest这些——都一律加上.PHONY

2.4 增量编译是怎么"自动"发生的

我们用gcc编译多文件工程时,正确做法是先把每个源文件编译成目标文件(.o),最后再链接成可执行文件。这样一来,如果只改了一个.c文件,就只需要重新编译那一个文件,再重新链接一次就行,链接速度比编译快得多。

makefile可以非常自然地描述这层关系:

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

这里的依赖关系是:app依赖main.outils.omain.o又依赖main.cutils.h。make会自动递归检查整条依赖链。你改了utils.c,它发现utils.outils.c旧,就去重新编译utils.o,然后发现app比新的utils.o旧,再去链接app。而main.o不用重编,因为是utils.c变了,跟main.o的依赖无关。

这就是make最聪明的部分:它不需要知道你的编译逻辑,它只按时间戳做事。你负责把依赖关系写清楚,它负责判断什么需要重做。

3. 变量、自动变量与常用函数:写一个不重复自己的makefile

3.1 变量让makefile摆脱"硬编码"

如果每个目标都把gcc命令完整写一遍,那makefile本身就会变得又臭又长。更合理的做法是把编译命令的参数提取成变量:

CC = gcc CFLAGS = -Wall -g -I./include LDFLAGS = -lm app: main.o utils.o $(CC) $(LDFLAGS) -o app main.o utils.o main.o: main.c utils.h $(CC) $(CFLAGS) -c main.c

CC是编译器名字,CFLAGS是编译选项(警告开关、调试信息、头文件路径),LDFLAGS是链接选项。引用变量统一用$(变量名)的方式。这样做的好处是,想换编译器、想加编译选项,只需要改文件顶部的一处定义,而不是满文件地改。

makefile里的变量赋值方式有四种,新手经常分不清:

赋值符号含义典型场景
=递归展开赋值,解析时才展开适合写稍后会用到的值
:=立即展开赋值,定义时立刻取值更符合直觉,推荐多用
?=如果变量未定义才赋值允许外部环境变量覆盖
+=追加内容给CFLAGS追加选项

举个例子说明=:=的区别:

A = hello B = $(A) world A = bye # 此时 B 的值是 "bye world",因为 = 是递归展开,用到B时才去找A的最新值 C := hello D := $(C) world C := bye # 此时 D 的值是 "hello world",因为 := 在定义时就确定了

我最常用的是:=?=?=特别适合接环境变量,比如让用户可以在命令行覆盖默认值,这个下面马上会说到。

3.2 自动变量:$@、$^、$< 是写规则的三把刀

写规则命令时,你经常要写"目标名""所有依赖""第一个依赖",如果都用硬编码,工作量大且容易漏改。make提供了一批自动变量,专门在规则命令里用:

自动变量含义
$@当前规则的目标名
$^当前规则的所有依赖(去重)
$<当前规则的第一个依赖
$*目标去掉扩展名后的部分

有了它们,上面的例子就能简化成:

main.o: main.c utils.h $(CC) $(CFLAGS) -c $< -o $@

$<会自动展开成main.c$@自动展开成main.o。这样即使目标名改了,命令也不用动。链接阶段也是最常见的使用方式:

app: main.o utils.o $(CC) $(LDFLAGS) -o $@ $^

$^展开为main.o utils.o,一次引用所有依赖。

3.3 通配符和两个高频函数:wildcard、patsubst

真正工程里的源文件通常不会只有两三个,而是几十个。手动一个个列出来不现实。这时候要用通配符和函数让make"自动发现"源文件:

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

$(wildcard *.c)会把当前目录下所有.c文件列出来,赋值给SRCS$(patsubst %.c,%.o,$(SRCS))SRCS中的每个.c后缀换成.o,得到期望的目标文件列表。

这两个函数我几乎在每个makefile里都会用到。一开始可能觉得多了一层抽象,但用习惯以后,往目录里加新源文件时完全不用改makefile,相当省心。

除了这两个,$(notdir 路径)可以去掉路径只剩文件名,$(shell 命令)可以在makefile里执行shell命令并取回结果。比如内核源码和uboot里经常见的$(shell ...)用法,本质就是这种思路,把一些环境探测交给shell去跑。

3.4 命令行传变量:makefile里的"参数开关"

有时候你不想改makefile,只想临时换一下编译行为,比如热词里出现的这种命令:

make have_x11=no have_glfw=no prefix=/usr/local

这其实是make的一个设计:命令行上传入的变量值,会覆盖makefile里的同名赋值。也就是说,只要makefile里写的是prefix ?= /usr/local,命令行上传了prefix=/usr/local,命令行生效。这个机制非常适合做软开关和安装路径配置。写makefile时,凡是可能由用户定制的参数,都应该用?=来声明默认值,留出命令行覆盖的口子。

4. 一个能直接抄的多文件工程makefile:从自动收集源文件到并行编译

4.1 一份适合中小型C项目的通用makefile

把前面说的知识点串起来,我整理了一份自己经常复用的小型C工程makefile。不是花架子,每一行都有实际用途:

CC := gcc CFLAGS := -Wall -O2 -g -I./include LDFLAGS := -lm TARGET := app SRCS := $(wildcard src/*.c) OBJS := $(patsubst src/%.c,build/%.o,$(SRCS)) DEPS := $(OBJS:.o=.d) all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(LDFLAGS) -o $@ $^ build/%.o: src/%.c @mkdir -p build $(CC) $(CFLAGS) -MMD -MP -c $< -o $@ clean: rm -rf build $(TARGET) -include $(DEPS) .PHONY: all clean

这份makefile有几个细节值得展开讲:

源文件目录与管理。我把.c放在src/下,中间产物.o和依赖文件.d统一丢进build/目录。好处是工程根目录不会被编译产物搞乱。$(wildcard src/*.c)自动收集所有源文件,加新文件不用改makefile。

模式规则build/%.o: src/%.c是一条模式规则,它告诉make:任何一个build/下的.o文件,都由对应的src/下的.c文件生成。make会自动匹配,对每个源文件套用这条规则。用模式规则替换掉一条条手写规则之后,makefile的体积和可维护性都上了一层台阶。

自动依赖生成。这里用了gcc的-MMD -MP选项,编译时顺便生成.d依赖文件。.d文件记录了这个.o文件依赖的所有头文件,这样头文件变化时也能触发重新编译。-include $(DEPS)让它们参与make的依赖检查。这个功能一开始可以不加,但项目头文件多了之后,比如只需要加一行#include "common.h"改动,如果依赖没跟上,make不会意识到需要重编,程序可能在用旧对象文件,非常难受。加上.d依赖以后,这类问题就自动消失了。

命令前的@符号@mkdir -p build前面的@表示执行命令时不在终端打印这条命令本身。mkdir -p的意思是目录存在不报错,不存在就创建。

4.2 并行编译:老电脑也能享受多核红利

make默认是一个目标一个目标串行执行,但多个没有依赖关系的目标完全可以并行编译。方法特别简单:

make -j8

-j后面的数字建议填CPU物理核心数或稍大一点。想知道自己机器有几个核心,可以用:

nproc

我自己的习惯是make -j$(nproc),让make自己拿核心数。如果项目里有多个独立的.o要编译,并行后速度提升非常明显。一个几分钟的编译任务,并行后可能三四十秒就完了。

需要提醒的是,-j在某些复杂的makefile里可能引发意外的竞争问题(比如多个编译任务同时写同一个文件),但一般中小型项目不用太担心,开大了顶多多占内存。

4.3 嵌入式和Go项目里常见的make用法

热词里出现了好几条嵌入式相关的,比如u-boot make menustm32 makefilecubemx makefile vscode,一起说说。

嵌入式开发里,make的使用率相当高。STM32CubeMX生成的工程可以直接生成makefile(选中Makefile工具链),然后在终端里用make -j编译,烧录用make flash或者配合其他脚本。很多人在VSCode里做STM32开发,其实也是调用了底层的make——VSCode的tasks.json里通常写着"command": "make"。理解了makefile,你就知道CubeMX那个工程骨架在编译阶段到底做了什么,出了问题也知道去哪看。

uboot和Linux内核的编译是另一个经典场景。它们的顶层makefile极其复杂,但你日常只需记住几个目标:

make menuconfig # 打开图形化配置菜单 make -j$(nproc) # 开始编译 make dtbs # 只编译设备树

你不需要把顶层makefile读完,只需要理解make的依赖和目标机制,这些目标在背后就是一组规则:menuconfig依赖配置工具,dtbs依赖所有设备树源文件。底层逻辑和最简单的makefile一模一样。

Go项目纯用go build就能完成任务,为什么还要套一层make?我见过很多团队用make来封装"流程":比如make test跑全部测试、make lint做静态检查、make build-linux交叉编译出Linux二进制。这种用法的本质不是让make来管编译依赖,而是把它当成一套统一的任务入口,把容易记错的多步命令封装成简短目标。这个思路在任何语言的项目里都通用。

5. 高频报错与排查链路:从"No targets"到"missing separator"

5.1 make: *** No targets specified and no makefile found. Stop.

这个报错我见过太多次了,几乎每个Linux新手都会撞上。热词里也专门有一条"make没有指明目标并且找不到makefile"。这句英文拆开看就三件事:

  • 没有指定目标(make时没带目标名,默认用第一个目标)
  • 找不到makefile文件(当前目录下没有名为makefile、Makefile或GNUmakefile的文件)

常见原因我按出现频率排个序:

  1. 当前目录根本不是你项目所在目录,或者项目目录里压根没创建makefile。
  2. 文件名写错了。GNU make查找文件名是有先后顺序的:GNUmakefile->makefile->Makefile。Windows上习惯的文件名到了Linux下大小写敏感,如果建的是MakeFileMakefile.txt,make也认不出来。
  3. makefile在子目录里,但你人不在那个目录下。这时直接用make -C 子目录名,或者先cd进子目录再make

排查时先执行ls -l看目录下有没有makefile,再确认文件名。另外注意:有些同学把工程文件从Windows拖到Linux虚拟机里,文件名后缀可能被改掉,或者打包时文件权限有问题,这种情况ls能看到但读不了,执行一下chmod +r Makefile就能解决。

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

这个报错八成是Tab键问题。makefile的规则命令必须以Tab开头,但很多编辑器默认用空格代替Tab。你把文件从Windows记事本或某些IDE里粘贴过来时,空格就悄悄混进去了。make解析到第二行,发现这一行既不是变量赋值也不是目标规则,于是报"缺少分隔符"。

排查命令特别实用:

cat -A Makefile

-A参数会把不可见字符全部显示出来。Tab会显示成^I,行尾是$。你如果看到那行开头不是^I而是普通空格,就是它了。解决办法很简单:在编辑器里把缩进方式改成"Tab",或者把那行开头的空格删掉重新敲一个Tab。顺便说一句,我用的VSCode会在状态栏显示缩进方式,报错时第一反应就是看缩进是不是被自动换成空格了。

5.3 No rule to make target 'xxx.h', needed by 'xxx.o'

这条的意思是make找不到某个依赖文件。常见原因:

  • 头文件确实不存在,或者路径写错了。检查#include的头文件名有没有拼错。
  • 头文件在别的目录,但makefile里的规则没有把目录加进路径。此时在CFLAGS里加-I./目录名,或者在依赖规则里明确写上路径。
  • 头文件是编译过程中生成的(比如配置工具生成的config.h),但生成这个头文件的目标还没执行。

这条报错也是"因为没有提前生成"的典型案例。某次编译顺序出问题,就卡在这了。解决办法是把生成头文件的规则排在编译规则之前,让它作为依赖被make自动调度。理解依赖链之后,这类错误基本看哪一行依赖没理顺,就补哪一行。

5.4 'app' is up to date. 和 Nothing to be done

这两种提示不是错误,是make在告诉你"目标没有过期,不需要重编"。你可能觉得不对:"我刚改了代码啊"。这时候先别急,检查两点:

  • 是否真的保存了源文件,目标文件的时间戳是否晚于源文件。
  • 你在执行的是不是正确的目标。比如改了utils.c,却去执行make clean这种伪目标,它可不管源码动不动,该啥反应还是啥反应。

如果确实想无视时间戳强制重新编译,可以用make -B(force rebuild)。排查时间戳问题我习惯用stat 文件名看详细时间:

stat utils.c app

对比一下两个的时间谁新谁旧,问题一目了然。

5.5 undefined reference to ... 链接顺序的经典坑

这条严格来说不完全是make的报错,但因为通过make触发编译,很多人会在makefile场景下遇到。典型情况是:

app: main.o gcc -o app main.o -lm

这个是正常的。但如果写成:

gcc -o app -lm main.o

有时候就会报undefined reference to 'pow'。原因是ld在处理库的时候只扫描一次,从左到右,在遇到-lm时扫描的是之前的符号,如果那时main.o里的符号还没被解析出来,后面再出现main.o时数学库已经被处理过了,函数就找不到了。

解决办法很简单:库的-l参数一定要放在引用它的目标文件之后。遇到链接报错时,先看我是不是把库变量放在了命令行顺序的前面。这个坑在链接sqlite、pthread等库时也经常出现,排查思路完全一样。

5.6 结合真实场景:eclipse makefile:49: fw-cnpc-app-proj.elf error 1

热词里有一条eclipse makefile:49: fw-cnpc-app-proj.elf error 1,这是交叉编译工具链在链接ELF文件时出错的典型输出。看到error 1不要慌,往前翻日志,真正有用的信息在它上面——通常是某个具体错误,比如arm-none-eabi-gcc: error: ...或者某个头文件找不到。那个"error 1"只是make对链接命令退出码的转述,表示"这条命令失败了"。

排这种错,我的固定套路是:先把make的完整输出拉长看看(make 2>&1 | tee build.log),找到第一条报错而不是看最后一行;如果命令太长看不清,用make -n把将要执行的命令打印出来,手动在终端里跑一遍就能看到编辑器里被吞掉的细节。这套方法在嵌入式交叉编译和普通Linux程序上都屡试不爽。

6. 面试考点和真实项目定位:make和CMake到底怎么分工

6.1 make在项目里到底扮演什么"角色"

一句话定位make:它是构建流程的调度器,不是构建流程本身。编译靠gcc、链接靠ld、库管理靠各种工具,而make负责在正确的时间调用正确的工具、传正确的参数。

这就解释了它在真实项目里的位置。很多现代项目在外层还会套一层CMake,典型流程是:

cmake . && make

热词里的cd nvbandwidth && cmake . && make就是这种模式。CMake根据CMakeLists.txt生成makefile,make再去执行具体的编译。为什么不直接用makefile?因为makefile在跨平台场景下有短板——Windows上的路径分隔符、不同编译器的参数差异、寻找第三方库的机制都很麻烦。CMake把这些抽象了,生成对应平台的构建文件。但CMake生成的构建文件,底层几乎仍然是makefile体系(Linux下如此)。

对学习路径的建议是:先认真学makefile,再学CMake会轻松很多。因为CMake生成的makefile虽然复杂,但里面的很多语法——目标、变量、依赖——和手写makefile是一脉相承的。见过底层原理的人,看CMakeLists时更容易心领神会。

6.2 面试喜欢问的make/makefile问题

以下几个方面是最常出现在Linux面试题里的,没必要背标准答案,理解原理就能答出来:

makefile中目标、依赖、规则的关系。一句话讲清楚:目标是最终文件,依赖是材料,规则是从材料到目标的过程。make比较目标和依赖的时间戳,决定要不要执行规则。

为什么命令前要是Tab而不是空格。make的解析器把Tab当作"规则命令开始"的标记。这确实是历史设计,但既然语法规定了就得遵守。面试时可以说"这是make的语法定义,行首Tab用来识别命令区域"。

什么是伪目标,为什么要用.PHONY。伪目标不指向真实文件,用来避免和目标文件重名导致的误判,保证clean这类操作每次都真正执行。

$@、$^、$<分别代表什么。目标名、所有依赖、第一个依赖。这三个用得最多,建议背熟。

makefile如何实现增量编译。通过拆分目标文件,让make自动比较时间戳,只重建发生变化的部分。

makefile和shell脚本有什么区别。makefile重点在于声明依赖关系,make根据依赖自动推导执行顺序;shell脚本是顺序执行的命令流,不会有make这种"自动判断哪些需要重跑"的机制。

Makefile、makefile、GNUmakefile的区别。GNU make依序查找这三者,查到哪个用哪个。一般推荐用Makefile,因为它排在Linux下ls列表的前面,容易发现。

6.3 值得记住的几个make调试技巧

最后,再分享一下我平时用make时会用到的小工具,算是在错误排查之外的日常技巧:

make -n # 只打印将要运行的命令,不真正执行 make -B # 强制重新构建所有目标,忽略时间戳 make -C 目录名 # 进入子目录执行make make -w # 打印当前工作目录,适用于子目录递归构建时

-n这个参数尤其推荐。刚写完一个makefile,不确定写没写对之前,先make -n看一遍即将执行的命令。它会直接打印出gcc那几行,你一眼就能看出变量有没有展开、顺序对不对。比直接make踩到错误再回头看要从容得多。

最后再分享一个小技巧

写makefile这么多年,最大的体会是:不要把它当成一门高深的语言去学,而是当成一份"给机器看的编译笔记"。你在终端手工敲过的每一次编译命令,其实就是最原始的构建脚本,makefile做的只是把这份笔记结构化、可复用、带依赖判断而已。所以从"我之前是怎么编译的"出发,一条条把命令搬进makefile,再逐步用变量替换重复的部分,这个过程本身就是在学会make。

如果你刚入门,建议拿一个只有三五个源文件的小项目练手,自己手工写一份makefile,完整跑一遍编译、清理、增删源文件的过程。一旦体会过"改一处makefile,全工程编译逻辑都跟着走"的感觉,后面再接触内核或嵌入式项目里那些庞大makefile时,心态会完全不一样。至少,你不会再害怕它了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 5:56:30

730+免费API完整指南:快速找到合适的接口并跑通第一次调用

730免费API完整指南&#xff1a;快速找到合适的接口并跑通第一次调用 【免费下载链接】public-api-lists A curated list of free public APIs — searchable, community-maintained, with a free JSON API. 项目地址: https://gitcode.com/GitHub_Trending/pu/public-api-li…

作者头像 李华
网站建设 2026/9/11 5:55:37

时间戳排序并发控制:从原理到工程落地的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 5:46:15

CTF压缩包爆破全攻略:从原理到实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 5:45:09

3 分钟拉齐 TikTok 账号全部作品链接:TikTokDownloader 批量获取上手

3 分钟拉齐 TikTok 账号全部作品链接&#xff1a;TikTokDownloader 批量获取上手 【免费下载链接】TikTokDownloader 抖音 / TikTok 平台作品下载/数据采集工具 项目地址: https://gitcode.com/GitHub_Trending/ti/TikTokDownloader 给它一个主页链接&#xff0c;还你一…

作者头像 李华