我和 Codex 连夜给 drwateR 屎山包做体检

一份不太严肃但很认真的 drwateR 核心 R 包改造记录:依赖、跨平台、安全、文档、版本、NEWS、Git、SVG logo 和 DRAN 发布一次排查。
软件
R
DRAN
作者

苏 命

Codex

发布于

2026年09月06日

这不是重构,这是给代码做体检

事情的起因很简单:我看了一眼 drwateR 相关的 R 包,然后 Codex 看了一眼我。空气里出现了短暂的沉默,像医生打开体检报告后先把眼镜摘下来。

这些包不是不能用。恰恰相反,它们能做不少事,只是有些地方带着明显的“当年先跑起来再说”气质:README 像家族相册,版本号分散在不同角落,发布依赖记忆力,跨平台行为偶尔相信祈祷,旧 tarball 还可能在关键时刻冒充新包。

于是我们决定不再给屎山喷香水,而是把它拆开,一块一块编号、测试、记录,最后送进 DRAN。项目 Git 入口是 git.drwater.net,R 包仓库集中在 DRWATER Git 组织,构建后的包则放在 DRAN。

从屎山现场到 DRAN 的路线图。

第一项工作:确认到底有几座山

本轮只处理核心 10 个包。dateR1 不再维护,customsetup 已经退休,它们被请出了本次体检中心。核心包各自分工如下,像一个不太整齐但确实能干活的项目组:

包 主要职责 仓库 SVG logo
drwateR 生态入口、公共工具、DRAN 更新检查 Git logo.svg
dfeR 数据格式与数据处理 Git logo.svg
dateR 日期和时间处理 Git logo.svg
langeR 语言与国际化辅助 Git logo.svg
uniteR 单位、量纲与统一表达 Git logo.svg
cctdb 数据库访问与数据接口 Git logo.svg
cctda 藻类与水环境数据分析 Git logo.svg
dwfun 通用函数与分析辅助 Git logo.svg
rmdify R Markdown、Quarto 与复杂标记 Git logo.svg
figeR 图形、图件与绘图工作流 Git logo.svg

十个 logo,先让它们认领自己的工位

这些 SVG logo 直接来自各仓库的 main 分支。下面把它们真的展示出来,免得“有 logo”最后只停留在 DESCRIPTION 的文学创作里。

drwateR hex logo dfeR hex logo dateR hex logo langeR hex logo uniteR hex logo cctdb hex logo cctda hex logo dwfun hex logo rmdify hex logo figeR hex logo

第二项工作:把“能跑”翻译成“别人也能接手”

每个包都补上了 <pkg>_version()、<pkg>_info() 和 <pkg>_lifecycle()。这几个函数不负责治百病,但至少能回答三个经常被问、也经常没人敢回答的问题:你是谁、你现在是哪一版、你到底还活不活跃。

DESCRIPTION 里也明确记录生命周期,README 改成中英文双语,NEWS 不再只写“update”,而是记录这次到底改了什么。一个包终于不必通过观察作者的表情来判断自己是否处于 stable 状态。

第三项工作:让三个操作系统停止互相假装

macOS、Linux 和 Windows 被分别检查了路径、临时目录、可执行文件定位、文件查看器和 system2() 调用。结论很朴素:

  • 路径用 file.path(),不要手写斜杠迷宫。
  • 命令用 Sys.which() 或 R.home("bin") 定位,不要假设电脑上一定有某个绝对路径。
  • 不要把 .exe 当成全世界的后缀,也不要把 /bin/sh 当成跨平台人格。
  • system2() 可以继续用,但参数和命令名不能偷偷绑定某一台电脑。

换句话说,Windows 用户不应该因为没有 /usr/bin 就被开除出 DRWATER,Linux 用户也不应该因为没有 Finder 而失去打开文件的权利。

rmdify:正则表达式终于被请去喝茶

rmdify 的 clab 是本轮最像真实战场的地方。标记可能跨行、跨段落,里面有嵌套引用、公式、内联 R,还可能夹着图形 chunk。单条正则表达式面对这种文档时,通常会表现得像一只看见吸尘器的猫:先僵住,然后从最奇怪的方向逃跑。

现在解析器按状态机跟踪括号深度和代码围栏,支持旧式属性和 options clab="...",能处理跨段落内容。内联 2 会被处理,围栏中的 R/Quarto chunk 则保留而不擅自执行。图形引用用明确的 Quarto label:


::: {.cell}

```{.r .cell-code}
ggsavep("../figures/demo.pdf", loadit = TRUE)
```
:::

这条原则值得贴在办公室墙上:文档转换器可以读代码,但不要因为看见代码就顺手替用户执行一遍。

第四项工作:让发布流程不再依赖记忆力

核心包现在按 Makefile 流程更新版本、运行 roxygenise、生成 NEWS、检查、提交到 main、推送 commit、创建 tag、推送 tag,并构建 DRAN。DRAN 的入口是 dran.drwater.net,安装示例也统一指向它:

install.packages("drwateR", repos = "https://dran.drwater.net")

期间抓到了一个经典事故:根目录残留旧 tarball 时,构建过程可能“成功地”复制了错误版本。现在先清理旧 tarball,再从 DESCRIPTION 读取精确版本匹配构建产物。终端说成功之前,至少要确保它成功的是今天的包,而不是昨天的幽灵。

最后:屎山没有消失,但已经有门牌号了

这轮 10 个核心包都完成了版本更新、检查、commit、push、tag,并重新构建到本地 DRAN。drwateR 还提供更新检查:

drwateR::check_drwateR_updates()

它不会半夜替用户升级,也不会把电脑变成发布服务器,只负责说:“这里有新版本,请你自己做决定。”这是一个很健康的边界。

当然,历史示例和旧文档仍有后续清理空间,完整 examples 检查也应该逐步恢复。我们没有把技术债宣布为已灭绝物种,只是终于给它建立了台账。

从前有人问“这个包怎么发布”,答案可能是“找记得那段历史的人”。现在答案是:打开 Git,看 main,读 NEWS,跑 Makefile,最后去 DRAN 查包。

屎山还在,但它已经有版本号、测试、logo、门牌号和逃生通道了。对于软件来说,这已经不是奇迹,是卫生条件开始达标。