为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
相关产品推荐
相关产品推荐

