Android Studio 在 Apple Silicon Mac 上无法找到 node 导致 Gradle 同步失败
问题描述
在 Apple Silicon Mac 上用 Android Studio 打开 React Native 项目的 android/ 目录,触发 Gradle Sync 时报错:
A problem occurred evaluating project ':react-native-gesture-handler'.
> A problem occurred starting process 'command 'node''
Caused by: java.io.IOException: Cannot run program "node"
(in directory ".../android"): error=2, No such file or directory
后续还会出现类似的错误,换成其他 native 模块(如 react-native-camera-roll):
A problem occurred evaluating project ':react-native-camera-roll_camera-roll'.
> /.../camera-roll/node_modules/react-native/ReactAndroid/gradle.properties
(No such file or directory)
根本原因
React Native 的 native 模块(gesture-handler、camera-roll 等)在 Gradle 配置阶段需要调用 node 命令来定位 react-native 包的路径,大致逻辑如下:
commandLine("node", "--print", "require.resolve('react-native/package.json')")
问题在于:macOS GUI 应用启动时,不会 source 任何 shell 配置文件(.zshrc、.zprofile 等),它从 launchd 继承一个极简的环境变量,其中的 PATH 不包含任何包管理器(nvm、Homebrew 在 Apple Silicon 上)注册的路径。具体来说:
- nvm:node 安装在
~/.nvm/versions/node/vX.X.X/bin/,只通过 shell 配置注入 PATH - Homebrew(Apple Silicon):node 安装在
/opt/homebrew/bin/,同样只在 shell 登录时通过/etc/profile或.zprofile注入 PATH
因此 Android Studio 的 Gradle daemon 启动后,执行 commandLine("node", ...) 时找不到可执行文件,构建失败。
Intel Mac 上用 Homebrew 安装 node 通常不受影响,因为 Intel Homebrew 的路径是 /usr/local/bin/,这个路径在系统默认 PATH 中。Apple Silicon 换用了 /opt/homebrew/bin/,不再有这个优待。
尝试过的方案
创建 /usr/local/bin/node symlink
sudo ln -sf $(which node) /usr/local/bin/node
没有解决问题。原因是 /usr/local/bin 虽然在 /etc/paths 里,但 macOS Sequoia 的 launchd 用户域并不通过 path_helper 读取 /etc/paths,GUI 应用实际拿到的 PATH 仍然不包含 /usr/local/bin。
在 gradle.properties 中设置 REACT_NATIVE_NODE_MODULES_DIR
REACT_NATIVE_NODE_MODULES_DIR=../node_modules/react-native
修复了第一个报错(gesture-handler),却引出第二个。原因是相对路径被 camera-roll 的 build.gradle 按照它自己的项目目录(node_modules/@react-native-camera-roll/camera-roll/android/)来解析,最终拼出了一个不存在的路径。
LaunchAgent 注入 PATH
创建 ~/Library/LaunchAgents/dev.hatcloud.set-gui-path.plist,在登录时调用 launchctl setenv PATH ...。在 macOS Sequoia 上没有生效,原因是新版本 launchctl setenv 的行为发生了变化,不再可靠地影响后续启动的 GUI 应用。
最终解决方案
在用户级别的 Gradle 全局 init 脚本中注入路径。
创建文件 ~/.gradle/init.d/react-native-node-path.gradle:
allprojects {
def rnDir = rootProject.file("../node_modules/react-native")
if (rnDir.exists()) {
ext.REACT_NATIVE_NODE_MODULES_DIR = rnDir.absolutePath
}
}
~/.gradle/init.d/ 是 Gradle 的全局初始化目录,其中的脚本会在每次构建开始前自动执行,无论是命令行还是 Android Studio。
rnDir.exists() 的判断保证了这段逻辑只对真正的 React Native 项目生效,普通 Android 项目不受影响。rootProject.file(...).absolutePath 确保路径是绝对路径,不会被各子模块按自己的目录相对解析。
为什么这个方案有效
React Native 的 native 模块 build.gradle 在调用 node 之前都会先检查 rootProject.ext 中是否存在 REACT_NATIVE_NODE_MODULES_DIR,有就直接用,跳过 commandLine("node", ...) 这一步。init 脚本在项目 build.gradle 执行前就把这个 ext 属性设好了,所以所有模块都能命中这个快捷路径。
其他人是怎么解决的
大多数人没有遇到这个问题,因为 React Native 官方文档推荐用 Homebrew 安装 node,在 Intel Mac 上 Homebrew 的路径 /usr/local/bin/ 恰好在系统 PATH 中。Apple Silicon 上使用 nvm 或关注到这个区别的人不多,相关讨论散落在各处,没有形成一个统一的官方答案。
常见的临时方案是从终端启动 Android Studio(open -a "Android Studio"),这样 Android Studio 会继承终端的 PATH。缺点是每次都要记得用命令行打开,从 Dock 启动就又失效了。
Gradle 全局 init 脚本的方案在这种场景下是最干净的:不需要修改任何项目代码,不依赖环境变量,一次配置,所有 RN 项目都受益。