You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为macOS打包含LaunchAgent的Flutter应用:签名启动失败求助

macOS LaunchAgent 代码签名问题排查(Flutter+Rust gRPC应用)

问题背景

我并非macOS用户,近期正在编写脚本创建可用于macOS安装的.dmg文件。我的应用采用客户端-服务器架构:Flutter前端与Rust后端通过gRPC通信,后端需要在用户登录后持续运行。

当前实现方案

  • 后端以LaunchAgent形式运行(未使用LoginItem,因为无法通过plist传递环境变量)
  • 后端嵌套在主应用包内部
  • 先对嵌套的后端包进行临时ad-hoc签名,再签名主应用包

遇到的问题

执行命令launchctl print gui/$(id -u)/com.todo.filerequest.backend时,显示后端进程退出原因:last exit reason = OS_REASON_CODESIGNING,但使用代码签名验证命令:

codesign --verify --deep --strict --verbose=2 /Applications/Filerequest.app/Contents/Library/Helpers/FilerequestBackend.app

和

codesign --verify --deep --strict --verbose=2 /Applications/Filerequest.app

均返回验证通过:

/Applications/Filerequest.app: valid on disk
/Applications/Filerequest.app: satisfies its Designated Requirement

完整安装包制作脚本

#!/usr/bin/env bash
set -euo pipefail

# === CONFIGURATION ===
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FE_DIR="$SCRIPT_DIR/filerequest_fe"
BE_DIR="$SCRIPT_DIR/filerequest_be_rust"
ARTIFACT_DIR="$SCRIPT_DIR/macos_install_artifacts"
APP_NAME="Filerequest.app"
DMG_NAME="FilerequestInstaller.dmg"
VOLUME_NAME="Filerequest Installer"
VERSION="0.1.1-$(date +%s)"

# === CLEAN & PREPARE ===
echo "→ Cleaning and recreating artifact directory…"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"

# === BUILD STEPS ===
if [[ "${1-}" != "--skip-build" ]]; then
  echo "→ Building Flutter (macOS universal release)…"
  pushd "$FE_DIR" >/dev/null
    flutter build macos --release
  popd >/dev/null

  echo "→ Building Rust backend (universal release)…"
  pushd "$BE_DIR" >/dev/null
    rustup target add aarch64-apple-darwin x86_64-apple-darwin
    cargo build --release --target aarch64-apple-darwin
    cargo build --release --target x86_64-apple-darwin
    lipo -create \
      "target/aarch64-apple-darwin/release/filerequest_be_rust" \
      "target/x86_64-apple-darwin/release/filerequest_be_rust" \
      -output "target/release/filerequest_be_rust"
  popd >/dev/null
else
  echo "→ --skip-build: reusing existing artifacts"
fi

# === STAGE THE FLUTTER APP ===
echo "→ Staging Flutter .app bundle…"
FLUTTER_APP="$FE_DIR/build/macos/Build/Products/Release/filerequest_fe.app"
cp -R "$FLUTTER_APP" "$ARTIFACT_DIR/$APP_NAME"
APP_DIR="$ARTIFACT_DIR/$APP_NAME"

# === EMBED HELPER AS ITS OWN .app BUNDLE ===
echo "→ Embedding backend as bundle…"
HELPER_APP_NAME="FilerequestBackend.app"
HELPER_APP_DIR="$APP_DIR/Contents/Library/Helpers/$HELPER_APP_NAME"
HELPER_EXEC_DIR="$HELPER_APP_DIR/Contents/MacOS"

# Create helper app bundle structure
mkdir -p "$HELPER_EXEC_DIR"
# Copy executable
cp "$BE_DIR/target/release/filerequest_be_rust" \
   "$HELPER_EXEC_DIR/filerequest_be"
chmod +x "$HELPER_EXEC_DIR/filerequest_be"
# Create Info.plist for helper
# I'm not sure if this is necessary, as this is not plist file the LaunchAgent is registered with
cat > "$HELPER_APP_DIR/Contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleIdentifier</key>
    <string>com.todo.filerequest.backend</string>
  <key>CFBundleExecutable</key>
    <string>filerequest_be</string>
  <key>CFBundleName</key>
    <string>FilerequestBackend</string>
  <key>CFBundlePackageType</key>
    <string>APPL</string>
  <key>CFBundleVersion</key>
    <string>0.1.0</string>
  <key>CFBundleShortVersionString</key>
    <string>0.1.0</string>
</dict>
</plist>
EOF

