这不是重构,这是给代码做体检
事情的起因很简单:我看了一眼 drwateR 相关的 R 包,然后 Codex 看了一眼我。空气里出现了短暂的沉默,像医生打开体检报告后先把眼镜摘下来。
这些包不是不能用。恰恰相反,它们能做不少事,只是有些地方带着明显的“当年先跑起来再说”气质:README 像家族相册,版本号分散在不同角落,发布依赖记忆力,跨平台行为偶尔相信祈祷,旧 tarball 还可能在关键时刻冒充新包。
于是我们决定不再给屎山喷香水,而是把它拆开,一块一块编号、测试、记录,最后送进 DRAN。项目 Git 入口是 git.drwater.net,R 包仓库集中在 DRWATER Git 组织,构建后的包则放在 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 的文学创作里。
第二项工作:把“能跑”翻译成“别人也能接手”
每个包都补上了 <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、门牌号和逃生通道了。对于软件来说,这已经不是奇迹,是卫生条件开始达标。