From 3075a15e5bcde05ba77b3c256c88c37dd0ae7b52 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:32:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(macos):=20Homebrew=206=20=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E6=96=B9=20tap=20=E4=BF=A1=E4=BB=BB=E9=97=A8=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E6=96=87=E6=A1=A3=E8=A1=A5=20brew=20trust,fresh-in?= =?UTF-8?q?stall=20=E5=8A=A0=20brew=20=E9=AA=8C=E8=AF=81=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 报错 Refusing to load formula mcpp-community/mcpp/mcpp-m from untrusted tap mcpp-community/mcpp. ## 为什么一直没被发现 Homebrew 6 拒绝从未信任的第三方 tap 加载公式,但 `brew install //` 是豁免的 —— 它被读作显式意图。 README 文档化的正是这条全限定命令,**所以它一直是绿的**,而用户真正会敲的 其它每一种拼写都是坏的。 真机实测(macOS 14.8.7 / 15.7.7 / 26.5.2,Homebrew 6.0.5 / 6.0.12 / 6.0.13, 临时探针 PR #419): | 命令 | rc | |---|---| | `brew install mcpp-community/mcpp/mcpp-m`(未 tap / 已 tap) | 0 / 0 | | `brew install mcpp-m`(短名) | **1** | | `brew install mcpp-community/mcpp/mcpp`(alias) | **1** | | `brew trust mcpp-community/mcpp` | 0 | | trust 后:短名 / alias / upgrade / new+run | 全 0 | alias 本身没坏:报错里点名的是它解析到的 `mcpp-m`,被拒的是**信任**。 ## 改动 - README.md / README.zh-CN.md:安装段写清信任门与 `brew trust` 一行修法 - ci-fresh-install.yml 新增 `macos-brew-fresh` job ## 为什么是独立 job 而不是加进 macos-fresh 那个 job 已经有一份经 xlings 装的 mcpp 在 PATH 上,再 brew 装一份会让下面每 一条断言都说不清自己量的是哪个二进制。 它也**不需要 `wait-index`**:brew 直接装 GitHub release tarball,而 tap 的 bump 工作流比 release 晚几分钟 —— 在这里断言"刚发布的版本"会因为与 mcpp 无关 的原因变 flaky。版本断言改成**自洽**的:tapped 公式声明什么版本,装出来的二进制 就必须报什么版本。 ## 这个 job 从两侧钉住信任门 只断言"trust 后短名能用"是不够的 —— 那分不清"门被正确处理了"和"这个 Homebrew 根本没有门",而一个不可能失败的测试与没在跑的测试无从区分。所以先 断言**未信任时被拒、且拒绝理由确实是 untrusted tap**(不是别的新缺陷),再断言 trust 之后短名/alias/upgrade 全通。将来 Homebrew 撤掉这道门,这一步会变红并说明 原因,而不是继续假装在保护什么。 另加两条只有真实用户会碰到的判据:装完能 `mcpp new` + `mcpp run` 真跑起来 (过 launcher 的 MCPP_HOME/MCPP_VENDORED_XLINGS 钉法与工具链自举),以及 per-user 状态确实落在 ~/.mcpp 而不是 Cellar(否则 `brew upgrade` 会把用户装 的工具链一起带走)。 tap 侧同步:mcpp-community/homebrew-mcpp@e5bdc21(README 那句 "short forms work too" 与 alias 宣传都已改准,公式 caveats 补了 brew trust)。 --- .github/workflows/ci-fresh-install.yml | 123 +++++++++++++++++++++++++ README.md | 15 +++ README.zh-CN.md | 15 +++ 3 files changed, 153 insertions(+) diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 82596446..a84005e9 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -404,6 +404,129 @@ jobs: mcpp clean mcpp run + # ────────────────────────────────────────────────────────────────── + # macOS via HOMEBREW — the other fresh-install channel + # + # A SEPARATE JOB, not extra steps in macos-fresh: that job already has an + # mcpp on PATH from xlings, and a second one from brew would make every + # assertion below ambiguous about which binary it measured. + # + # It also does NOT need `wait-index`. Homebrew installs the GitHub release + # tarball directly, and the tap's own bump workflow lags the release by + # minutes — asserting the just-released version here would be racy for a + # reason that has nothing to do with mcpp. The version assertion is instead + # made SELF-CONSISTENT: whatever version the tapped formula declares is the + # version the installed binary must report. + # + # WHAT THIS EXISTS TO CATCH (measured on macOS 14.8.7 / 15.7.7 / 26.5.2, + # Homebrew 6.0.5 / 6.0.12 / 6.0.13): + # + # Homebrew 6 refuses to load a formula from an untrusted third-party tap. + # `brew install //` is exempt — it reads as explicit + # intent — so the one-liner in README.md kept passing while EVERY other + # spelling a user reaches for was broken: + # + # brew install mcpp-m → exit 1, "untrusted tap" + # brew install mcpp-community/mcpp/mcpp → exit 1, "untrusted tap" + # + # A CI job that only ran the documented one-liner would have reported this + # channel healthy the entire time. So the trust gate itself is asserted + # from BOTH sides: refused before `brew trust`, accepted after. + macos-brew-fresh: + name: macOS fresh install (Homebrew) + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + # Same floor as macos-fresh: the formula declares `depends_on macos: + # :sonoma` + arm64, and macos-14 is the oldest image satisfying it. + runs-on: macos-14 + timeout-minutes: 30 + steps: + - name: Environment + run: | + echo "macOS $(sw_vers -productVersion) | $(uname -m) | $(brew --version | head -1)" + + # ① The command README.md documents, on a machine that has never tapped. + # Fully qualified, so Homebrew treats it as explicit intent. + - name: "Documented one-liner installs" + run: brew install mcpp-community/mcpp/mcpp-m + + # ② The launcher is the whole point of the formula: mcpp WRITES at + # runtime, so a bare symlink into the Cellar would make MCPP_HOME the + # versioned Cellar dir and `brew upgrade` would drop every installed + # toolchain. Assert the binary on PATH is brew's and reports the + # version the FORMULA declares — not a hardcoded one, which would go + # stale on every release. + - name: "Installed binary is brew's, and agrees with the formula" + run: | + set -euo pipefail + which mcpp + case "$(which mcpp)" in + "$(brew --prefix)"/bin/mcpp) ;; + *) echo "FAIL: mcpp on PATH is not the brew one"; exit 1 ;; + esac + formula_version="$(brew info --json=v2 mcpp-community/mcpp/mcpp-m \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["formulae"][0]["versions"]["stable"])')" + echo "formula declares: $formula_version" + mcpp --version + mcpp --version | grep -Fq "$formula_version" + + # ③ THE TRUST GATE, from the failing side first. + # + # Asserted before the fix is applied, because "short form works" alone + # cannot distinguish "the gate is handled" from "this Homebrew has no + # gate" — and a test that cannot fail is indistinguishable from one + # that is not running. If a future Homebrew drops the gate, this step + # turns red and says so, rather than silently protecting nothing. + - name: "Untrusted tap: short form is refused (states its own premise)" + run: | + set +e + brew uninstall --force mcpp-m > /dev/null 2>&1 + out="$(brew install mcpp-m 2>&1)"; rc=$? + echo "$out" | tail -5 + if [ $rc -eq 0 ]; then + echo "NOTE: this Homebrew did not gate the tap — the trust step below" + echo " is now a no-op and README's warning can be revisited." + exit 0 + fi + echo "$out" | grep -Fq 'untrusted tap' || { + echo "FAIL: short-form install failed for some OTHER reason than the" + echo " trust gate — that is a new bug, not the known one." + exit 1 + } + + # ④ …and from the working side. `brew trust` is what README tells users + # to run, so it is what CI runs. + - name: "brew trust unlocks the short form, the alias, and upgrade" + run: | + set -euo pipefail + brew trust mcpp-community/mcpp + brew uninstall --force mcpp-m + brew install mcpp-m # short form + brew uninstall --force mcpp-m + brew install mcpp-community/mcpp/mcpp # the `mcpp` alias + brew upgrade mcpp-m || true # no-op when current; must not be refused + + # ⑤ The only assertion a user actually cares about: it builds and runs. + # Exercises the launcher's MCPP_HOME/MCPP_VENDORED_XLINGS pinning, + # the bundled xlings, and a real toolchain bootstrap. + - name: "Real use: mcpp new → run (toolchain bootstrap)" + run: | + set -euo pipefail + cd "$(mktemp -d)" + mcpp new brewhello + cd brewhello + mcpp run | tee run.log + grep -Fq 'Hello from brewhello' run.log + + # ⑥ Per-user state must live outside the Cellar, or `brew upgrade` takes + # the user's toolchains with it. §⑤ just created it — prove where. + - name: "State lives in ~/.mcpp, not the Cellar" + run: | + set -euo pipefail + test -d "$HOME/.mcpp" + if find "$(brew --prefix)/Cellar/mcpp-m" -name 'toolchain*' -o -name 'xpkgs' 2>/dev/null | grep -q .; then + echo "FAIL: toolchain state landed inside the Cellar"; exit 1 + fi + # ────────────────────────────────────────────────────────────────── # Windows WITH Visual Studio: llvm@20.1.7 + MSVC STL # diff --git a/README.md b/README.md index 530d81f9..8a2339c7 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,21 @@ macOS 14; per-user data lives in `~/.mcpp/`. Homebrew's `mcpp` is an unrelated C preprocessor, hence the `mcpp-m` formula name — the command it installs is still `mcpp`. +**Homebrew 6 gates third-party taps.** The fully-qualified command above is +read as explicit intent and works as-is, but every *other* spelling — the short +`brew install mcpp-m`, the `mcpp` alias, and later upgrades — is refused with: + +``` +Refusing to load formula mcpp-community/mcpp/mcpp-m from untrusted tap +mcpp-community/mcpp. +``` + +Trust the tap once and all of them work: + +```bash +brew trust mcpp-community/mcpp +``` +
diff --git a/README.zh-CN.md b/README.zh-CN.md index be4ac637..30192ac6 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -126,6 +126,21 @@ brew install mcpp-community/mcpp/mcpp-m Homebrew 上 `mcpp` 属于一个无关的 C 预处理器,所以公式名是 `mcpp-m`, 装出来的命令仍然是 `mcpp`。 +**Homebrew 6 对第三方 tap 加了信任门。** 上面那条全限定命令会被当作显式意图、 +可以直接用;但**其它任何拼写**——短名 `brew install mcpp-m`、`mcpp` 别名、 +以及之后的升级——都会被拒: + +``` +Refusing to load formula mcpp-community/mcpp/mcpp-m from untrusted tap +mcpp-community/mcpp. +``` + +信任这个 tap 一次,它们就都能用了: + +```bash +brew trust mcpp-community/mcpp +``` +