# === CREATE A LAUNCHAGENT PLIST ===
echo "→ Writing LaunchAgent plist…"
LAUNCHAGENTS_DIR="$APP_DIR/Contents/Library/LaunchAgents"
mkdir -p "$LAUNCHAGENTS_DIR"
INSTALL_PATH="/Applications/$APP_NAME"
cat > "$LAUNCHAGENTS_DIR/com.todo.filerequest.backend.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
    <string>com.todo.filerequest.backend</string>
  <key>ProgramArguments</key>
    <array>
      <string>$INSTALL_PATH/Contents/Library/Helpers/$HELPER_APP_NAME/Contents/MacOS/filerequest_be</string>
    </array>
  <key>EnvironmentVariables</key>
    <dict>
      <key>PORT</key>
        <string>50051</string>
      <key>OUTPUT_DIR</key>
        <string>Documents/Filerequest</string>
      <key>DB_PATH</key>
        <string>filerequest.db</string>
      <key>ADDRESS</key>
        <string>127.0.0.1</string>
    </dict>
  <key>RunAtLoad</key>
    <true/>
</dict>
</plist>
EOF

# === SIGN HELPER APP BUNDLE ===
echo "→ Signing helper app bundle…"
ENTITLEMENTS="$FE_DIR/macos/Runner/Release.entitlements"
# This is a WIP, so using the same entitlements file here is a temporary crutch
codesign --force \
  --sign - \
  --entitlements "$ENTITLEMENTS" \
  "$HELPER_APP_DIR"

# === SIGN THE ENTIRE APP ===
echo "→ Ad-hoc signing Filerequest.app…"
codesign --force \
  --sign - \
  --entitlements "$ENTITLEMENTS" \
  "$APP_DIR"

# === CREATE DMG INSTALLER ===
echo "→ Creating compressed DMG installer…"
hdiutil create \
  -volname "$VOLUME_NAME" \
  -srcfolder "$ARTIFACT_DIR" \
  -ov \
  -format UDZO \
  "$ARTIFACT_DIR/$DMG_NAME"

echo "✅ Done!"
echo "   • DMG: $ARTIFACT_DIR/$DMG_NAME"

排查方向与修复建议

1. LaunchAgent路径与注册逻辑问题

  • 当前LaunchAgent硬编码了/Applications/$APP_NAME路径,若用户将应用安装到非默认位置(如~/Applications),路径会失效。建议改用动态路径,比如通过主应用bundle定位:
    <key>ProgramArguments</key>
    <array>
      <string>/usr/bin/env</string>
      <string>bash</string>
      <string>-c</string>
      <string>exec "$(dirname "$0")/../../../../Contents/Library/Helpers/FilerequestBackend.app/Contents/MacOS/filerequest_be"</string>
    </array>
    
  • macOS不会自动加载应用包内Contents/Library/LaunchAgents目录下的plist,需在主应用首次启动时,将plist复制到用户的~/Library/LaunchAgents目录,再执行launchctl load ~/Library/LaunchAgents/com.todo.filerequest.backend.plist完成注册。

2. 权限与签名配置问题

  • 后端复用主应用的entitlements可能存在权限不匹配:主应用的权限可能多余,或缺少LaunchAgent运行所需权限。建议给后端单独创建entitlements文件,至少包含:
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>com.apple.security.inherit</key>
        <true/>
        <key>com.apple.security.network.client</key>
        <true/>
    </dict>
    </plist>
    
  • Ventura及以上版本对ad-hoc签名的LaunchAgent限制更严格,若有苹果开发者账号,建议用开发者ID签名测试,ad-hoc签名可能无法满足系统的运行校验要求。

3. 深层签名校验

  • 用spctl工具做Gatekeeper评估,更贴近LaunchAgent运行时的校验逻辑:
    spctl --assess --verbose=4 /Applications/Filerequest.app/Contents/Library/Helpers/FilerequestBackend.app
    
  • 直接运行后端可执行文件,查看是否有明确的签名报错:
    /Applications/Filerequest.app/Contents/Library/Helpers/FilerequestBackend.app/Contents/MacOS/filerequest_be
    

4. 环境变量优化

  • OUTPUT_DIR使用相对路径可能导致后端无法正确定位目录,建议改为完整路径:
    <key>OUTPUT_DIR</key>
    <string>~/Documents/Filerequest</string>
    

更优实现方案

  • 改用LoginItems + XPC通信:若环境变量需求不复杂,可使用Flutter的macOS LoginItem API添加登录项,通过XPC与后端通信,更符合macOS应用规范。
  • 后端作为XPC Service:将后端封装为主应用的XPC Service,系统会自动管理其生命周期,无需手动配置LaunchAgent,权限与签名更易处理。

内容的提问来源于stack exchange,提问作者NoBullsh1t

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 00:07:03