MiniDebug 工程笔记

在云端 Mac 上自动化 Unity iOS 导出与 Xcode 构建验收

在云端 Mac 上自动化 Unity iOS 导出与 Xcode 构建验收

Unity 项目在本地能导出 iOS 工程,换到云端 Mac 后却停在资源导入、原生依赖或链接阶段。最麻烦的不是失败本身,而是所有步骤挤在一个命令里:日志只留下“构建失败”,无法判断该重跑 Unity、清理缓存,还是检查 Xcode 工程。更稳妥的做法是把流水线拆成两个独立关卡:先生成可审计的 Xcode 工程,再做不含签名动作的原生编译验收。

先定义输入与产物边界

一次可复现构建至少要固定四类输入:提交版本、Unity 编辑器版本、包依赖锁定文件和场景清单。不要让脚本临时读取某位开发者机器上的编辑器设置,也不要把上一次导出的 Xcode 工程直接覆盖后继续编译。

建议为每次任务准备独立目录:

路径 用途 是否可缓存
Source/ 当前提交的 Unity 工程
Library/ 资源导入结果 有条件
Build/iOS/ 本次导出的 Xcode 工程
.build/DerivedData/ 原生编译中间文件 有条件
Artifacts/ 日志、摘要与验收结果

Library 的缓存键应包含 Unity 版本、目标平台以及 Packages/manifest.jsonPackages/packages-lock.json 的摘要。任意一项变化就重新导入。DerivedData 也不要在不同 Xcode 版本或不同工程设置之间直接复用。

缓存只能加速已被固定的输入,不能替代版本锁定。命中一个边界不清的缓存,通常比完整重建更难排查。

用批处理方法导出 Xcode 工程

把导出入口放在 Assets/Editor/IosExport.cs,场景由构建设置中已启用的条目生成,避免脚本与编辑器界面维护两份列表。

using System;
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Reporting;

public static class IosExport
{
    public static void Run()
    {
        var scenes = EditorBuildSettings.scenes
            .Where(scene => scene.enabled)
            .Select(scene => scene.path)
            .ToArray();

        if (scenes.Length == 0)
            throw new InvalidOperationException("No enabled scenes.");

        var options = new BuildPlayerOptions
        {
            scenes = scenes,
            locationPathName = "Build/iOS",
            target = BuildTarget.iOS,
            options = BuildOptions.CleanBuildCache
        };

        var report = BuildPipeline.BuildPlayer(options);
        if (report.summary.result != BuildResult.Succeeded)
            throw new InvalidOperationException("Unity export failed.");
    }
}

执行时显式传入项目目录、日志路径和方法名:

"$UNITY_EDITOR" \
  -batchmode \
  -nographics \
  -quit \
  -projectPath "$PWD" \
  -executeMethod IosExport.Run \
  -logFile "$PWD/Artifacts/unity-export.log"

任务开始前应删除 Build/iOS,但不要无条件删除 Library。任务结束后检查退出码、日志文件和 Build/iOS/Unity-iPhone.xcodeproj 是否同时存在;只看到目录存在并不代表导出成功,失败任务也可能留下半成品。

分离原生依赖与编译验收

Unity 导出成功只说明生成器完成了工作,不能证明 Objective-C、Swift、IL2CPP 产物和原生库能够一起编译。先运行项目规定的原生依赖安装步骤,再判断应构建 workspace 还是 project。依赖工具生成了 .xcworkspace 时,继续构建 .xcodeproj 往往会出现模块找不到或链接库缺失。

set -euo pipefail

cd Build/iOS
rm -rf ../../.build/DerivedData

if [ -d "Unity-iPhone.xcworkspace" ]; then
  xcodebuild \
    -workspace Unity-iPhone.xcworkspace \
    -scheme Unity-iPhone \
    -configuration Release \
    -sdk iphoneos \
    -derivedDataPath ../../.build/DerivedData \
    CODE_SIGNING_ALLOWED=NO \
    build
else
  xcodebuild \
    -project Unity-iPhone.xcodeproj \
    -scheme Unity-iPhone \
    -configuration Release \
    -sdk iphoneos \
    -derivedDataPath ../../.build/DerivedData \
    CODE_SIGNING_ALLOWED=NO \
    build
fi

这里的目标是验证编译和链接,不是生成可分发安装包。签名应放在后续受控阶段,避免普通提交验证任务接触不必要的敏感材料。

让缓存可失效而不是靠猜

Unity 项目常见的错误做法是只用分支名作为缓存键。两个提交位于同一分支,不代表资源导入结果相同。可以先生成输入摘要:

{
  printf '%s
' "$UNITY_VERSION"
  printf '%s
' "$XCODE_VERSION"
  shasum -a 256 Packages/manifest.json
  shasum -a 256 Packages/packages-lock.json
  find Assets -name '*.asmdef' -o -name '*.rsp' | sort | xargs shasum -a 256
} | shasum -a 256 | awk '{print $1}' > Artifacts/cache-key.txt

不要缓存整个工作目录

整个工程目录打包缓存会混入旧导出物、临时设置和已删除文件。应分别管理 LibraryDerivedData,恢复后仍执行输入摘要校验。出现无法解释的编译差异时,先保留失败日志,再用空缓存重跑一次;如果空缓存成功,问题应归类到缓存边界,而不是直接修改业务代码。

归档能复盘的最小证据集

构建通过后不要只保存一个成功标记。至少归档 Unity 完整日志、Xcode 输出、提交哈希、工具版本、缓存键和导出工程摘要。工程摘要可以记录 scheme、配置与构建设置,但应排除凭据、令牌和本机私有路径。

常见失败可以按阶段快速分流:

最后把“导出成功”和“原生编译成功”设为两个独立状态。前者失败时无需运行 Xcode,后者失败时也不必反复导入全部资源。流水线因此更容易重试,日志更短,责任边界也更清楚。

常见问题

为什么不直接让 Unity 一步生成最终安装包?

拆分后可以明确区分 Unity 脚本、资源导入、原生依赖和 Xcode 编译故障;无签名编译还能先验证源码与链接是否通过,再进入凭据受控的签名阶段。

Unity 的 Library 目录可以跨项目或跨版本复用吗?

不建议。缓存键至少应包含 Unity 编辑器版本、依赖锁定文件和目标平台;键不一致时应重新生成 Library,避免旧导入结果污染构建。

导出的工程何时应该使用 xcworkspace?

原生依赖安装生成 workspace 后应使用 xcworkspace;没有 workspace 时才使用 xcodeproj。脚本应在依赖安装完成后检测文件,而不是永久写死其中一种。

独享物理 Mac

把下一次 iOS 构建放到 MiniDebug M4 上。

固定提供 M4、16GB RAM 与 256GB SSD,可按天、周、月或季租用,并从新加坡、日本东京、韩国首尔、香港、美国东部五个节点中选择。实际可用状态以控制台实时返回为准。

选择节点并订购