27.10.2026 17 мин чтения Просмотров:0

Сборка Unity из командной строки: batchmode, скрипт сборки и CI

Пишем скрипт сборки на C# и запускаем Unity из терминала: аргументы командной строки, версия и define-символы, коды выхода, проверки через IPreprocessBuildWithReport и пример для GitHub Actions.

Сборка Unity из командной строки: batchmode, скрипт сборки и CI

Эта статья — практическое руководство по сборке Unity-проекта без нажатия кнопки Build. Вы напишете скрипт сборки на C#, научитесь запускать его из командной строки в режиме -batchmode, передавать свои аргументы (версию, тип сборки, путь), правильно сообщать об ошибке через код выхода и встраивать проверки в пайплайн через IPreprocessBuildWithReport. В конце будет готовый шаблон, который одинаково работает на вашей машине, на сборочном сервере и в GitHub Actions.

Зачем вообще собирать из командной строки

Пока проект маленький, кнопки Build в окне Build Profiles (в старых версиях — Build Settings) хватает. Проблемы начинаются, когда сборок становится много: dev-сборка для тестировщиков, релиз в Google Play, WebGL для демо, сервер для мультиплеера. Каждую нужно собрать с правильными галочками, правильной версией и правильными define-символами. Человек ошибается: забыл снять Development Build, не поднял bundleVersionCode, собрал из грязной ветки.

Скрипт сборки решает это раз и навсегда:

  • Воспроизводимость. Одна команда — одна и та же сборка, независимо от того, кто и где её запускает.
  • Автоматизация. Сборку можно повесить на коммит в main, на тег или на ночное расписание.
  • Проверки. Перед сборкой можно убедиться, что в сцене нет отладочных объектов, что версия поднята, что все сцены на месте, — и уронить сборку, если что-то не так.
  • Пост-обработка. После сборки — заархивировать, переименовать по версии, положить рядом changelog, отправить в хранилище.

Минимальный скрипт сборки

Всё начинается со статического метода в editor-сборке. Файл должен лежать в папке Editor (или в asmdef, ограниченном платформой Editor), иначе UnityEditor будет недоступен и игровая сборка просто не скомпилируется.

// Assets/Editor/BuildScript.cs
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;

public static class BuildScript
{
    [MenuItem("Build/Windows (Release)")]
    public static void BuildWindows()
    {
        var options = new BuildPlayerOptions
        {
            scenes = EditorBuildSettings.scenes
                .Where(s => s.enabled)
                .Select(s => s.path)
                .ToArray(),
            locationPathName = "Builds/Windows/MyGame.exe",
            target = BuildTarget.StandaloneWindows64,
            options = BuildOptions.None
        };

        BuildReport report = BuildPipeline.BuildPlayer(options);
        BuildSummary summary = report.summary;

        if (summary.result == BuildResult.Succeeded)
            Debug.Log($"Сборка готова: {summary.totalSize / (1024 * 1024)} МБ за {summary.totalTime}");
        else
            Debug.LogError($"Сборка провалилась: {summary.result}, ошибок: {summary.totalErrors}");
    }
}

Что здесь важно:

  • Список сцен берётся из настроек проекта, а не хардкодится. Так скрипт не разъедется со списком сцен, который видят все остальные.
  • locationPathName — путь к исполняемому файлу, а не к папке, для Windows (.exe), macOS (.app) и Android (.apk или .aab). Для WebGL и iOS это путь к папке. Путь относительный — он считается от корня проекта.
  • BuildPipeline.BuildPlayer возвращает BuildReport. Не игнорируйте его: именно по summary.result вы узнаёте, получилась ли сборка.

Атрибут [MenuItem] необязателен, но удобен: тот же метод можно запустить руками из меню редактора и убедиться, что он работает, прежде чем отдавать его серверу.

Запуск без окна редактора

Теперь вызовем этот метод из терминала. На Windows путь к редактору обычно выглядит как C:\Program Files\Unity\Hub\Editor\<версия>\Editor\Unity.exe, на macOS — /Applications/Unity/Hub/Editor/<версия>/Unity.app/Contents/MacOS/Unity.

"C:\Program Files\Unity\Hub\Editor\6000.0.58f1\Editor\Unity.exe" ^
  -batchmode -quit -nographics ^
  -projectPath "D:\Projects\MyGame" ^
  -buildTarget StandaloneWindows64 ^
  -executeMethod BuildScript.BuildWindows ^
  -logFile "D:\Projects\MyGame\Builds\build.log"

Разберём аргументы:

  • -batchmode — редактор запускается без интерфейса. Никаких диалогов, никаких окон. Если код попытается показать EditorUtility.DisplayDialog, в batchmode он сразу вернёт значение по умолчанию.
  • -quit — закрыть редактор после выполнения метода. Без него процесс повиснет навсегда, и CI будет ждать до таймаута.
  • -nographics — не инициализировать графическое устройство. Полезно на серверах без видеокарты. Но осторожно: если сборка запекает освещение или генерирует что-то через GPU, этот флаг придётся убрать.
  • -projectPath — папка проекта (та, в которой лежат Assets и ProjectSettings).
  • -buildTarget — платформа, на которую редактор переключится при открытии проекта. Об этом подробнее ниже.
  • -executeMethod — полное имя статического метода: Пространство.Класс.Метод. Метод должен быть статическим, без параметров.
  • -logFile — куда писать лог. Если передать -logFile - (дефис), лог пойдёт в стандартный вывод, что удобно в CI: всё видно прямо в консоли джобы.

Почему не переключать платформу внутри скрипта

Соблазнительно написать в начале метода EditorUserBuildSettings.SwitchActiveBuildTarget(...) и не думать о флагах. Так делать не стоит. Переключение платформы запускает реимпорт ассетов и перекомпиляцию скриптов, а в batchmode после перекомпиляции домен перезагружается уже после того, как ваш метод начал выполняться. Итог — нестабильные сборки, в которых часть кода собрана со старыми define-символами. Платформу правильно задавать флагом -buildTarget: редактор откроет проект сразу в нужном режиме, и к моменту вызова вашего метода всё уже импортировано и скомпилировано.

Отсюда же практический совет для CI: держите отдельную рабочую копию (или кэш папки Library) на каждую платформу. Переключение Windows → Android на большом проекте может занимать десятки минут, а с готовым кэшем — секунды.

Код выхода: как сказать CI, что сборка упала

Самая частая ошибка в самописных скриптах — сборка проваливается, а процесс Unity завершается с кодом 0, и CI рапортует зелёную галочку. Причина в том, что Debug.LogError не влияет на код выхода. Нужно завершать процесс явно:

private static void Finish(BuildReport report)
{
    BuildSummary summary = report.summary;
    Debug.Log($"Результат: {summary.result}, размер: {summary.totalSize} байт, время: {summary.totalTime}");

    if (!Application.isBatchMode)
        return; // запуск из меню — редактор не закрываем

    EditorApplication.Exit(summary.result == BuildResult.Succeeded ? 0 : 1);
}

EditorApplication.Exit(int) закрывает редактор с указанным кодом. Проверка Application.isBatchMode нужна, чтобы при запуске из меню скрипт не закрыл вам редактор с несохранённой сценой.

Ещё одна страховка: если внутри -executeMethod вылетело необработанное исключение, Unity в batchmode с -quit завершится с ненулевым кодом. Поэтому в собственных проверках смело бросайте исключение — это проще, чем протаскивать флаг ошибки через весь код.

Свои аргументы командной строки

Метод для -executeMethod не принимает параметров, но все аргументы процесса доступны через System.Environment.GetCommandLineArgs(). Unity игнорирует незнакомые ему флаги, поэтому можно добавлять свои. Напишем маленький парсер:

using System;
using System.Collections.Generic;

public static class BuildArgs
{
    private static Dictionary<string, string> _cache;

    public static string Get(string name, string fallback = null)
    {
        _cache ??= Parse();
        return _cache.TryGetValue(name, out var value) ? value : fallback;
    }

    public static bool Has(string name)
    {
        _cache ??= Parse();
        return _cache.ContainsKey(name);
    }

    private static Dictionary<string, string> Parse()
    {
        var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        string[] args = Environment.GetCommandLineArgs();

        for (int i = 0; i < args.Length; i++)
        {
            if (!args[i].StartsWith("-")) continue;

            string key = args[i].TrimStart('-');
            bool hasValue = i + 1 < args.Length && !args[i + 1].StartsWith("-");
            result[key] = hasValue ? args[++i] : "true";
        }
        return result;
    }
}

Теперь в командную строку можно дописать, например, -buildVersion 1.4.2 -buildNumber 87 -dev -outputPath Builds/Android/game.aab и читать это в скрипте через BuildArgs.Get("buildVersion") и BuildArgs.Has("dev").

Чтобы не столкнуться с встроенными флагами Unity, давайте своим аргументам узнаваемый префикс или составные имена: -buildVersion, а не -version. Заметьте ещё одну тонкость парсера: отрицательные числа вида -5 он примет за флаг. Для версий и путей это не проблема, но если понадобятся отрицательные значения, передавайте их в форме -offset=-5 и разбирайте по знаку равенства.

Версия, номер сборки и define-символы

Скрипт сборки — правильное место, чтобы проставить версию. Тогда она всегда совпадает с тем, что знает CI (тег, номер запуска пайплайна), и никто не забудет её поднять.

private static void ApplyVersion()
{
    string version = BuildArgs.Get("buildVersion");
    string number = BuildArgs.Get("buildNumber");

    if (!string.IsNullOrEmpty(version))
        PlayerSettings.bundleVersion = version;

    if (int.TryParse(number, out int code))
    {
        PlayerSettings.Android.bundleVersionCode = code;
        PlayerSettings.iOS.buildNumber = code.ToString();
    }
}

Помните, что PlayerSettings — это ассет проекта (ProjectSettings/ProjectSettings.asset). Изменения из скрипта сохранятся в файле, и на локальной машине вы увидите их в git как изменённый файл. На CI это обычно неважно, рабочая копия всё равно одноразовая. Локально же можно либо не коммитить эти изменения, либо в конце скрипта возвращать старые значения.

С define-символами то же самое. Например, вы хотите, чтобы в dev-сборке была включена внутриигровая консоль, а в релизе — нет. Современный API работает с NamedBuildTarget:

using UnityEditor.Build;

private static void ApplyDefines(BuildTarget target, bool dev)
{
    var group = BuildPipeline.GetBuildTargetGroup(target);
    var named = NamedBuildTarget.FromBuildTargetGroup(group);

    string current = PlayerSettings.GetScriptingDefineSymbols(named);
    var symbols = new HashSet<string>(current.Split(';', StringSplitOptions.RemoveEmptyEntries));

    if (dev) symbols.Add("DEV_CONSOLE");
    else symbols.Remove("DEV_CONSOLE");

    PlayerSettings.SetScriptingDefineSymbols(named, string.Join(";", symbols));
}

Здесь есть ловушка, связанная с той же перекомпиляцией. Изменение define-символов запускает компиляцию скриптов, но сам код скрипта сборки уже выполняется. Сборка игрока при этом компилирует игровые скрипты заново, с новыми символами, так что в плеер они попадут правильно. А вот код вашего editor-скрипта, который проверяет #if DEV_CONSOLE, увидит символы только после следующей перезагрузки домена. Правило простое: не полагайтесь на #if внутри editor-кода сборки, читайте настройки из аргументов.

Альтернатива, которая обходит ловушку целиком, — поле extraScriptingDefines в BuildPlayerOptions. Символы из него добавляются только для этой конкретной сборки и не меняют настройки проекта:

options.extraScriptingDefines = dev
    ? new[] { "DEV_CONSOLE", "ENABLE_CHEATS" }
    : Array.Empty<string>();

Это предпочтительный вариант: файл ProjectSettings.asset не меняется, и в git ничего не всплывает.

Dev и Release: флаги BuildOptions

BuildOptions — это enum с атрибутом [Flags], значения комбинируются через побитовое ИЛИ. Самые нужные:

  • Development — development-сборка: работают Debug.Log с полными стектрейсами, доступен профайлер, в углу экрана отладочная надпись.
  • AllowDebugging — разрешить подключение отладчика к скриптам (только вместе с Development).
  • ConnectWithProfiler — плеер при старте сам подключится к профайлеру редактора.
  • DetailedBuildReport — собрать подробный отчёт о том, что и почему попало в сборку. Полезно, когда надо понять, откуда взялись лишние сто мегабайт.
  • StrictMode — не считать сборку успешной, если в процессе были ошибки. Без этого флага часть ошибок (например, в отдельных шейдерах) может закончиться «успешной» сборкой с битым содержимым.
  • CleanBuildCache — собрать с нуля, игнорируя кэш. Медленно, но спасает от редких странных проблем после обновления Unity.
private static BuildOptions GetOptions(bool dev)
{
    var options = BuildOptions.StrictMode;

    if (dev)
        options |= BuildOptions.Development | BuildOptions.AllowDebugging;

    if (BuildArgs.Has("cleanBuild"))
        options |= BuildOptions.CleanBuildCache;

    return options;
}

Build Profiles в Unity 6

В Unity 6 появились Build Profiles — ассеты, которые хранят платформу, список сцен, define-символы и часть настроек конкретной сборки. Вместо одной глобальной конфигурации у вас может быть «Android Dev», «Android Release» и «WebGL Demo», каждая в своём файле в проекте.

Для командной строки это означает два новых способа:

  • -activeBuildProfile "Assets/Settings/Build Profiles/Android Release.asset" вместе с -build "Builds/Android/game.aab" — собрать по профилю вообще без своего скрипта.
  • -activeBuildProfile вместе с -executeMethod — профиль задаёт платформу и настройки, а ваш скрипт добавляет версию, проверки и пост-обработку.

Во втором случае в скрипте используется перегрузка BuildPipeline.BuildPlayer, принимающая BuildPlayerWithProfileOptions:

using UnityEditor.Build.Profile;

public static void BuildWithActiveProfile()
{
    BuildProfile profile = BuildProfile.GetActiveBuildProfile();
    if (profile == null)
        throw new BuildFailedException("Не задан профиль: передайте -activeBuildProfile");

    var options = new BuildPlayerWithProfileOptions
    {
        buildProfile = profile,
        locationPathName = BuildArgs.Get("outputPath", "Builds/Default/game"),
        options = GetOptions(BuildArgs.Has("dev"))
    };

    Finish(BuildPipeline.BuildPlayer(options));
}

Если проект ещё на Unity 2022 LTS или вы не хотите заводить профили, классический BuildPlayerOptions с -buildTarget продолжает работать и в Unity 6. Профили удобны, когда конфигураций много и их хочется держать в виде ассетов, а не в виде веток if в коде.

Проверки перед сборкой: IPreprocessBuildWithReport

Бывают проверки, которые должны срабатывать при любой сборке — и из скрипта, и по кнопке в редакторе. Для этого есть колбэки сборки. Класс, реализующий IPreprocessBuildWithReport, Unity находит сам и вызывает перед каждой сборкой плеера.

using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;

public class ReleaseChecks : IPreprocessBuildWithReport
{
    public int callbackOrder => 0;

    public void OnPreprocessBuild(BuildReport report)
    {
        bool dev = (report.summary.options & BuildOptions.Development) != 0;
        if (dev) return;

        if (PlayerSettings.bundleVersion == "0.1")
            throw new BuildFailedException("Релиз с версией по умолчанию 0.1 — поднимите bundleVersion");

        if (report.summary.platform == BuildTarget.Android && !PlayerSettings.Android.useCustomKeystore)
            throw new BuildFailedException("Релиз под Android должен подписываться своим keystore");
    }
}

Ключевой момент — BuildFailedException. Это специальное исключение, которое корректно останавливает сборку: Unity прервёт процесс, а BuildReport получит статус Failed. Обычное исключение в колбэке в некоторых версиях Unity просто логируется, и сборка идёт дальше.

callbackOrder задаёт порядок, если колбэков несколько: меньшее число вызывается раньше. Это пригодится, когда один колбэк, например, генерирует файл, а второй его проверяет.

Парный интерфейс — IPostprocessBuildWithReport. Он вызывается после сборки и получает тот же BuildReport, в том числе summary.outputPath. Туда удобно вынести всё, что делается с готовыми файлами: скопировать конфиг рядом с exe, записать файл с номером версии, удалить папку _BurstDebugInformation_DoNotShip, которая не должна попасть к игрокам.

using System.IO;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;

public class StripDebugFolders : IPostprocessBuildWithReport
{
    public int callbackOrder => 100;

    public void OnPostprocessBuild(BuildReport report)
    {
        string outputDir = Path.GetDirectoryName(report.summary.outputPath);
        if (string.IsNullOrEmpty(outputDir) || !Directory.Exists(outputDir)) return;

        foreach (string dir in Directory.GetDirectories(outputDir, "*_DoNotShip"))
        {
            Directory.Delete(dir, true);
            Debug.Log($"Удалена отладочная папка: {dir}");
        }
    }
}

Подпись Android-сборки без паролей в репозитории

Релизная Android-сборка должна быть подписана вашим ключом. Хранить пароль от keystore в ProjectSettings.asset нельзя: Unity и так не сохраняет пароли в проекте, а значит, на CI их надо передать снаружи. Лучший способ — переменные окружения, которые CI подставляет из секретов:

private static void ApplyAndroidSigning()
{
    string keystore = Environment.GetEnvironmentVariable("ANDROID_KEYSTORE_PATH");
    if (string.IsNullOrEmpty(keystore))
        return; // локальная сборка — подпись отладочным ключом

    PlayerSettings.Android.useCustomKeystore = true;
    PlayerSettings.Android.keystoreName = keystore;
    PlayerSettings.Android.keystorePass = Environment.GetEnvironmentVariable("ANDROID_KEYSTORE_PASS");
    PlayerSettings.Android.keyaliasName = Environment.GetEnvironmentVariable("ANDROID_KEY_ALIAS");
    PlayerSettings.Android.keyaliasPass = Environment.GetEnvironmentVariable("ANDROID_KEY_ALIAS_PASS");
}

Переменные окружения, а не аргументы командной строки, выбраны не случайно. Аргументы процесса видны в списке процессов и часто попадают в лог джобы целиком, а секреты из переменных окружения CI-системы маскируют в логах автоматически.

Для AAB-формата (обязательного для Google Play) включите EditorUserBuildSettings.buildAppBundle = true и указывайте путь с расширением .aab.

Собираем всё вместе

Вот итоговый метод, который объединяет всё сказанное. Он читает аргументы, ставит версию, подпись и define-символы, собирает под платформу, на которую открыт редактор, и возвращает правильный код выхода.

public static void BuildFromCommandLine()
{
    try
    {
        BuildTarget target = EditorUserBuildSettings.activeBuildTarget;
        bool dev = BuildArgs.Has("dev");

        ApplyVersion();
        if (target == BuildTarget.Android)
        {
            ApplyAndroidSigning();
            EditorUserBuildSettings.buildAppBundle = !dev;
        }

        string output = BuildArgs.Get("outputPath") ?? DefaultPath(target, dev);

        var options = new BuildPlayerOptions
        {
            scenes = EditorBuildSettings.scenes.Where(s => s.enabled).Select(s => s.path).ToArray(),
            locationPathName = output,
            target = target,
            options = GetOptions(dev),
            extraScriptingDefines = dev ? new[] { "DEV_CONSOLE" } : Array.Empty<string>()
        };

        if (options.scenes.Length == 0)
            throw new BuildFailedException("В Build Settings нет ни одной включённой сцены");

        Finish(BuildPipeline.BuildPlayer(options));
    }
    catch (Exception e)
    {
        Debug.LogException(e);
        if (Application.isBatchMode)
            EditorApplication.Exit(1);
    }
}

private static string DefaultPath(BuildTarget target, bool dev)
{
    string kind = dev ? "dev" : "release";
    string version = PlayerSettings.bundleVersion;

    return target switch
    {
        BuildTarget.StandaloneWindows64 => $"Builds/{kind}/Windows/{Application.productName}.exe",
        BuildTarget.StandaloneOSX => $"Builds/{kind}/macOS/{Application.productName}.app",
        BuildTarget.Android => $"Builds/{kind}/Android/game-{version}.{(dev ? "apk" : "aab")}",
        BuildTarget.WebGL => $"Builds/{kind}/WebGL",
        _ => $"Builds/{kind}/{target}"
    };
}

Обратите внимание на EditorUserBuildSettings.activeBuildTarget: метод не решает, под какую платформу собирать, он просто берёт ту, на которую редактор открыт флагом -buildTarget. Один метод — все платформы, а выбор платформы остаётся за командной строкой.

Пример для GitHub Actions

Собирать Unity на облачных раннерах GitHub можно через готовые экшены сообщества GameCI (game-ci/unity-builder): они берут на себя установку редактора в Docker-контейнере и активацию лицензии. Наш скрипт с ними совместим: у экшена есть параметр buildMethod, куда передаётся имя статического метода, и customParameters для своих аргументов. Если же у вас свой раннер с установленным Unity, всё сводится к одному шагу:

jobs:
  android:
    runs-on: [self-hosted, unity]
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true

      - uses: actions/cache@v4
        with:
          path: Library
          key: library-android-${{ hashFiles('Packages/packages-lock.json') }}

      - name: Build
        env:
          ANDROID_KEYSTORE_PATH: ${{ runner.temp }}/release.keystore
          ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }}
          ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
          ANDROID_KEY_ALIAS_PASS: ${{ secrets.ANDROID_KEY_ALIAS_PASS }}
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$ANDROID_KEYSTORE_PATH"
          "$UNITY_PATH" -batchmode -quit -nographics \
            -projectPath . \
            -buildTarget Android \
            -executeMethod BuildScript.BuildFromCommandLine \
            -buildVersion 1.4.${{ github.run_number }} \
            -buildNumber ${{ github.run_number }} \
            -logFile -

      - uses: actions/upload-artifact@v4
        with:
          name: android-release
          path: Builds/release/Android/

Несколько деталей, которые экономят часы:

  • Кэш Library привязан к платформе в ключе. Кэш от Windows-сборки для Android не подходит и только замедлит первый импорт.
  • Keystore хранится в секрете в base64 и разворачивается во временную папку раннера, а не в папку проекта, откуда он мог бы случайно попасть в артефакты.
  • github.run_number монотонно растёт, поэтому годится как bundleVersionCode: Google Play не примет сборку с кодом меньше или равным уже загруженному.
  • -logFile - выводит лог Unity в консоль джобы. Без этого при падении вы увидите только «exit code 1» без объяснений.

Частые проблемы и как их узнать по логу

Процесс завершился мгновенно, сборки нет

Почти всегда это ошибка в имени метода. Unity пишет в лог, что не может найти метод для -executeMethod. Проверьте полное имя с пространством имён и что метод публичный и статический. Второй вариант — ошибки компиляции: если в проекте есть хоть одна ошибка компиляции, editor-сборка с вашим скриптом не загрузится, и вызывать будет нечего. Ищите в логе строки с error CS.

Другой процесс Unity уже открыл этот проект

Unity не позволяет открыть один проект двумя редакторами. Если вы запускаете сборку локально, закройте редактор с этим проектом. На CI это обычно значит, что предыдущая джоба зависла: добавьте таймаут на шаг сборки.

Сборка «успешна», но поведение как у dev-сборки или наоборот

Проверьте, не переключаете ли вы define-символы через PlayerSettings и не проверяете ли их через #if в editor-коде. Перейдите на extraScriptingDefines и чтение аргументов.

Первая сборка на сервере идёт очень долго

Это импорт всех ассетов с нуля в пустую папку Library. Настройте кэш, как в примере выше. Если у проекта много текстур, имеет смысл посмотреть и в сторону Unity Accelerator — кэш-сервера импорта, который делят все машины команды.

Лицензия

Batchmode тоже требует активированной лицензии Unity. На собственном раннере достаточно один раз войти в Unity Hub под пользователем, от которого работает раннер. В контейнерах лицензию активируют через переменные окружения, — экшены GameCI описывают этот процесс в своей документации.

Что в итоге

Скрипт сборки — это пара сотен строк, которые окупаются уже на второй неделе. Минимальный набор, с которого стоит начать:

  1. Статический метод в папке Editor, который собирает через BuildPipeline.BuildPlayer и смотрит на BuildReport.
  2. Запуск с -batchmode -quit -buildTarget ... -executeMethod ... -logFile - и явный EditorApplication.Exit с кодом 1 при ошибке.
  3. Версия и номер сборки — из аргументов, define-символы — через extraScriptingDefines, пароли — из переменных окружения.
  4. Проверки, общие для всех сборок, — в IPreprocessBuildWithReport с BuildFailedException.

Когда это заработает локально одной командой, перенос в CI займёт полчаса. А дальше можно наращивать: собирать несколько платформ параллельно, отправлять готовые сборки тестировщикам, публиковать в магазины. Основа у всего этого одна — сборка, которая запускается без единого клика.

Комментарии

Чтобы оставить комментарий, войдите в аккаунт.

Пока нет комментариев. Будьте первым!

Смотрите также