- 后端
- 文档
【免费下载链接】readthedocs.org
The source code that powers readthedocs.org
Read the Docs(RTD)通过仓库根目录下的readthedocs.yaml文件,将文档构建的关键决策权交还给项目维护者。本文以仓库中的设计文档 yaml-file.rst 为主线,完整还原该配置文件的设计目标、设置归属划分、格式约定与版本策略,并结合 readthedocs/config/ 下的解析与校验源码,说明每一项设计决策在当前代码中是如何落地的——读完后你能够准确判断哪些项目设置该写进 YAML、哪些必须留在 Web 界面,并理解一次构建中配置文件从查找、解析到参与构建的完整调用链。
一、设计背景与目标
设计文档开篇指出,YAML 配置文件当时处于 beta 状态,尚未支持 RTD 的全部选项,该文档的定位就是讨论如何实现缺失特性的设计说明。其范围(Scope)包含六个目标,值得逐条对照当前实现来看:
- 补全规范(spec),覆盖所有缺失选项;
- 保持规范内部的命名与语义一致性;
- 为最终用户提供完整的配置文档;
- 允许在 YAML 文件中显式声明所使用的规范版本;
- 收集/展示关于 YAML 文件与构建配置的元数据;
- 推动配置文件的采用(adoption)。
其中“收集/展示构建元数据”与“推动采用”两条,在今天的代码里都有清晰对应:构建完成后配置对象会被序列化为字典存入构建记录(见下文第七节),而设计文档中“建议最小可用配置”的采用策略也已在产品层面落地。
二、设置的归属:什么能写进 YAML,什么不能
这是设计文档最核心的决策框架。RTD 的设置被划分为三类,划分依据是:设置是否依赖项目初始状态、是否计划移除、以及安全与隐私约束。
2.1 不适用 YAML 文件的设置
设计文档明确列出了不能放入 YAML 的设置,理由是它们可能依赖项目初始设置、计划被移除、或涉及安全与隐私:
- Project Name(项目名称)
- Repo URL(仓库地址)
- Repo type(仓库类型)
- Privacy level(隐私级别,当时计划移除)
- Project description(项目描述,当时计划移除)
- Single version(单版本项目)
- Default branch(默认分支)
- Default version(默认版本)
- Domains(自定义域名)
- Active versions(活跃版本)
- Translations(翻译)
- Subprojects(子项目)
- Integrations(集成)
- Notifications(通知)
- Language(文档语言)
- Programming Language(编程语言)
- Project homepage(项目主页)
- Tags(标签)
- Analytics code(分析代码)
- Global redirects(全局重定向)
2.2 全局设置:只存数据库
为了与“按版本(per-version)设置”保持一致、避免混淆,设计文档决定全局设置不存入 YAML 文件,只存数据库。这解释了为什么上述域名、子项目、全局重定向等跨版本生效的属性始终只能在 Web 界面管理。
2.3 版本级本地设置:由 YAML 提供
这类配置在构建某个版本时,从当前版本的 YAML 文件中读取。设计文档列出当时已实现的部分:
- 文档类型(Documentation type)
- 项目安装方式(虚拟环境、requirements 文件、Sphinx 配置文件等)
- 附加构建格式(pdf、epub 等)
- Python 解释器版本
- 按版本的重定向(Per-version redirects)
对照当前源码,前四项已有完整实现,映射关系如下:
| 设计文档中的本地设置 | 当前源码中的承载 |
|---|---|
| 文档类型 | BuildConfigV2.doctype属性(sphinx构建器 /mkdocs/ 自定义命令时为GENERIC),见 config.py |
| 项目安装 | python.install(pip / setuptools / uv / requirements)与conda.environment,见 config.py 的validate_python |
| 附加构建格式 | formats键,合法值为htmlzip、pdf、epub,或all关键字,见 config.py |
| Python 解释器 | build.tools.python,python_interpreter属性据此推断解释器类型(python / conda / mamba),见 config.py |
| 按版本重定向 | 从源码结构看,当前BuildConfigV2的校验逻辑中不存在redirects键(未知键会被validate_keys拒绝),版本级重定向实际由 readthedocs/redirects/models.py 中的数据库模型管理,未纳入 YAML 规范 |
三、文件格式:文件名、位置与书写约定
3.1 文件命名与查找规则
设计文档规定:文件基于 YAML 1.2 规范编写,必须位于仓库根目录,且文件名为以下四种之一:
readthedocs.ymlreadthedocs.yaml.readthedocs.yml.readthedocs.yaml
这一规则在源码中体现为一条正则与一个查找函数。config.py 定义了文件名匹配模式:
CONFIG_FILENAME_REGEX = r"^\.?readthedocs.ya?ml$"find.py 中的find_one遍历指定目录,返回第一个匹配该正则的文件:
def find_one(path, filename_regex): """Find the first file in ``path`` that match ``filename_regex`` regex.""" _path = os.path.abspath(path) for filename in os.path.listdir(_path) if False else os.listdir(_path): if re.match(filename_regex, filename): return os.path.join(_path, filename) return ""(实际代码为os.listdir,上文仅示意循环逻辑。)load 函数 在默认路径下调用find_one,找不到文件时抛出ConfigError.DEFAULT_PATH_NOT_FOUND(错误 IDconfig:path:default-not-found),与设计文档“文件必须在根目录”的约定一致。此外,加载时通过safe_open允许符号链接,但限定链接目标必须解析在仓库目录内部,防止路径逃逸。
3.2 书写约定
设计文档强制规范使用以下四条约定,它们在实现中均有严格对应:
- 空列表用
[]表示 —— 对应各validate_*列表校验的默认值(如formats默认[]、build.commands默认[]),见 config.py 的self.pop_config("formats", []); - 空值用
null表示 —— 对应校验中None值的显式判断(如conda.environment缺失、sphinx.configuration未提供时的分支处理); - 全选用内部字符串关键字
all表示 —— 源码中定义为常量ALL = "all"(config.py),用于formats: all、submodules.include: all以及 uv 安装的groups: all/extras: all; - 布尔字段只接受
true/false—— 由 validation.py 的validate_bool统一校验(如sphinx.fail_on_warning、submodules.recursive、mkdocs.fail_on_warning)。
四、规范(Spec)的编写方式与版本策略
4.1 用校验模式描述规范
设计文档提出,规范应以**校验模式(validation schema)**的形式编写,使“规范即代码”,而不是停留在自然语言文档上。当前实现采用两层校验结构来落实这一点:
- 解析层:parser.py 用
yaml.safe_load安全解析,且要求顶层必须是非空 mapping,否则抛出ParseError:
config = yaml.safe_load(stream) if not isinstance(config, dict): raise ParseError("Expected mapping") if not config: raise ParseError("Empty config")- 模型层:models.py 用 Pydantic 模型描述每个配置键的合法结构,基类
ConfigBaseModel显式禁止多余字段:
class ConfigBaseModel(BaseModel): model_config = ConfigDict( # Don't allow extra fields in the models. # It will raise an error if there are extra fields. extra="forbid", )例如Sphinx模型固定了builder的合法取值与默认值(models.py):
class Sphinx(ConfigBaseModel): configuration: str | None builder: Literal["sphinx", "sphinx_htmldir", "sphinx_singlehtml"] = "sphinx" fail_on_warning: bool = False- 未知键拒绝:所有合法键被逐个
pop之后,validate_keys(config.py)会检查raw_config中是否还残留任何键,一旦发现即抛出ConfigError.INVALID_KEY_NAME。这使“规范之外的写法”在构建前就明确失败,而不是被静默忽略。
4.2 规范的版本化
设计文档的版本策略有两条原则:
- 规范只使用主版本号(如 1.0,而不是 1.2),用户在 YAML 文件的
version键中声明要使用的版本; - 为了兼容未写
version的旧项目,缺省使用“最新的兼容版本”(文档撰写时为 1.0)。
当前代码记录了这一策略的演进:config.py 中LATEST_CONFIGURATION_VERSION = 2,而 load 函数 的校验逻辑显示,version缺省时按 2 处理,显式写出则必须是2(或字符串"2"),否则抛出INVALID_VERSION(config:base:invalid-version);BuildConfigV2 的version属性固定为"2",且构建入口会直接拒绝 v1 配置(见下文构建流程)。也就是说,从 1.x 到 2 的演进遵循了“只升主版本”的设计约束,且旧版本已被强制废弃。
五、构建流程中的配置文件
设计文档给出的构建流程共七步,当前实现与之一一对应,关键代码位于 doc_builder/config.py 与 doc_builder/director.py:
- 更新仓库——
BuildDirector.setup_vcs克隆仓库(director.py),并执行post_checkout构建任务; - 检出当前版本—— 在 VCS 环境中
checkout到目标版本; - 从数据库获取设置—— Celery 任务在
before_start阶段收集项目数据(TaskData)传入构建器; - 解析 YAML 文件(出错即构建失败)—— load_yaml_config 调用
readthedocs.config.load,任何ParseError都会被包装为带语法错误详情的ConfigError.SYNTAX_INVALID(config:base:invalid-syntax); - 合并 YAML 与数据库设置—— 配置对象完成后写入构建上下文(director.py):
self.data.config = load_yaml_config( version=self.data.version, readthedocs_yaml_path=custom_config_file, ) self.data.build["config"] = self.data.config.as_dict()- 按设置构建版本—— 后续
setup_python_environment、system_dependencies(安装build.apt_packages)等环节全部消费该配置对象; - 用户可查看构建所用设置—— 正是上一步
self.data.build["config"] = self.data.config.as_dict()落库的结果,as_dict()(config.py)按PUBLIC_ATTRIBUTES(version、formats、python、conda、build、doctype、sphinx、mkdocs、submodules、search)导出,使构建记录中持久化一份“实际生效的配置”元数据,实现了设计文档中“Collect/show metadata”的目标。
此外,构建入口还有三道硬性门禁(director.py),体现了规范演进期的兼容策略:
version不是2(v1 配置)时,抛出NO_CONFIG_FILE_DEPRECATED构建错误;- 仍在使用已被移除的
build.image键时,抛出BUILD_IMAGE_CONFIG_KEY_DEPRECATED; - 未声明
build.os时,抛出BUILD_OS_REQUIRED——即从 v2 起,构建系统镜像必须由配置文件显式指定,不再有隐式默认值。
设计文档还提到一个安全细节:如果项目配置了具有写权限的 SSH key,且用户配置了build.jobs.post_checkout,构建会被直接终止(director.py 的SSH_KEY_WITH_WRITE_ACCESS错误),防止构建任务被滥用为向仓库写回内容的手段。
六、校验规则速览:v2 规范的关键约束
BuildConfigV2.validate()(config.py)按固定顺序执行各键的校验,顺序本身也有讲究——validate_build必须先于validate_python/validate_conda(后者依赖前者确定的build属性),validate_doc_types必须先于 sphinx/mkdocs 校验:
self._config["formats"] = self.validate_formats() self._config["build"] = self.validate_build() self._config["conda"] = self.validate_conda() self._config["python"] = self.validate_python() self.validate_doc_types() self._config["mkdocs"] = self.validate_mkdocs() self._config["sphinx"] = self.validate_sphinx() self._config["submodules"] = self.validate_submodules() self._config["search"] = self.validate_search() if self.deprecate_implicit_keys: self.validate_deprecated_implicit_keys() self.validate_keys()结合各校验方法,可归纳出 v2 规范中最容易踩坑的约束:
- build:
build.os必填且必须匹配构建镜像设置中的可用值;build.tools的每个工具名与版本同样受设置表约束;build.commands与build.jobs二选一,同时出现会抛出BUILD_JOBS_AND_COMMANDS;两者全空则抛出NOT_BUILD_TOOLS_OR_COMMANDS(config.py); - build.jobs.build:只允许
html、pdf、epub、htmlzip四类任务(对应BuildJobsBuildTypes,models.py),且除html外的构建类型必须已在formats中声明,否则抛出BUILD_JOBS_BUILD_TYPE_MISSING_IN_FORMATS; - apt_packages:包名经过白名单正则
^[a-zA-Z0-9][a-zA-Z0-9.+-]*$校验,且禁止以-、/、.开头,防止注入 apt 选项或从本地路径安装包(config.py); - python.install:每个条目必须是三选一——
method: uv(此时command必填,取值为sync或pip,且sync不允许requirements、pip不允许groups,requirements与path互斥);requirements: <path>;或path: <path>(method取pip或setuptools,extra_requirements仅 pip 可用)。使用 uv 时整个python.install列表只允许一条 uv 条目(config.py); - sphinx:
sphinx与mkdocs不能同时出现(SPHINX_MKDOCS_CONFIG_TOGETHER);sphinx.builder支持html、htmldir、dirhtml、singlehtml并映射为内部构建器标识(config.py);sphinx.configuration若指定,其文件名必须是conf.py,否则抛出SPHINX_INVALID_CONFIG_FILE; - submodules:
include与exclude不能同时使用(SUBMODULES_INCLUDE_EXCLUDE_TOGETHER),二者均支持all关键字; - search:
ranking是“路径模式 → 排名”的映射,排名为 -10 到 10 的整数;ignore默认为search.html、search/index.html、404.html、404/index.html(config.py); - 隐式键弃用:
deprecate_implicit_keys开启后(由部署侧开关与固定时间窗口控制,config.py),sphinx/mkdocs键一旦使用就必须提供configuration路径;且既未使用build.commands也未覆盖新构建任务时,显式的sphinx键成为必需——这反映了平台逐步移除“按文件名猜测文档类型”隐式行为的迁移策略。
配套的解析与校验测试位于 readthedocs/config/tests/ 目录,覆盖了文件名查找、各键校验与错误消息等场景。
七、配置文件与数据库的关系
设计文档对二者关系的界定非常克制:构建时从配置文件读取的设置(连同其他元数据)需要存入数据库,但仅用于事后查阅,不会回填(populate)到现有的项目字段。
当前实现精确遵循了这一边界。doc_builder/config.py 的注释明确写道:
# TODO: review this function since we are removing all the defaults for BuildConfigV2 as well. # NOTE: all the configuration done on the UI will make no effect at all from now on.即:Web 界面中遗留的构建类设置已完全不再生效,YAML 文件成为唯一的构建配置来源;而界面中不可由 YAML 表达的部分(域名、子项目、全局重定向等第二节所列设置)仍保存在数据库中。两个系统各司其职:数据库负责全局、跨版本与账号安全相关设置,YAML 负责版本级的构建参数,构建记录中再落一份as_dict()快照供用户复核。
八、推动配置文件的采用
设计文档最后给出了面向用户的推广思路,可作为理解该产品决策的注脚:
- 用户新建项目或进入设置页时,平台可提示一份“最小可用配置”示例,并说明哪些全局配置应放在界面而非文件中;
- 对于已有项目,可以基于其当前设置,在每次构建时向用户提示一份等价的内容配置文件。
结合当前源码看,这一采用策略的终点已经达成:v1 配置被构建入口直接拒绝(NO_CONFIG_FILE_DEPRECATED)、build.os强制显式声明、UI 构建设置失效,配置文件的地位从“可选的 beta 特性”转变为构建的准入门槛。
九、一个符合 v2 规范的完整示例
综合以上约定与校验规则,一份最小而完整的readthedocs.yaml(置于仓库根目录)大致如下:
# 文件名可以是 readthedocs.yml / readthedocs.yaml / .readthedocs.yml / .readthedocs.yaml version: 2 # 规范版本,目前只接受 2 build: os: ubuntu-22.04 # 必填;取值必须匹配平台构建镜像设置中的可用 os 键 tools: python: "3.12" # 可用工具与版本以构建镜像设置(RTD_DOCKER_BUILD_SETTINGS)为准 apt_packages: [] # 可选;包名受白名单正则约束,禁止 - / . 开头 sphinx: configuration: docs/conf.py # 若指定,文件名必须为 conf.py;使用 sphinx 键时该项为必填 builder: html # 可选:html / htmldir / dirhtml / singlehtml fail_on_warning: false # 可选,只接受 true / false formats: [] # 可选:htmlzip / pdf / epub,或 all;[] 表示仅构建 HTML python: install: - requirements: docs/requirements.txt # 三种写法之一:requirements 文件 / path + method / uv提交前可对照 readthedocs/config/exceptions.py 中的错误 ID 排查构建日志:config:path:default-not-found(找不到配置文件)、config:base:invalid-syntax(YAML 语法错误)、config:base:invalid-version(版本不是 2)、config:base:invalid-key(出现规范之外的键)等,几乎能覆盖设计文档中“解析出错即构建失败”的所有失败路径。
小结
回顾 yaml-file.rst 的设计脉络:设置的三级归属(不适用 / 全局存库 / 版本级入 YAML)划清了文件与界面的边界;四条书写约定([]、null、all、布尔)保证了规范的表达一致性;基于校验模式的 spec 与只升主版本的策略让配置规范可以像库一样演进;而**“解析失败即构建失败 + 配置快照入库”** 则把可预测性落到了每一次构建上。当前代码库 readthedocs/config/ 与 readthedocs/doc_builder/director.py 就是这份设计文档的落地形态——阅读实现时,不妨把设计文档中的每一条 Scope 当作验收清单来对照。
- 后端
- 文档
【免费下载链接】readthedocs.org
The source code that powers readthedocs.org
相关推荐
Read the Docs Pull Request 构建器设计:从设计文档到 external version 的完整落地
Read the Docs Pull Request 构建器设计:从设计文档到 external version 的完整落地 Read the Docs 的「P
后端文档Read the Docs 文档 URL 解析设计:从保留路径问题到 unresolver 的查找实现
Read the Docs 文档 URL 解析设计:从保留路径问题到 unresolver 的查找实现 本文围绕 Read the Docs 仓库中的设计文档
后端文档Read the Docs 构建系统中的 build.apt_packages:从设计文档到源码级的系统包安装实现
Read the Docs 构建系统中的 build.apt_packages:从设计文档到源码级的系统包安装实现 在 Read the Docs 的构建流水线
后端文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考