系列目录 · 工程与交付 · Read in English
一位同事想验证两行修改,运行构建脚本,编译到一半失败。问题不大,先用上次安装的工具继续排查——然后发现安装目录已经空了。
“我按的是构建按钮,为什么旧工具也被收走了?”
这类困惑通常说明,脚本把几个职责揉成了一个动作:加载环境、同步依赖、配置工程、编译、清空安装前缀、安装、裁剪产物。每一步单看都合理,放在一起却很难回答“失败到这里,哪些东西仍然可靠”。
所审阅的演进把流程拆为环境准备、构建、安装三个阶段,并让编译不再顺便清理安装产物。这不是单纯多出两个脚本,而是重新定义失败边界。
三个目录,三种所有权
源目录保存输入,构建目录保存由输入推导出的中间结果,安装目录保存给下游消费的接口。它们可能在同一棵工作区中,但不应该拥有相同的生命周期。
1 | 源文件与明确依赖 |
假设工程有一个生成 Python 辅助文件的步骤。如果配置时直接把它写进安装目录,那么“只运行配置”已经改变了下游能看见的状态;而安装清理又可能把刚生成的文件删掉。把生成物先放在构建树,再由安装规则复制,能够让依赖图更真实:生成文件属于构建产物,安装是另一个消费它的动作。
同样,构建脚本自动更新外部仓库会让同一次命令兼具网络同步与编译两个含义。源码没改,依赖可能变;网络断了,本来可以离线完成的编译也失败。把依赖准备作为显式前置条件,能减少这些不确定性。
环境脚本为什么需要“版本”
新的 shell 并不知道当前工程的安装前缀、依赖位置和 Python 搜索路径。环境脚本需要建立这些约定,但 shell 的寿命可能长于脚本文件的某个版本。
于是会出现一个微妙问题:磁盘上的脚本已经更新,终端里仍保留昨天导出的变量。仅检查“变量非空”不够,因为非空值可能属于另一份工作区或旧规则。
一种简单做法是给环境契约设置修订标记,构建与安装入口检查标记和必要变量。标记不匹配时,尽早提示重新加载环境。它的价值在于把“构建到一半才暴露路径不一致”变成“启动前就发现前置条件缺失”。
但标记不是完整环境指纹。即使标记正确,工具链文件仍可能被替换,用户也可能手动覆盖某个变量。更强的可复现性需要记录工具版本、依赖版本和配置参数。不能把一个环境修订字符串说成已经锁定所有输入。
反复加载环境,会发生什么
最常见的实现是每次把目录直接放到 PATH 前面。加载十次之后,十份相同路径排成一列;切换工作区后,新旧路径又交错出现。
更稳妥的策略是按路径项检查是否已经存在,再决定是否添加。注意检查的是完整项,而不是简单子串:/opt/compiler 与 /opt/compiler-old 不是同一个目录。
另一条约束是工作区隔离。构建树与安装树若仍指向上一份工程,即使当前源码根目录已更新,也可能把两个版本混在一起。因此环境准备需要检查关联路径是否属于当前工作区,并给出明确的覆盖机制。
原脚本对这些关系建立了检查,但一般的路径前缀判断仍不等价于规范化后的文件系统归属证明。涉及清理时,还应考虑符号链接、父目录跳转和根目录特殊情况。这是进一步的设计要求,不应倒推成某个历史版本已经完全实现。
安装为什么值得成为独立阶段
独立安装入口可以集中回答四个问题:
- 构建目录是否配置完成?
- 安装目标是否是本工程可以替换的目录?
- 安装之后哪些文件必须存在?
- 下游怎样知道这份产物来自哪次构建?
最后一个问题很容易被忽视。一个目录里有可执行文件,不代表它来自当前源码。可以增加一份安装收据,记录源码修订、构建类型、依赖安装位置和关键产物。收据不是完整的供应链证明,却能让“我用的到底是哪一版”有一个可检查的起点。
更重要的是收据应在安装检查完成后写入,或者至少区分“正在安装”与“可使用”。如果刚写收据就崩溃,下游可能把半成品当成成功安装。所阅读的脚本先安装、检查关键路径,再写入收据;这支持阶段完成后的记录方式,但不等于目录替换已经原子化。
一个失败矩阵,比十次顺利构建更有价值
可以用下表审视设计:
| 失败时刻 | 理想的可观察状态 |
|---|---|
| 环境未加载 | 无产物改动,直接说明缺少什么 |
| 依赖前缀不存在 | 配置前失败,指出具体依赖 |
| 编译失败 | 保留诊断和中间文件,不冒充安装成功 |
| 安装缺少必要模块 | 不生成“完整交付”的结论 |
| 安装前缀误指向共享目录 | 拒绝清理 |
| 修改环境脚本后沿用旧 shell | 可识别契约版本不一致 |
| 切换工作区 | 关键路径指向同一工作区 |
还应覆盖路径包含空格、变量未设置、不同构建模式和显式自定义依赖。脚本里的默认值也需要作为接口来测试:默认 Debug 还是 Release,不能只由文档猜测;历史上默认值变化时,应把代码与文档的时期对应起来。
拆分会不会让日常使用更麻烦
多一步命令确实增加操作成本。可以在上层提供组合入口,但组合入口应明确表达自己要做哪些阶段。简单任务用组合入口,排障时使用单阶段入口,两者不矛盾。
性能收益也应谨慎描述。避免每次构建都清理安装树,可以减少重复工作;依赖显式化可能让增量构建更稳定。但没有测量就不能给出“构建快了多少”。若要验证,应分别记录配置、编译、安装和打包耗时,不能把缓存冷热或网络变化算成脚本优化。
真正可立即解释的收益是状态更容易理解:编译失败不会自动意味着安装失败,安装完成也不再只是某个脚本最后一行恰好打印了成功。
当下一位同事问“我可以只重编译而保留当前工具吗”,系统能给出清楚答案,工程接口才算变得可用。
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !