首页
/ openpilot 文档站开发指南:用 docs/serve.py 构建与发布 docs.comma.ai

openpilot 文档站开发指南:用 docs/serve.py 构建与发布 docs.comma.ai

2026-09-04 23:08:56作者:尤辰城Agatha

openpilot 官方文档站的源码就是仓库里的 docs/ 目录,本文围绕 docs/README.md 讲解文档站的两条核心命令(一次性构建与本地热重载预览),并深入 docs/serve.py 这个零依赖的 Markdown 转 HTML 站点生成器的实现细节,以及 .github/workflows/docs.yaml 如何将构建产物自动推送到线上站点。读完本文,你可以独立完成本地构建、预览文档改动,并理解从一次 master 推送直到站点更新的完整 CI 链路。

文档即源码:docs/ 目录的结构

docs/README.md 开宗明义:docs/ 树就是线上文档站的源码,站点在推送 master 时由工作流自动更新。当前 docs/ 目录的组织方式是:

从源码结构看,侧边栏导航不是从文件树自动扫描出来的,而是由 docs/serve.py 中硬编码的 NAV 列表显式定义的(一组 (标题, 目标) 元组,目标为 None 表示分节标题)。这意味着新增一个文档页时,除了在对应目录放 .md 文件外,还应考虑是否需要在 NAV 中登记入口。

两条核心命令:构建与本地运行

docs/README.md 给出了文档站开发的全部两条命令:

1. 构建站点

python docs/serve.py --build

2. 本地运行站点(文件变化时自动重建)

python docs/serve.py

--build:一次性构建

--build 参数的解析位于 docs/serve.py 的入口处,仅有一个布尔开关。执行后调用 build() 函数(docs/serve.py),其流程是:

  1. 读取 docs/template.html 模板;
  2. 递归收集 docs/ 下所有 *.md 文件,但排除 docs/README.md 本身以及 _site__pycache__ 目录(见 EXCLUDE_DIRSdocs/serve.py);
  3. 追加一个动态生成的术语表页concepts/glossary.md),其内容由 GLOSSARY_DESCRIPTIONS 字典渲染,覆盖 onroad、offroad、route、segment、panda、comma four 等术语(docs/serve.py);
  4. 删除并重建输出目录 docs/_site/,把模板以外的所有资源文件(图片、SVG 等)按原相对路径拷贝进去(copy_assets()docs/serve.py);
  5. 对每个 Markdown 页渲染正文、提取标题,用字符串替换填充模板占位符 {{TITLE}}{{ROOT}}{{HOME_HREF}}{{NAV}}{{BODY}}{{EDIT_URL}},输出到 docs/_site/<路由>/index.html,并打印 docs: built N pages into ...

其中 {{EDIT_URL}} 指向该页面对应的 GitHub 编辑地址(docs/serve.py),模板底部据此渲染 "Edit this page on GitHub" 链接(docs/template.html)。

不带参数:本地热重载服务

python docs/serve.py 会先进入构建,然后启动一个 ThreadingHTTPServer,监听随机可用端口,并在终端打印实际地址(如 http://localhost:PORT/)。其监视逻辑在 docs/serve.py

  • 每 0.5 秒轮询一次 docs/ 下所有受监视文件(同样排除 _site__pycache__)的 mtime;
  • 一旦发现 mtime 发生变化,打印 docs: change detected, rebuilding... 并重新执行完整构建;
  • 构建失败会打印 docs: build failed: ... 但服务不退出,继续监视;
  • Ctrl-C 触发 KeyboardInterrupt,服务器优雅关闭。

这种"轮询 mtime + 全量重建"的设计意味着本地预览是秒级反馈、且永远与构建逻辑一致,适合编辑文档时开一个终端挂着。

深入 serve.py:一个零依赖的 Markdown 站点生成器

serve.py 只使用 Python 标准库(argparsehttp.serverrehtmlpathlibthreading 等),没有引入任何第三方 Markdown 解析库,全部渲染逻辑手写。对文档作者最有价值的行为细节如下:

页面路由规则

page_route()docs/serve.py)决定每个 Markdown 文件发布后的 URL 形态:

  • index.md 映射到所在目录本身,例如 index.md → 站点根路径,how-to/connect-to-comma.md 这类非 index 页映射到去掉 .md 后缀的路径;
  • 对于非 index 页,构建器还会额外写一个 foo.html 跳转文件(write_html_redirect()docs/serve.py),通过 meta refresh 和 location.replace 把访问旧式 .html 地址的用户重定向到 foo/ 目录形式,并保留 query 与 hash——这是对旧链接的兼容处理;
  • 页面间的相对链接由 page_href() / rewrite_relative_url() 统一重写成基于路由的相对地址(docs/serve.py)。注意其边界条件:以 #/ 开头、带 scheme 或域名、以及**向上跳出版本目录(../)**的目标都不会被重写。因此写文档时,跨目录引用要么用同目录相对路径、要么用外部 URL,不要依赖 ../ 跨越站点根。

内置的 Markdown 语法支持

_render_blocks()docs/serve.py)按块级元素逐个识别,支持的特性包括:

