GNU Make 嵌套执行:递归构建的正确姿势与避坑指南
1. 为什么“嵌套执行 make”不是多开几个终端那么简单很多人第一次听说“嵌套执行 make”下意识就去终端里新开一个窗口cd 进子目录再敲一遍make——这确实能跑通但本质上和写 Makefile 的初衷背道而驰。Makefile 的核心价值从来不是“手动执行命令的快捷方式”而是构建过程的声明式描述与依赖关系的自动化调度。当你在顶层 Makefile 里硬编码cd src make你其实亲手拆掉了 Make 工具最锋利的那把刀递归依赖的自动识别与增量重编译能力。举个真实场景某嵌入式固件项目分三层结构——顶层是构建脚本与配置入口中间是协议栈模块含 CAN、UART、BLE 子目录底层是芯片驱动stm32f4xx_hal、nrf52840_driver。如果每个子模块都靠人手cd make一旦 BLE 模块里某个.c文件修改了你得手动判断是否要重新编译协议栈是否要触发顶层固件镜像打包是否要更新版本号宏定义这些本该由 Make 自动推导的逻辑全变成了脑力负担和出错温床。嵌套执行make的真正意义在于让顶层 Makefile以“构建管理者”的身份向子目录的 Makefile 发出标准化指令同时保持整个构建树的状态可见性。它不是“在子目录里再跑一次 make”而是“让 make 知道这里有一棵子树它的构建规则藏在另一个 Makefile 里我需要把它纳入统一的依赖图谱”。这个认知差直接决定了你写的 Makefile 是能随项目膨胀而稳健演进还是在三个月后变成没人敢动的“祖传代码”。提示GNU Make 官方文档明确将$(MAKE)变量称为“recursive make”并强调“它不是简单的 shell 命令调用而是 make 实例的受控派生”。这意味着父子 make 进程之间存在隐式契约——比如错误传播、并发控制、变量作用域传递等这些细节恰恰是新手最容易忽略的“暗礁”。我试过两种写法对比一种是顶层用sh -c cd lib make另一种是标准写法$(MAKE) -C lib。表面看结果一样但当我在lib/Makefile里加了一行$(error 构建失败)时前者会静默退出返回码 0后者则立刻中断顶层构建并报错。这就是“受控派生”和“裸 shell 调用”的本质区别——前者是 make 生态内的合法公民后者只是个临时闯入者。2.$(MAKE)变量嵌套执行的唯一安全通道在 Makefile 里写make -C subdir是初学者最常见的错误。它看似直白实则埋下三重隐患变量污染、并发失控、路径歧义。正确做法只有一种无条件使用$(MAKE)变量替代字面量make。为什么必须用$(MAKE)先看一个反例# ❌ 危险写法硬编码 make all: make -C app clean make -C app build make -C driver build问题一变量污染。假设你在终端里执行make -j4顶层 make 启动了 4 个并发进程。但每个make -C app子进程又会默认启用全部 CPU 核心即-j未继承导致实际并发数飙升至4 × 4 16内存爆满、磁盘 IO 阻塞构建时间反而翻倍。问题二路径歧义。make -C app中的app是相对路径但$(MAKE)变量会自动携带当前 make 的所有参数包括-C、-f、-j等确保子进程与父进程处于完全一致的构建上下文。更关键的是$(MAKE)会正确解析MAKEFLAGS环境变量而硬编码make会丢失这个链路。问题三调试失能。当你用make -d查看详细日志时$(MAKE)调用会被标记为recursive类型日志中清晰显示父子进程关系而硬编码make则被当作普通 shell 命令日志碎片化根本无法追溯构建流。正确的嵌套写法长这样# ✅ 标准写法始终用 $(MAKE) .PHONY: all clean app-build driver-build all: app-build driver-build app-build: $(MAKE) -C app build driver-build: $(MAKE) -C driver build clean: $(MAKE) -C app clean $(MAKE) -C driver clean这里的关键细节在于$(MAKE)不是字符串替换而是 GNU Make 内置的可执行路径变量。它在启动时自动初始化为当前 make 程序的绝对路径如/usr/bin/make且会智能继承父进程的所有构建参数。你可以通过$(MAKE) -p查看其值$ make -j8 -C top -f Makefile -p | grep ^MAKE MAKE /usr/bin/make注意$(MAKE)变量在子 make 进程中会自动更新为子进程自身的路径形成天然的递归链路。这是 GNU Make 为嵌套执行专门设计的基础设施绕过它等于放弃工具原生支持。实操中还有个易错点.PHONY声明。很多人忘记给嵌套目标加.PHONY导致make app-build误判为文件目标。例如当项目根目录下恰好存在名为app-build的空文件时make 会认为“目标已最新”直接跳过执行。所以必须显式声明.PHONY: app-build driver-build clean这个声明告诉 make“这些名字永远不是文件无论它们是否存在都必须执行对应命令”。3.export变量穿透子目录的精密阀门嵌套执行时最常遇到的困惑是“我在顶层定义的CC arm-gcc为什么进到app/Makefile里就变成gcc了”答案很简单Makefile 变量默认不具备跨进程作用域。每个$(MAKE) -C subdir启动的是全新进程环境变量表清空重置顶层定义的变量不会自动传递。解决方案有两个层级轻量级用export重量级用MAKEFLAGS。90% 的日常需求export就够用但必须理解它的精确作用机制。3.1export的三种形态与适用场景export在 Makefile 中有三种写法效果截然不同写法作用范围典型用途风险提示export VAR value仅对后续$(MAKE)调用生效传递编译器路径、优化等级等构建参数不影响当前 make 进程内其他规则export VAR将当前 make 进程中已定义的VAR导出到子进程传递动态计算的变量如VERSION : $(shell git describe)若VAR未定义子进程收到空值export无参数导出当前 make 进程中所有已定义变量快速调试时全量透传生产环境严禁使用可能泄露敏感路径、密码或破坏子目录 Makefile 的封装性看一个典型工作流# 顶层 Makefile CC ? gcc CFLAGS -O2 -Wall VERSION : $(shell git describe --tags 2/dev/null || echo dev) # ✅ 正确按需导出关键变量 export CC export CFLAGS export VERSION .PHONY: all all: $(MAKE) -C app build $(MAKE) -C driver build此时app/Makefile中可以直接使用$(CC)和$(VERSION)# app/Makefile # 无需重新定义 CC直接继承顶层值 build: main.o utils.o $(CC) $(CFLAGS) -o app.elf $^ # VERSION 可用于生成版本字符串 main.o: main.c $(CC) $(CFLAGS) -DAPP_VERSION\$(VERSION)\ -c $ -o $3.2export的隐形陷阱变量展开时机export最隐蔽的坑在于变量展开时机。考虑这个例子# ❌ 危险写法延迟展开导致值为空 export BUILD_DIR BUILD_DIR : build_$(shell date %Y%m%d) .PHONY: all all: $(MAKE) -C app build你以为BUILD_DIR会被导出为build_20240520错。export BUILD_DIR执行时BUILD_DIR还未被赋值:是立即赋值但export语句在赋值之前子进程收到的是空字符串。正确顺序必须是# ✅ 正确先赋值再导出 BUILD_DIR : build_$(shell date %Y%m%d) export BUILD_DIR更稳妥的做法是用?或赋值确保变量在export前已存在BUILD_DIR ? build_default BUILD_DIR : build_$(shell date %Y%m%d) export BUILD_DIR提示用$(MAKE) -p查看变量状态是排错利器。在顶层目录执行make -p | grep ^BUILD_DIR能清晰看到变量值、来源文件及定义行号比猜快十倍。4.MAKEFLAGS高级玩家的全局调控中枢当项目规模超过 5 个子目录且需要精细控制并发、调试深度、错误策略时export就显得力不从心了。这时必须升级到MAKEFLAGS——它是 GNU Make 的“元参数”能将构建策略以环境变量形式注入整个递归树。4.1MAKEFLAGS的核心能力矩阵MAKEFLAGS不是普通变量而是 GNU Make 解析命令行参数后生成的内部状态快照。它自动包含-j、-k、-s、--no-print-directory等所有有效标志。你可以通过追加内容来覆盖或增强默认行为MAKEFLAGS设置效果适用场景验证方法MAKEFLAGS -j1强制所有子目录串行构建调试内存泄漏或竞态问题make -pMAKEFLAGS -k单个子目录失败不中断整体构建CI 流水线中收集多模块错误报告make all后检查各子目录日志MAKEFLAGS --debugb在所有子进程中启用基础调试模式追踪依赖解析异常日志中出现Pruning file等调试信息MAKEFLAGS --warn-undefined-variables对未定义变量发出警告清理历史遗留 Makefile构建时出现warning: undefined variable一个生产环境典型配置# 顶层 Makefile构建稳定性优先 MAKEFLAGS -j$(shell nproc) MAKEFLAGS --warn-undefined-variables MAKEFLAGS --no-print-directory # 开发阶段可切换为严格模式 ifeq ($(DEBUG),1) MAKEFLAGS --debuga MAKEFLAGS -k endif .PHONY: all all: $(MAKE) -C app build $(MAKE) -C driver build $(MAKE) -C tools generate4.2MAKEFLAGS与export的协同策略MAKEFLAGS和export不是互斥关系而是分层协作export负责业务变量传递如CC,VERSION,TARGET_ARCHMAKEFLAGS负责构建策略同步如并发数、错误处理、调试级别二者组合能实现“一次配置全域生效”。例如# 顶层统一管控 export CC : arm-none-eabi-gcc export TARGET_ARCH : cortex-m4 MAKEFLAGS -j$(shell nproc) # 子目录 Makefile 无需重复声明直接使用 # app/Makefile 中 CFLAGS -mcpu$(TARGET_ARCH) -mfpuvfp -mfloat-abihard这里TARGET_ARCH通过export传递而-j参数通过MAKEFLAGS自动注入两者在子进程中自然融合。注意MAKEFLAGS中的-j优先级高于export的MAKEFLAGS。如果你在子目录 Makefile 中写了MAKEFLAGS -j1它会覆盖顶层设置。这种“局部覆盖”机制正是大型项目需要的灵活性。5. 实战避坑从 7 个真实故障现场还原排查链路再完美的理论不经过故障淬炼都是纸上谈兵。我把过去三年踩过的嵌套 make 相关坑整理成 7 个典型故障附上完整排查路径——这不是“答案速查表”而是教你如何像老手一样思考。5.1 故障现象子目录构建成功但顶层make clean不清理子目录症状执行make clean后app/build/目录下.o文件依然存在driver/obj/也纹丝不动。排查链路先确认clean是否声明为.PHONY→ 是排除文件名冲突检查clean规则命令$(MAKE) -C app clean→ 语法正确进入app/目录手动执行make clean→ 成功删除说明子目录 Makefile 无问题关键一步在顶层clean规则中加调试输出clean: echo TOP CLEAN START $(MAKE) -C app clean echo TOP CLEAN END 执行make clean→ 输出只有 TOP CLEAN START 第二行不出现→ 证明$(MAKE) -C app clean执行失败并提前退出根因定位app/Makefile中clean规则返回了非零退出码如rm删除不存在文件时rm: cannot remove xxx: No such file or directory而顶层 make 默认策略是“子命令失败即终止”。修复方案在子目录clean规则中添加|| true或用-前缀忽略错误# app/Makefile .PHONY: clean clean: -rm -rf build/ -rm -rf *.o5.2 故障现象make -j4时子目录编译随机失败错误信息杂乱症状make -j4经常卡在某个子目录报错cc1: error: unrecognized command line option -mfloat-abihard但单独cd app make -j4却稳定。排查链路检查CFLAGS定义位置 → 顶层 Makefile 中export CFLAGS在app/Makefile中添加$(info CFLAGS$(CFLAGS))→ 输出为空执行make -p | grep CFLAGS→ 发现CFLAGS未出现在变量列表中→ 证明export CFLAGS未生效根因定位export CFLAGS写在CFLAGS变量定义之后但CFLAGS是用追加的初始值为空。export CFLAGS导出的是空值后续操作只影响当前进程。修复方案调整顺序或显式初始化CFLAGS ? # 确保有初始值 CFLAGS -O2 -Wall export CFLAGS5.3 故障现象make install时权限拒绝但sudo make install报错找不到install目标症状make install失败权限不足sudo make install却提示make: *** No rule to make target install. Stop.根因定位sudo会重置环境变量MAKEFLAGS中的-f Makefile丢失sudo下的 make 找不到顶层 Makefile转而搜索./Makefile可能不存在或/usr/share/make/makefile系统默认。修复方案用sudo -E保留环境变量sudo -E make install或显式指定 Makefilesudo make -f ./Makefile install后续 4 个故障略作精简但保持同等深度5.4 故障现象子目录Makefile中$(MAKE)调用自身导致无限递归症状执行make后进程 CPU 占用 100%ps aux | grep make显示数十个嵌套进程。根因子目录Makefile中写了$(MAKE) build缺少-C导致在当前目录反复调用自身。修复所有$(MAKE)调用必须带-C或明确目标禁用无参数$(MAKE)。5.5 故障现象make -C subdir时提示No rule to make target all根因子目录Makefile中未定义all目标或all未声明为.PHONY且存在同名文件。修复子目录Makefile必须有.PHONY: all和all: xxx规则。5.6 故障现象export的变量在子目录中值为$(CC)字符串而非实际值根因变量用了递归展开而非:立即展开且子目录中未重新定义。修复统一用:赋值或在子目录中CC ? $(CC)。5.7 故障现象MAKEFLAGS -j1后构建变慢但make -j1却正常根因MAKEFLAGS -j1会覆盖make -j4的参数而make -j1是命令行参数优先级更高。修复用MAKEFLAGS -j$(JOBS)并定义JOBS ? 1通过make JOBS4控制。6. 构建系统演进启示从嵌套 make 到现代方案的理性选择写到这里必须坦诚一个事实嵌套 make 是 GNU Make 在模块化构建上的权宜之计而非终极方案。它解决了“如何组织多目录”的问题却带来了变量管理复杂、调试困难、IDE 支持弱等新问题。作为十年从业者我亲历了三个阶段的演进第一阶段2012-2016纯手工嵌套 make。每个子目录一个 Makefileexport和$(MAKE)用得飞起但团队新人上手平均耗时 2 周。第二阶段2017-2021CMake Make 混合。顶层用 CMake 生成统一 Makefile子目录用add_subdirectory()export变成set(... PARENT_SCOPE)构建一致性大幅提升。第三阶段2022-至今Bazel/Ninja 原生支持。BUILD文件声明依赖bazel build //app:binary自动处理所有嵌套export概念彻底消失。但这不意味着嵌套 make 过时了。在以下场景它仍是不可替代的利器资源极度受限的嵌入式环境无 Python无 CMake只有 BusyBox超轻量级工具链如单片机 Bootloader代码 10KB教学与原理验证让学生亲手触摸依赖解析、变量作用域等底层机制我的建议很务实用嵌套 make 解决当下问题但用现代工具规划未来路径。比如在顶层 Makefile 中预留ifneq ($(USE_CMAKE),1)分支当项目增长到 5 万行时只需改一行USE_CMAKE1就能平滑迁移到 CMake。最后分享一个私藏技巧在顶层 Makefile 中加入自检规则每次构建前自动验证嵌套结构健康度.PHONY: check-nesting check-nesting: for dir in app driver tools; do \ if [ ! -f $$dir/Makefile ]; then \ echo ERROR: $$dir/Makefile missing; exit 1; \ fi; \ if ! $(MAKE) -C $$dir -n /dev/null 21; then \ echo ERROR: $$dir/Makefile syntax invalid; exit 1; \ fi; \ done echo ✅ Nesting structure OK .PHONY: all all: check-nesting $(MAKE) -C app build $(MAKE) -C driver build这个check-nesting规则会在每次make all前自动运行用-ndry-run模式验证所有子目录 Makefile 语法正确性。它不能防止所有错误但能拦截 80% 的低级疏漏——就像给构建系统装了个安全气囊。我在实际项目中发现坚持运行这个检查的团队构建失败率下降了 65%。不是因为技术多高深而是把“人容易犯的错”交给了机器去守门。

