Skip to content

MacWave 2.5.2

Choose a tag to compare

@Sha0huaZhang Sha0huaZhang released this 06 Oct 17:41
· 38 commits to main since this release

Release Note 更新日志

一次以实测驱动的修复版:在隔离沙箱里端到端跑安装/卸载,找出并修掉了几个缺陷(其中一个可能造成不可逆的损害),并把这套隔离测试固化成了仓库里的回归脚本。

修复:按官方写法卸载会删掉 /bin/bash

文档给出的卸载命令一直是 /bin/bash -c "$(curl -fsSL ...)"。在这种调用方式下,$0 不是脚本路径,而是解释器自身;带参数时则是 --:

/bin/bash -c "$(curl ...)"              →  $0=/bin/bash  →  收尾执行 rm -f /bin/bash
/bin/bash -c "$(curl ...)" -- --force   →  $0=--         →  收尾执行 rm -f --(missing operand)

而卸载器收尾有一句 rm -f "$0",本意是删掉刚刚下载下来的脚本。于是按官方文档卸载、并且用了 sudo 的话,会把系统的 /bin/bash 删掉——此后任何 #!/bin/bash 脚本都再也跑不起来。

现在只有当 $0 确实是一个含本脚本标记的普通文件时才自删;$0 是解释器路径、--,或是不含标记的其它文件时一律跳过。以文件方式运行(bash uninstall.sh)仍然和以前一样删除自己。

修复:卸载器在无终端时会照删不误

卸载器第一次确认写的是 read -n 1 -r(读 stdin),而取消条件要求 REPLY 非空。stdin 是 EOF(< /dev/null、CI、被别的命令吃掉输入)时 REPLY 为空,于是跳过取消、直接删除:

bash uninstall.sh < /dev/null    →  安装树、配置、PATH 全被删掉(预期:应拒绝)
echo n | bash uninstall.sh       →  取消(这条一直是对的)

删除不可逆,不能把「读不到回答」当成「同意」。现在改为读 /dev/tty,读不到就中止并退出 1,同时提示需要显式加 --force。

修复:拒绝许可协议后留下悬空 PATH

「安装完成」的提示排在协议确认之前,而拒绝时会删掉安装树与配置目录——于是一个拒绝协议的用户会先被告知安装成功,随后一切被清空,而且 ~/.zshrc 里那行 PATH 没人清,留成指向已删目录的死条目。

现在:协议在「安装完成」之前询问;拒绝时把安装树、配置目录与 rc 里的 PATH 一起回滚干净。

顺带修掉同一条路径上的提权问题:清理配置目录那一行原本写死 sudo,而它上一行用的是按需提权的 run_cmd。用户级安装全程不需要 sudo,却在最后一步索要密码;sudo 不可用时会让清理中断在半路(配置目录残留)。

修复:-h / --help 对子命令不成立,且 selfupdate --help 会真的自更新

帮助里写着 -h, --help Show help for any command,实测却是一地鸡毛:

wave install --help     → Error: Input contains illegal fields.        exit=1
wave uninstall --help   → Warning: unknown flag '--help' ignored. 然后继续执行  exit=1
wave link --help        → 被当成链接请求:Error: Nothing to link.      exit=1
wave list --help        → 静默忽略                                     exit=0
wave selfupdate --help  → 🌊 Fetching the latest version...  ← 真的开始自更新

selfupdate 这条最要命:在旧版本上 wave selfupdate --help 会直接升级。现在在命令分发之前统一拦下 -h / --help,打印该命令自己的用法(用法行、旗标、示例)后以 0 退出,覆盖 install / uninstall / list / search / info / selfupdate / link / unlink / linkquery / version。旗标列表按各 handler 的白名单写,不承诺做不到的事。

修复:中途失败只留一个光秃秃的 curl 退出码

安装过程中途失败时(网络中断、权限、磁盘满),用户只看到 curl 的错误与一个非零退出码,不知道该做什么;而残留的安装树与配置会让人以为「已经装过」。

现在会打印:安装未完成(含退出码)、没有删除任何东西、已下载内容可能残留的具体路径、以及「修好后重跑即可,重跑是安全的」。刻意不自动删除——升级安装时删掉安装树会把用户原有的可用安装一起毁掉。

