跳转至

2026-06-25

今日主题

  • Homebrew 与 Ruby 的关系
  • brew 的包管理与版本机制
  • mise 的环境变量与工具管理
  • macOS JDK 查找机制
  • Rust 工具链架构(rustup / cargo / rustc)
  • brew services 的完整生命周期
  • zsh 与 mise 初始化
  • mise env 语义
  • mise 与 IDE 启动语义
  • zsh 启动模式
  • mise 安全机制
  • zsh 配置文件命名之谜
  • AI 的幻觉式解释

新增认知

Homebrew 与 Ruby 的关系

  • Homebrew 自包含 Ruby 运行时,不依赖系统 Ruby:brew 命令本质是 Bash 脚本,
    启动链路为 /opt/homebrew/bin/brew(Bash)→ brew.sh(Bash)→ portable Ruby(内建于 /opt/homebrew/Library/Homebrew/vendor/portable-ruby/)。
    全程不碰系统 Ruby(/usr/bin/ruby)和 brew 安装的 ruby@3.2。

  • macOS 系统 Ruby 受 SIP 保护不可卸载:/usr/bin/ruby 位于 SIP 保护区,即使 root 也无法删除。
    Apple 从 Catalina 起不允许修改该路径,且部分内部工具可能引用它。最佳做法是无视它,让它安静待着。

brew 的包管理与版本机制

  • brew 是包管理器而非版本管理器:brew 的每个 formula 描述如何构建特定版本的软件,不带 @ 的 formula 跟踪最新稳定版,
    带 @ 的锁定大版本。同一 formula 在 Cellar 下只保留一个版本,旧版本会被覆盖。
    "多版本共存"只能通过 ruby 和 ruby@3.2 这样的独立 formula 实现,
    这是包管理器的通用设计(APT 的 openssl 和 openssl@1.1 同理),不是 brew 特有的。

  • brew link 是全量目录映射而非文件选择:brew 不判断哪些文件是 bin,
    只是把 Cellar 下 bin/lib/share/etc 等标准目录整个符号链接到 prefix。目录结构由软件自身的 make install 决定,
    brew 只负责搬运。带 @ 的 formula 默认 keg-only 不 link,因为其 bin 目录与不带 @ 的版本有同名文件冲突。

  • brew services 是声明式而非自动发现:服务不是扫描可执行文件得来的,
    必须由 formula 作者在 service do ... end 块中显式声明启动命令。brew install 只安装不启动,
    brew services start 才写 plist 到 ~/Library/LaunchAgents/ 并调用 launchctl load。
    start 之后即使 stop 也只是暂停当前进程,plist 还在,重启后 launchd 仍会自动拉起。
    彻底不让启动需要 brew services remove 删除 plist。

mise 的环境变量与工具管理

  • go env -w 写入全局文件,跨版本共享
    go env -w 写入 ~/Library/Application Support/go/env(macOS),不是一个环境变量,而是 Go 的全局配置文件。
    所有 Go 版本共用此文件,mise 切换版本不会改变其内容。如果需要按项目区分,应在 .mise.toml 的 [env] 段设置环境变量,
    环境变量优先级高于 go env -w 文件。

  • mise 复用已有工具安装而非重复下载:mise 安装 Rust 时检测到系统已有 rustup 管理的版本,不会重新下载,
    而是创建软链直接指向 ~/.cargo/bin。这样既节省磁盘空间,又避免与官方工具链管理器冲突。Go、Node 没有这种官方版本管理器,
    mise 才自己下载安装。

  • mise 通过 shell hook 而非 shim 实现版本切换
    mise 依赖 eval "$(mise activate zsh)" 注入 shell hook,
    每次 cd 或执行命令前读取 .mise.toml 修改 PATH。mise exec 不依赖 hook 直接生效,
    但 zsh -c '...' 启动的是全新 subshell 不经过 hook,所以不生效。IDEA 不从终端 PATH 找 Go SDK,
    而是用户在 Settings 里手动指定路径,所以 mise 对 IDEA 无效。