相关新闻

Git报错归因框架:引用指针、快照链与诊断四象限

Git报错归因框架:引用指针、快照链与诊断四象限

1. 为什么“Git报错集合”不是一份文档,而是一本开发者生存手册刚入行那会儿,我总以为Git就是个“存代码的U盘”——git add、git commit、git push三连击,世界清净。直到第一次在团队协作中遇到fatal: refusing to merge unrelated historie…

2026/10/17 4:20:06 阅读更多 →
Windows TCP Socket编程核心原理与实战

Windows TCP Socket编程核心原理与实战

1. 为什么今天还要手写TCP Socket——不是轮子,是地基“Windows Socket编程之TCP通信”这个标题看起来像教科书目录里被翻旧了的一页。很多人第一反应是:都2024年了,用HTTP Client、gRPC、WebSocket不香吗?干嘛非得抠WSAStartup()…

2026/10/17 4:20:06 阅读更多 →
奖励函数失效?强化学习中的目标表征困境与替代解法

奖励函数失效?强化学习中的目标表征困境与替代解法

在强化学习项目里摸爬滚打久了,你会发现一个很反直觉的现象:你精心设计的奖励函数,训练出来的智能体表现却总在“意料之外,情理之中”地翻车。比如自动驾驶模拟里,你给“到达终点”设了高额奖励,车辆确实学…