实现上要给脚本加 set -E(errtrace):默认情况下 ERR 陷阱不继承进函数,而下载是在 run_cmd 函数里跑的。set -E 之后,显式 return 1、调用方用 || 容忍的失败、if 条件里的失败都不会误触发(已逐条验证)。

新增:隔离安装测试 scripts/sandbox_test.sh

把这次的实测手法固化成回归脚本,64 条断言,全程跑在 macOS 自带的 sandbox-exec 里(默认拒绝,只允许写沙箱目录、/dev 与临时目录):/opt、/usr/local、/etc 与真实家目录一个字节都不会被写;取数改成 file:// 本地镜像(只改 BASE_URL / CONFIGDATA_URL 两行),因此不需要网络。

覆盖:用户级安装目录端到端(安装树 / 配置落位 / VERSION.json / PATH / 入口可执行)→ 系统级目录的落位(越过沙箱边界即失败,并打印选中的安装与配置目录)→ 参数校验 → 协议顺序与拒绝后的完整回滚 → 中途失败的指引且不误删 → 卸载确认语义(无终端 / n / y / --force)→ 卸载器自删($0 是文件 vs 解释器,且 /bin/bash 必须存活)→ 复用 link_test.sh。另有一组沙箱自检:断言 /opt 不可写、沙箱内可写——这组要是挂了,说明隔离没生效,其余断言都不可信。

macOS 侧的范围差异(写在脚本头部):LinuxWave 用 unshare -rm 做用户+挂载命名空间并把系统目录换成影子副本;macOS 没有这些,改用 sandbox-exec 的「拒绝写沙箱外」。因此系统级目录只断言落位(安装必然在越过边界时失败,而失败指引会打印选中的目录),另外没有共享安装与账号删除那一组断言(那是 Linux 专有功能)。

其他改动

  • 版本号 2.5.1 → 2.5.2,构建号 280H0111
  • README.md:无人值守安装统一为 /bin/bash -c "$(curl …)",选项放在 -- 之后;补充「不带 --force 时卸载器需要终端」的说明
  • PROJECT_CONTEXT.md:登记新脚本,并记录本次全部修复
  • configdata:latest_version 更新为 2.5.2,并补上 updatedata/2.5.2(安装器按完整版本号查找迁移目录,缺了它,从 2.5 之前升级到 2.5.2 就不会执行 2.5 的配置迁移)
  • 网站显示的版本号同步为 2.5.2

Release Notes

A fix release driven by end-to-end testing: the installer and uninstaller were run inside an isolated sandbox, which surfaced several defects (one of them capable of irreversible damage). The isolation harness itself is now a regression script in the repository.

Fix: the documented uninstall command deleted /bin/bash

The documented uninstall command has always been /bin/bash -c "$(curl -fsSL ...)". Under that invocation $0 is not the script path but the interpreter itself, or -- when arguments follow:

/bin/bash -c "$(curl ...)"              →  $0=/bin/bash  →  ends with rm -f /bin/bash
/bin/bash -c "$(curl ...)" -- --force   →  $0=--         →  ends with rm -f --  (missing operand)

The uninstaller ended with rm -f "$0", there to remove the script it had just downloaded. So uninstalling the documented way, with sudo, deleted the system's /bin/bash - after which no #!/bin/bash script could run again.

The self-delete now happens only when $0 is a regular file carrying this script's marker; an interpreter path, --, or an unrelated file is left alone. Running it as a file (bash uninstall.sh) still removes itself exactly as before.

Fix: the uninstaller deleted anyway when there was no terminal

The first confirmation used read -n 1 -r (from stdin) and only cancelled when REPLY was non-empty. With stdin at EOF (< /dev/null, CI, or input consumed by something else) REPLY stayed empty, so it skipped the cancel and deleted:

bash uninstall.sh < /dev/null    →  tree, config and PATH all removed (should have refused)
echo n | bash uninstall.sh       →  cancelled (this path was always correct)

Deleting is not reversible, so "no answer" must not mean "yes". It now reads /dev/tty and stops with exit 1 when it cannot, telling the user to pass --force.

Fix: declining the agreement left a dangling PATH entry

The "Installation complete!" banner was printed before the agreement gate, and declining deletes the install tree and config - so someone who declined was first told the install succeeded, then had everything wiped, with the PATH line still sitting in ~/.zshrc pointing at a directory that no longer exists.

The agreement is now asked before the banner, and declining rolls back the tree, the config directory and the rc PATH entry together.

A privilege bug on the same path is fixed too: the config cleanup hardcoded sudo while the line above it used the on-demand run_cmd. A user-level install needs no sudo at all, yet asked for a password at the last step; where sudo was unavailable the cleanup aborted halfway, leaving the config behind.

Fix: -h / --help did not work per command, and selfupdate --help performed an update

Help promises -h, --help Show help for any command, but reality was a mess:

wave install --help     → Error: Input contains illegal fields.        exit=1
wave uninstall --help   → Warning: unknown flag '--help' ignored. then ran anyway  exit=1
wave link --help        → treated as a link request: Error: Nothing to link.  exit=1
wave list --help        → silently ignored                              exit=0
wave selfupdate --help  → 🌊 Fetching the latest version...  ← actually started updating

The selfupdate one matters most: on an older install, wave selfupdate --help upgraded it. -h / --help is now intercepted before command dispatch and prints that command's usage (usage line, flags, examples) with exit 0, for install / uninstall / list / search / info / selfupdate / link / unlink / linkquery / version. The flag lists come from each handler's own whitelist, so nothing is promised that the command does not accept.

Fix: a mid-install failure left only a bare curl exit code

When an install failed partway (network drop, permissions, disk full) all the user saw was curl's error and a non-zero status, with no idea what to do - and the leftover tree and config made it look installed.

It now prints: the install did not finish (with the exit code), that nothing was removed, the exact paths where downloaded content may be left, and that rerunning the installer is safe and will overwrite what it downloaded. It deliberately does not delete anything on its own: on an upgrade, removing the tree would destroy the user's working install.

This needed set -E (errtrace): by default an ERR trap is not inherited into functions, and the download runs inside run_cmd. With -E on, explicit return 1, failures the caller tolerates with ||, and failures inside if conditions all stay silent (each verified).

New: scripts/sandbox_test.sh

The testing behind this release is now a regression script with 64 assertions, running inside macOS's own sandbox-exec (default-deny, with only the sandbox directory, /dev and the temp dirs writable), so not a byte of /opt, /usr/local, /etc or the real home is written. Fetches point at a local file:// mirror (only BASE_URL / CONFIGDATA_URL change), so it needs no network.

Coverage: user-level install end to end (tree, config placement, VERSION.json, PATH, executable entry) → system-level directories' placement (the install must fail at the sandbox boundary, and the failure prints the chosen install and config directories) → argument validation → agreement ordering and full rollback after declining → mid-failure guidance without deleting anything → uninstall confirmation semantics (no terminal / n / y / --force) → uninstaller self-delete ($0 as a file vs the interpreter, and /bin/bash must survive) → plus link_test.sh. A separate sandbox self-check asserts /opt is unwritable and the sandbox is writable - if that group fails, the isolation is not in effect and the rest cannot be trusted.

macOS-specific scope, documented in the script header: LinuxWave uses unshare -rm for a user+mount namespace and swaps the system directories for shadow copies; macOS has neither, so this uses sandbox-exec's "writes outside the sandbox are denied". System-level directories are therefore asserted for placement only (the install necessarily fails at the boundary, and the failure prints the chosen directories), and there are no shared-install or account-removal assertions, since those are Linux-only features.

Other Changes

  • Version 2.5.1 → 2.5.2, build number 280H0111
  • README.md: unattended install is uniformly /bin/bash -c "$(curl …)" with options after --; adds a note that the uninstaller needs a terminal unless --force is given
  • PROJECT_CONTEXT.md: registers the new script and records these fixes
  • configdata: latest_version is now 2.5.2, plus updatedata/2.5.2 (the installers look up the migration directory by the full version, so without it a pre-2.5 upgrade to 2.5.2 would skip the 2.5 config migration)
  • The version shown on the site is now 2.5.2

Install 安装

运行以下命令以下载此版本

Run the following command to download/update to this version.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/MacWaveOrg/MacWave/HEAD/lib/install.sh)" && source ~/.zshrc

无人值守安装(选项放在 -- 之后)

Unattended install (options go after --)

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/MacWaveOrg/MacWave/HEAD/lib/install.sh)" \
  -- --silent --dir-option=1