一个让我的开源项目在 Linux 上 100% 装不上的回车符
我维护一个 GPU 能耗测量工具(MLCube 容器格式)。昨天一位复现者在干净的 Ubuntu 22.04 上跑"一键脚本",反馈:100% 失败。截图里第一行报错就是标题这句——`set: pipefail: invalid option name`。 熟悉的人一眼就能看出后面的 `\r`:这是 Windows 行尾符(CRLF)的经典症状。bash 读到 `pipefail\r` 时,`\r` 不是空白字符,于是把 `pipefail\r` 当成一个不存在的选项名。 ## 排查:比预想的严重得多 我一开始以为只是单个脚本的问题。逐个核对字节后发现问题全在:仓库里 **9 个 shell 脚本全部是 CRLF**——入口 `run.sh`(197 行)、`quickstart.sh`(368 行)、部署用的六个脚本,连站点的"下载器" stub 自己也是 CRLF。源头不是 release 上传时损坏,而是仓库 blob 本身就是 CRLF——在 Windows 上开发、git autocrlf 配置不严谨,行尾就这样一路带进了每个发布资产。 最有讽刺意味的一处:复现者下载完先做了 SHA-256 校验,**通过了**——然后脚本炸了。原因很简单:校验和就是对着 CRLF 版本算的,文件和校验和"自洽地错"。校验和保障的是完整性(没被篡改),不保障正确性(行尾本身就是错的)。一排错误的字节配上一个匹配错误的校验和,安全校验做得再好也拦不住。 ## 修复:四件事一起做 单把文件转回 LF 不够,得保证不再复发: 1. **源码全量转 LF**,9 个 `.sh` 逐一字节级替换,并补上仓库里缺失的 wrapper 源文件; 1. **`.gitattributes` 钉死规则**:`*.sh text eol=lf`——以后不管谁在什么平台上 clone、提交,仓库里的 `.sh` 永远是 LF; 1. **CI 加行尾守卫**:每次 CI 显式检查 `.sh` 不含 CR,从源头报警; 1. **重新发布**:发布 v2 资产并换绑,v1 保留作墓碑,不悄悄删——让老用户的报错可追溯。 替换旧 release 资产时还顺手抓到一个潜伏 bug:checksum 清单里 schema 文件的哈希**从来就没对上过**。为什么一直没被发现?因为校验流程测的是脚本资产,schema 那行从没人校验过——**没人去校验校验和文件本身**。发布即验证,发布即发现。 ## 修 CRLF 时踩的坑:在 Windows 上修 Windows 的问题 还有个工程细节值得一提:修复过程中我先用 Git Bash 的 `sed 's/\r$//'` 做替换,**静默失败**——MSYS 的 text-mode 会在管道里自动做行尾转换,导致你以为在删 `\r`,实际读进来的早就被转换过,`grep -c $'\r'` 的计数也会撒谎。最后是用 Python 的 `read_bytes`/`write_bytes` 做纯字节替换才可靠。也就是说:**在 Windows 上修 CRLF 问题,工具链本身就会被 CRLF 坑**。 ## 教训 - 一键脚本是新用户的**第一触点**,它挂了,别人对项目的第一印象就是"这项目装不上",后面写得再好都没机会展示; - Windows 上开发跨平台项目,`.gitattributes` 不是可选项,是第一天就该有的东西; - 校验和能自洽地错——它验证完整性,不验证正确性; - 见到 `xxx: invalid option name` 或 `$'do\r': command not found`,先查行尾,再查别的。