语法 渲染行为
围栏代码块 ``` 输出 <pre><code class="language-xxx">,语言名取自围栏后缀;模板侧还会为每个代码块注入 copy 按钮(docs/template.html
#~###### 标题 生成 slug 锚点 id,并追加 hover 才显示的 headerlink# 永久链接)
--- 分隔线 <hr>
表格 识别分隔行中的 :---:--:---: 并转换为 th/td 的左右/居中对齐样式
有序/无序列表 支持嵌套(缩进按 4 空格一级)、同一层级 ul/ol 混排
> 引用块 普通 blockquote;若首行为 [!NOTE][!TIP][!IMPORTANT][!WARNING][!CAUTION],则渲染为带颜色左边框的 admonition 提示框(docs/template.html
行内 HTML / 注释 原样保留(会先对其中的 href/src 做相对路径重写)
行内语法 反引号行内代码、图片、链接、**粗体***斜体*、行尾双空格换行,以及裸 URL 自动加链autolink_plain()

另外 esc() 在转义 HTML 时会保留已存在的 HTML 实体(如 &amp;&#x27;)不被二次转义(docs/serve.py)。

全局术语表注入

inject_glossary()docs/serve.py)会在除术语表页以外的所有页面正文中,把 GLOSSARY_DESCRIPTIONS 定义的术语(如 "onroad"、"route"、"segment"、"panda")替换为带悬停 tooltip 的链接,点击可跳转到术语表对应锚点。注入时会跳过 codeh1~h6prekbdscriptstyle 等标签内部(GLOSSARY_SKIPdocs/serve.py),避免在代码或标题里误伤词面。tooltip 的悬浮样式见 docs/template.html

页面标题的取法

page_title()docs/serve.py)取文档第一个 # 开头行作为 <title>,最终呈现为 "页标题 · openpilot docs"(docs/template.html);若文档没有一级标题则回退为 "openpilot docs"。所以每篇文档的第一行 # 标题 直接决定浏览器标签与站点导航外的页面身份,撰写时不要省略。

CI 发布链路:从 master 推送到线上站点

docs/README.md 提到站点由工作流在推送 master 时更新,对应 .github/workflows/docs.yaml。该工作流的关键环节:

  1. 触发条件master 分支 push、任何 PR,以及可带 run_number 输入的 workflow_call;通过 concurrency 组 + cancel-in-progress: true 保证同分支只保留最新一次运行;
  2. 环境准备:runner 为 ubuntu-24.04,checkout 带子模块,并通过环境变量 lfs.fetchexclude 在 clone 阶段排除 openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx 这个大文件(docs.yaml);
  3. 构建:先 git lfs pullpython docs/serve.py --builddocs.yaml)。git lfs pull 这一步是必需的——因为 .gitattributes*.png*.svg*.ttf 等资源声明为 LFS 对象,docs 站点要拷贝的静态资源正是这些文件;
  4. 推送线上(仅当 github.ref == 'refs/heads/master' 且仓库为 commaai/openpilot 时执行):
    • 使用 secret OPENPILOT_DOCS_KEY 以 SSH 检出独立仓库 commaai/openpilot-docsopenpilot-docs/ 目录(docs.yaml);
    • 通过 source tools/release/identity.shtools/release/identity.sh 统一设置提交者身份)创建一个 orphan 分支,清空后执行 cp -r ../docs/_site/ docs/
    • touch docs/.nojekyll 关闭 Jekyll 处理,并写入 CNAME 内容为 docs.comma.ai
    • 提交后 git push -f origin tmp:gh-pages,即文档最终托管在该仓库的 gh-pages 分支上。workflow 中的注释也说明了独立仓库的动机:避免构建产物撑大 openpilot 主仓库的全量克隆体积。

撰写与维护文档的实操建议

结合上述实现,向 docs/ 贡献内容时可以遵循以下约定:

  • 新页面:放在 how-to/concepts/contributing/ 等已有分类下;首行必须是一级标题 # ...(它决定页面标题);如希望出现在侧边栏,需要更新 docs/serve.pyNAV
  • 相对链接:只写同目录或站内相对路径,避免依赖 ../ 上跳;外部链接会保持原样输出;
  • 提示框:在引用块首行写 [!NOTE] / [!TIP] / [!IMPORTANT] / [!WARNING] / [!CAUTION](大小写不敏感),即可获得对应配色的 admonition 样式;
  • 术语:新增术语应加进 GLOSSARY_DESCRIPTIONSdocs/serve.py),全站正文会自动获得 tooltip 链接,术语表页也会同步更新;
  • 资源文件:图片等资源放 docs/assets/ 或就近目录,copy_assets() 会原样拷贝;注意二进制资源走 Git LFS,拉取代码后若资源为空壳,本地预览前需先 git lfs pull(CI 正是这么做的);
  • 验证方式:改完跑一次 python docs/serve.py --build 确认输出 docs: built N pages into ... 且无异常,或直接用 python docs/serve.py 在浏览器里检查渲染效果与导航高亮。

整套机制的核心思想是:文档以纯 Markdown 维护、以零依赖脚本构建、以独立仓库 + gh-pages 分支托管,构建逻辑集中在单个 docs/serve.py 中,本地开发(热重载预览)与线上发布(CI 构建推送)复用同一条构建路径,保证所见即所得。

登录后查看全文
热门项目推荐
相关项目推荐