用途
这项资料说明 macOS 应用打包时一个容易被忽略的副作用:在已打包的 macOS 应用属性列表中声明 Launch Services 环境字典(LSEnvironment)后,该进程的 PATH 会被固定为四个系统目录。该字典通常是给已发布应用设置分配器标志、运行时开关或调试开关的正常方式,但声明字典这一行为本身就会产生影响。
主要机制
- 触发条件不是某个特定键,而是字典是否存在。一旦声明该字典,进程 PATH 就被固定为
/usr/bin:/bin:/usr/sbin:/sbin。 - 此后应用作为外部进程启动的任何工具,只能在这四个目录中查找;通过包管理器安装、位于用户主目录或
/usr/local下的工具都不会被找到。 - 该字典由 Launch Services 应用,因此覆盖文件管理器、Dock、Spotlight 和
open命令等所有正常启动已安装应用的方式。 - 如果直接从终端运行
Contents/MacOS/<binary>,该字典不会生效,进程继承的是 shell 环境。也就是说,从终端无法复现这一问题,而开发期测试桌面应用的典型方式恰恰是没有固定 PATH 的启动路径。
资料中给出的打包配置示例为:
mac {
# Declaring this dict pins PATH to /usr/bin:/bin:/usr/sbin:/sbin.
# Audited before adding — see the note in the audit section below.
info-plist.LSEnvironment.<KEY> = "<value>"
}
陷阱与注意事项
- 审计是前置条件而非事后补充。添加该键之前,应找出应用在 macOS 路径上可能启动的每个外部进程,并逐一记录其为何安全;这条记录本身就是产物,能避免他人重复审计,并在情况变化时提供提示。
- 受平台条件限制的启动调用同样应纳入审计。如果 macOS 代码路径上唯一的启动是限定在另一操作系统的探测调用,应写明“此处不可达”,而不是留下未解释的调用点,否则日后移除限制的改动将无从察觉。
- 某些平台 API 根本不查询 PATH,标记它们会浪费审计工作。通过桌面集成 API 打开 URL 或文件走 Launch Services,不经 PATH 解析处理程序;但前提是确认该调用确实经过此 API,而不是回退到启动命令行。按操作系统分级的回退链中调用外部命令,正是日后容易被加入的代码。
- 症状不会直接提到 PATH。它表现为辅助程序报“command not found”、功能悄悄失效,或只有正常安装应用的用户才会遇到的启动失败。若启动失败被吞掉(例如没有日志的
runCatching),则完全没有症状,只是一个失效的功能。 - 如果仍需要该键的效果,删除该键是错误的回滚方式。删除字典会恢复正常 PATH 继承,同时失去添加它的初衷。更合适的做法是在同一字典中显式添加 PATH,把辅助工具实际所在目录追加到四个基础目录之后。
- 绝对路径是持久的修复方式。对于随应用发布或可确定位置的任何程序,用绝对路径启动,或为进程构建器传入显式环境,使应用完全不依赖 PATH,这样 PATH 固定便没有代价。
- 该结论仅适用于 macOS。Windows 和 Linux 的打包路径有各自的环境机制和规则,不应把这一结论套用到其他平台。
验证方式
- 找出所有启动点并逐一记录结论:
grep -rn "ProcessBuilder\|Runtime.getRuntime().exec\|exec(" \
--include='*.kt' --include='*.java' <src>
- 在干净机器上用
command -v <tool>解析每个命令,确认结果位于/usr/bin、/bin、/usr/sbin或/sbin之内;其他位置在声明字典后将无法找到。 - 确认该键确实进入打包后的应用。应从构建产物的属性列表中读取,而不是信任打包配置;许多配置格式会静默接受层级写错的键,而它永远不会出现在输出中。
- 从文件管理器而非终端测试。按用户的方式启动已安装应用,然后打印运行进程的环境(
ps eww -p <pid>),确认变量存在以及 PATH 变成了什么。 - 每当功能新增外部进程时重跑第 1 步,并把审计记录放在配置键旁边,使两端都能发现彼此的关联。
适用场景与限制
该资料面向开发和维护 macOS 桌面应用的开发者,尤其是使用 Kotlin 或 Java 启动外部进程、并在打包配置中设置 Info.plist 的项目。它适用于排查仅在正常安装启动时出现的进程查找失败问题。资料未提供具体框架、构建工具的版本要求或其他平台的对应机制;Windows 与 Linux 的环境机制细节暂未提供。