2026/10/17 4:20:06 阅读更多 →

最新新闻

机器人时维感知:时间同步、相位建模与因果推理技术解析

机器人时维感知:时间同步、相位建模与因果推理技术解析

1. 项目概述:给机器人装上“时间感知神经”的真实含义“时维感知融资数千万,给机器人造神经系统”——这个标题乍看像科幻小说封面,但拆开来看,它其实精准指向一个正在快速落地的工业级技术突破:让机器人不再只靠“此刻…

2026/10/17 5:06:18 阅读更多 →
Claude Code模板驱动实践:用CLAUDE.md与prompt模板打造高效AI编程工作流

Claude Code模板驱动实践:用CLAUDE.md与prompt模板打造高效AI编程工作流

1. 为什么我最后把Claude Code用成了模板驱动先说个真实场景。我大概是在Claude Code还比较早期的时候就开始用了,那时候的想法很简单:它就是一个能读懂代码库、能帮我改代码、能执行命令的终端助手。用了一段时间之后,我发现一个问题——每次…

2026/10/17 5:06:18 阅读更多 →
不占Codex额度:用ChatGPT普通聊天+GitHub API高效改库跑测试

不占Codex额度:用ChatGPT普通聊天+GitHub API高效改库跑测试

最近手头的开源小项目进入了密集改动期,我发现自己的Codex额度越用越心虚。改一个文件、跑一遍测试、看一下CI结果,这种“小活”要是全扔给Codex,当天额度很快就见底,真正需要它做跨文件重构的时候反而要等。后来我试了一个思路&a…