macOS JDK 查找机制

  • macOS 的 /usr/bin/java 是 wrapper 而非真实二进制:macOS 的 java 命令查找顺序为:
    先看 JAVA_HOME 环境变量,
    没有则调用 /usr/libexec/java_home 在 ~/Library/Java/JavaVirtualMachines/ 和 /Library/Java/JavaVirtualMachines/ 中找当前激活的 JDK。
    mise 安装的 JDK 放在 ~/.local/share/mise/installs/java/,不在标准路径,因此 IDEA 不会自动识别,
    需要手动 Browse。jenv 通过 shim 模式(~/.jenv/shims/java)代理所有 Java 命令,
    与 mise 的 PATH 操作是不同机制。

Rust 工具链架构(rustup / cargo / rustc)

  • rustup 是代理,不是工具本身:~/.cargo/bin/ 下的 cargo、rustc、rustfmt、clippy 等全部是软链接,指向同目录下的 rustup 真实二进制。
    执行 cargo build 的实际路径是:~/.cargo/bin/cargo(软链)→ rustup → ~/.rustup/toolchains/1.96.0-.../bin/cargo(真实 cargo)。
    rustup 根据 ~/.rustup/settings.toml 的配置决定转发到哪个版本,这是版本切换的核心机制。

  • .cargo 和 .rustup 职责分离:~/.cargo/ 是 cargo 的数据目录(bin、registry 包缓存、git 缓存、config.toml),
    不管有没有 rustup 都存在。~/.rustup/ 是 rustup 自己的目录(存各版本 toolchain、下载缓存)。
    两者独立,rustup 把 shim 软链放进 ~/.cargo/bin/ 只是复用了 cargo 已有的 PATH 约定,~/.cargo/ 并不从属于 rustup。

  • rustup shim 放 .cargo/bin 是历史债:正常职责边界应是 ~/.rustup/bin/ 放 rustup shim,~/.cargo/bin/ 放用户通过 cargo install 安装的工具。
    但 cargo 早于 rustup 存在,~/.cargo/bin 已是 PATH 约定,rustup-init 为了零配置接入直接复用该目录,
    省去一个 PATH 配置项,代价是 rustup 的 shim 和用户安装的工具混在同一目录,职责边界模糊。

  • brew 装的 Rust 与 rustup 完全两套机制:brew install rust 把真实二进制直接装到 /opt/homebrew/bin/,
    没有 ~/.cargo/bin 里的 shim,没有 ~/.rustup/ 目录,也没有版本切换能力。
    两者同时存在时 PATH 顺序决定哪个生效,会互相打架。需要版本切换(stable/nightly 共存、组件管理、交叉编译 target)必须用 rustup,不能用 brew。
    brew 的 Rust 可通过 rustup toolchain link system /opt/homebrew 注册为 rustup 的一个 toolchain 共存。

brew services 的完整生命周期

  • brew services stop 会删除 plist 而非仅 unload
    brew services start 做的事是写 plist 到 ~/Library/LaunchAgents/ + launchctl load 启动进程。
    stop 时 brew 不仅 unload 进程,还会删除 plist 文件(这是 brew 的行为,不是 launchd 的),因此重启后不会自动启动。
    这与常见的 stop 只暂停进程、plist 保留的理解不同——brew 的 stop 是彻底的,等同于 remove。

  • brew services 的 formula 到 plist 是机械翻译
    formula 中的 service 块(Ruby DSL)被 brew 直接翻译为 macOS launchd 的 plist XML。映射关系:
    run → ProgramArguments,keep_alive true → KeepAlive + RunAtLoad,
    log_path → StandardOutPath,error_log_path → StandardErrorPath,
    working_dir → WorkingDirectory。brew 不做任何智能判断,只是格式转换。

  • brew 的 label 前缀 mxcl 是创始人 GitHub 用户名
    homebrew.mxcl. 中的 mxcl 是 Max Howell(Homebrew 创始人)的 GitHub 用户名。
    早期 Homebrew 以个人账号维护,label 用了他的用户名。后来迁移到 homebrew 组织,但命名约定沿用至今。