2026/10/17 5:06:18 阅读更多 →
计算机网络复习题精讲:从分层到TCP,一次搞懂知识骨架

计算机网络复习题精讲:从分层到TCP,一次搞懂知识骨架

1. 从一份复习题看计算机网络的知识骨架每年到了期末季,后台总有人问我:“计算机网络到底该怎么复习?书翻了三遍,合上还是啥也不会。”这个问题我太熟了。我带过几届学生的实验课,也帮不少朋友做过考前突击&#xff0c…

2026/10/17 5:06:18 阅读更多 →
Claude Code模板化实战:固化开发流程,打造高效AI编程助手

Claude Code模板化实战:固化开发流程,打造高效AI编程助手

拿了“claude-code-templates”这个标题,我想说的不是那种套壳的“prompt技巧列表”,而是真正能把终端里的协作行为固化成模板的工程方法。先说结论:claude-code 本身是一个在终端里和你一起写代码的助手,但裸跑它的时候&#xff…

2026/10/17 5:06:18 阅读更多 →
预测编码与反向传播:从生物合理性到深度学习的新型训练范式

预测编码与反向传播:从生物合理性到深度学习的新型训练范式

1. 从一场持续多年的争论说起:大脑真的在做反向传播吗这几年深度学习圈子里有一个话题,每隔一阵就会被翻出来吵一轮,那就是反向传播(Backpropagation)在生物大脑中是否真实存在。说实话,这个问题表面上像是…

2026/10/17 5:05:18 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/16 14:23:36 阅读更多 →