zsh 与 mise 初始化

  • zshenv 要轻量.zshenv 会被所有 zsh 实例读取,包括脚本和非交互场景,所以只适合放基础 PATH 和少量真正全局的环境变量。
    完整 shell hook、补全、alias、插件初始化不应放进去,否则会污染脚本执行并拖慢所有 zsh 启动。

  • mise 分层激活:mise 的 zsh 集成可以拆成两层:
    .zprofilemise activate zsh --shims 让 login shell、IDE 或非交互入口能找到工具 shims;
    .zshrc 用完整 mise activate zsh 给交互 shell 安装目录切换等 hook。
    前提是 .zshenv 已提供能找到 mise 的基础 PATH。

mise env 语义

  • 下划线是命名空间mise.toml[env] 中,普通键会导出为环境变量;
    _.file_.path_.source 这类下划线键不是 TOML 语法,而是 mise 约定的特殊配置命名空间,
    用来描述如何加载 env 文件、追加 PATH 或 source 脚本。

mise 与 IDE 启动语义

  • shim 与 hook 不冲突:hook 模式是交互 shell 主动在 cd 等时机维护当前 shell 的 PATH 和环境;
    shim 模式是命令名先命中 ~/.local/share/mise/shims
    再由 shim 按当前工作目录查 .mise.toml 分发到真实工具。前者要求 shell 已执行完整 activate,
    后者只要求 PATH 命中 shim 且 cwd 正确,所以更适合 IDE Run Configuration 这类不完整加载交互 shell 的入口。

  • IDE 只继承启动环境:IDEA 的 Terminal 往往能感知 mise,是因为它启动 shell;
    但 Run/Debug、Go SDK、Project SDK 不等于交互 shell。
    IDE 进程启动后不会自动重载 .mise.toml 或 shell profile,Run Configuration 若要走 mise,
    关键是 IDE 进程 PATH 里有 shims,并且 working directory 位于项目目录。
    SDK/索引层通常仍需手动指向 mise where 或插件集成。

zsh 启动模式

  • 三标志决定加载:zsh 是否读 .zprofile.zshrc 不是由 -c 单独决定,
    而是由 login 与 interactive 两个维度决定:-l.zprofile-i.zshrc
    所有 zsh 都读 .zshenv。因此 zsh -lc 是非交互 login,通常有 shims 无完整 hook;
    zsh -ic 是交互非 login,有 hook 但不读 .zprofilezsh -lic 才同时读 profile 和 rc。

mise 安全机制

  • mise 需要显式信任配置文件:mise 有安全信任机制,首次使用的目录下的 .mise.toml 默认不受信任,
    运行时会报 "not trusted" 错误。通过 mise trust 命令信任该目录即可,信任目录后会信任其下所有子目录的配置文件。
    这与 git 的 safe.directory 机制类似,目的是防止恶意配置文件被自动执行。

zsh 配置文件命名之谜

  • zsh 配置文件加载顺序:5 个文件按 .zshenv → .zprofile → .zshrc → .zlogin → .zlogout 顺序加载,
    但只有 login shell 走完整链路,interactive shell 只走 .zshenv → .zshrc,
    non-interactive shell 只读 .zshenv。所以大部分配置放在 .zshrc 即可,
    .zshenv 只放必需环境变量以避免拖慢所有 zsh 脚本。

  • zsh 前缀 vs z 前缀的历史原因不明:.zshrc/.zshenv 用全名 zsh,
    .zprofile/.zlogin/.zlogout 只用 z 前缀,这个命名差异的真实原因没有可靠的一手资料。
    AI 先后给出了'原创 vs 继承'和'模仿源文件命名'两种解释,但都被用户识破是推测而非事实。结论:这类历史命名问题不应轻信 AI 的自信回答,
    应以原始文档为准。

AI 的幻觉式解释

  • AI 会对 obscure 历史细节编造可信解释:当被问到 zsh 命名约定的历史原因时,
    Claude 先给出'原创 vs 继承'的假解释(被用户指出逻辑矛盾),又给出'模仿源文件命名'的另一个推测(仍无证据),直到用户质疑'真的吗?'才承认不确定。
    模式:AI 倾向于给每个问题一个听起来自信的答案,即使没有可靠来源。这类问题(历史命名、设计决策动机)尤其容易触发幻觉,
    应以原始文档/changelog/作者访谈为准。