Files
Faset_Engine/docs/studies/18-build-cook-and-delivery.md
T

36 KiB
Raw Blame History

18. Сборка, cook и доставка: как сделать результат объяснимым

Дата исследования: 17.09.2026; актуализация решений: 18.09.2026. Дополнение к исследованию стеков. Приняты CMake+Ninja, Clang Linux/clang-cl Windows, C++ core/gameplay первым и Lua следующим модулем. Gameplay статически линкуется в отдельный Player dev/release; schema export выполняется служебным процессом; Editor plugins — DLL/SO под точный SDK/restart. MCP существует только в Editor/headless editor services, без runtime inspection/mutation и без MCP в Player/SchemaExporter. Канон — архитектура, этапы — PLAN.md. Ниже сохранён проект BuildService на этапе исследования. Реализованный MVP-профиль и проверки — в Manual экспорта и журнале реализации.

Исходники: Unreal Engine 5.8.2, commit 16d75d84714512edfb744e1fd0a59e9c74d57873; общий манифест. Ни UE, ни будущий Faset в этом исследовании не собирались. Ниже — чтение тел функций и официальной документации, затем собственный проект контрактов и проверок.

1. Первый полезный перенос: два разных вопроса об актуальности

В Unreal есть отдельная проверка плана сборки и проверка действий внутри плана. Это существенно для UX: изменение одного .cpp не должно выглядеть так же, как смена SDK или добавление нового исходного файла.

В TargetMakefile сравниваются дополнительные аргументы, использованные конфигурационные значения и внешние метаданные платформы. IsValidForSourceFiles ищет новые, удалённые и заменённые исходники, изменение набора inline-generated C++, внешних и внутренних зависимостей, платформенного SDK. Причины возвращаются вызывающему коду текстом. Это позволяет решить, можно ли повторно использовать сохранённое описание сборки. Проверки конфигурации, проверки исходников.

После получения графа GatherPrerequisiteActions начинает с требуемых выходных файлов и рекурсивно собирает производящие их действия. GatherAllOutdatedActions сначала параллельно проверяет действия по отдельности, затем распространяет устаревание через зависимости. Это не доказательство, что любая часть UBT параллельна, и не готовый рецепт собственного планировщика. Полезен сам порядок: выбрать необходимую часть графа, проверить локальные причины, учесть зависимые действия. Обход prerequisites, два этапа актуальности.

Faset должен показывать цепочку причин; для принятой явной C++ регистрации пример выглядит так:

PlayerMovement.hpp изменён
  → компиляция зависящих gameplay/registration файлов
  → обновление статической gameplay-библиотеки и линковка Player
  → линковка/запуск SchemaExporter при изменении его зависимостей
  → проверка и публикация декларативного schema manifest

Это пример будущего интерфейса. Регистрация пишется явно на C++, собственный parser/codegen всего C++ на первом этапе не нужен. Если schema export выдаёт прежнее содержимое, зависимые от самой схемы стадии не должны получать ложную invalidation; изменившийся gameplay binary всё равно требует нового Player. «План обновлён», «команда выполнена» и «выходной файл изменился» — три разных события. CLI, окно сборки и MCP должны получать их из одного журнала.

2. Почему mtime и один хеш исходника недостаточны

IsIndividualActionOutdated учитывает путь команды, аргументы и версию команды; отсутствие результата; для compile-действий — также нулевой размер .obj/.o; прямые prerequisites и dependency-list. Отсутствующий dependency-list делает действие устаревшим. Список позволяет проверить зависимости, которые не были обычными prerequisites. Проверки времени в этом коде имеют собственные допуски и правила обработки import libraries: переносить их механически не следует. Тело проверки.

Отдельная интересная деталь: ActionHistory.ComputeHash переводит строку в верхний регистр перед хешированием, а UpdateProducingCommandLine хранит историю по выходному файлу. Это характеристика данного механизма UE, не доказательство ошибки его использования. Для собственного кэша Faset нельзя бездумно повторять нормализацию: регистр Linux-пути, имени macro и значения аргумента может менять смысл. История команды.

Ключ подготовленного артефакта должен учитывать:

  • идентификатор и версию преобразователя, его binary/toolchain identity;
  • байты входов и транзитивных зависимостей;
  • упорядоченные аргументы, значимые настройки и явно разрешённые переменные окружения;
  • целевую ОС, архитектуру, конфигурацию и графический профиль;
  • версию формата результата и схему метаданных, если она влияет на выход.

Для первого локального прототипа достаточно манифеста зависимостей и кэша файлов. Удалённый кэш, распределённое исполнение и собственная замена Ninja не являются необходимым началом. Быстрая проверка по размеру/mtime допустима как оптимизация обнаружения изменений, но не как единственное основание корректности content cache.

Изменение SDK, compiler flags или импорта должно давать объяснимую причину промаха. Ключ без полного набора входов может очень быстро выдавать неправильную игру. Ключ со слишком широкими зависимостями будет правильным, но лишит проект быстрой итерации. Поэтому correctness и гранулярность проверяются отдельно.

3. Cook-зависимость не равна runtime-зависимости

В FSaveCookedPackageContext::FinishPlatform после сохранения обрабатываются пакеты, незапланированно загруженные во время save/cache/generator-работы. Затем вычисляются общие и платформенные runtime-зависимости, cook-зависимости и build-зависимости. После этого заполняется CommitPackageInfo и вызывается writer. У commit есть статус; наличие вызова само по себе не означает успешную публикацию всей игры. Последовательность FinishPlatform.

CalculatePlatformRuntimeDependencies добавляет общие зависимости, imports и soft references из результата save, затем обнаруженные зависимости. CalculateCookDependencies отдельно собирает зависимости результата подготовки; когда обычного результата save нет, выполняет harvesting через cook events и архив. RecordPlatformBuildDependencies извлекает транзитивные build dependencies из cook attachments. Runtime-зависимости, сбор cook-зависимостей, build-зависимости.

Практический пример для Faset: Blender-файл, профиль экспорта и версия mesh-cooker нужны для получения mesh, но не обязаны входить в поставку игры. Материал, skeleton и runtime-текстуры могут понадобиться Player. Debug symbols нужны разработчику при разборе сбоя, но могут поставляться отдельно. Один список «всех файлов проекта» не выражает эти отношения.

Предлагаемый importer/cooker возвращает три набора: использованные входы, полученные артефакты и runtime-ссылки. Если во время преобразования он прочитал дополнительный файл, зависимость надо зарегистрировать до признания результата пригодным для кэша. Доступ к файлам через предоставленный контекст импорта позволит собирать эти сведения; произвольные внешние процессы потребуют явного dependency manifest. Сам по себе этот API не обнаруживает чтения, которые его обходят.

Сборка пакета начинает с выбранных сцен и явно заданных runtime-корней. Ссылки, формируемые строками во время игры, требуют явного правила включения или каталога; статический обход не может угадать произвольную строку. В редакторе полезны два объяснения: «почему файл попал в игру» и «почему его изменение требует пересборки». Их графы связаны, но не совпадают.

4. Проверять нужно и попадания в кэш

Самая полезная находка в cooker — FIncrementalValidatePackageWriter. В режимах проверки он может принудительно пересохранить пакет, который инкрементальная логика сочла неизменённым. Первая фаза выявляет расхождение с ранее сохранённым результатом; повторная помогает отделить недетерминированный save от неверного решения пропустить работу. Выбор пакетов, сравнение и дополнительные проходы.

LogIncrementalDifferences прямо разделяет случаи: повторные результаты различаются — проблема детерминизма; повторные результаты совпадают, но отличаются от принятого ранее — false positive решения об отсутствии изменений. Это хороший способ диагностировать кэш, хотя конкретная причина неверного пропуска ещё требует расследования. Классификация.

Для Faset предлагается отдельный проверочный режим verify-cache. Он не должен делать каждую обычную сборку вдвое дороже. На фиксированном наборе fixtures либо на выборке CI:

  1. Сохранить старый артефакт A и решение кэша «пригоден».
  2. Для зафиксированного snapshot входов принудительно получить B, затем C.
  3. Если B и C различаются, сначала исследовать нестабильность преобразования или среды.
  4. Если B = C, но B ≠ A, исследовать недостающую зависимость, неверный ключ или порчу A.
  5. Если все равны, сохранить свидетельство пройденной проверки для этого набора входов.

Нельзя объявлять глобальную детерминированность после одной пары запусков. Сравнение должно учитывать формат: для бинарного runtime-артефакта желательны воспроизводимые байты; timestamps и диагностические поля лучше вынести в отдельный manifest. Допустимая нормализация сравнения обязана быть перечислена, иначе она может скрыть ошибку. Межплатформенное совпадение байтов не требуется там, где выход намеренно платформенный.

5. Что стоит поручить CMake, Ninja и инструментам C++

Faset владеет моделью проекта, схемами компонентов, импортом, job status и упаковкой; CMake/Ninja отвечают за native compilation graph. Это уменьшает количество одновременно создаваемых механизмов.

CMake File API даёт редактору структурированные сведения о сконфигурированном build tree. Клиент размещает запрос в своём каталоге query; после configure/generate читает индекс ответа и указанные им файлы. Версии протокола и объектов нужно согласовывать. Это источник сведений о targets и конфигурации, а не поток прогресса выполняемой компиляции. Файлы ответа принадлежат CMake; клиент не должен удалять их. CMake File API.

Presets позволяют отделить общие настройки от локального окружения. CMakePresets.json предназначен в том числе для хранения в репозитории, CMakeUserPresets.json — для персональных настроек без включения в Git. Для Faset это подходит как нижний слой общих профилей и локальных путей SDK; пользовательский экран может называться просто «Сборка Linux / Windows». Минимальную версию CMake и формат presets ещё нужно выбрать. CMake Presets.

Schema export и компиляция Slang/совместимого HLSL в SPIR-V имеют явные outputs, byproducts и входные зависимости. add_custom_command описывает такие результаты и поддерживает depfile при совместимом генераторе. Для Ninja byproducts позволяют восстановить отсутствующий побочный файл. В принятом первом варианте C++ metadata регистрируется вручную, а exporter выдаёт JSON manifest. Если позднее появится generator .hpp/.cpp, исчезновение одного output не должно маскироваться существованием другого. CMake add_custom_command.

Ninja depfile и restat решают разные задачи. Depfile сообщает обнаруженные файловые зависимости. restat повторно проверяет время изменения outputs после команды и позволяет убрать зависимые действия из очереди, когда выход не изменился. Следовательно, генератору полезно записывать файл лишь при изменении содержимого; одного имени режима «incremental» недостаточно. Зависимость от генерации заголовка должна существовать уже для первой чистой сборки, когда depfile ещё отсутствует. Ninja manual, официальный исходник документа.

compile_commands.json передаёт C++-инструментам контекст компиляции файла. Формат содержит рабочий каталог, файл и аргументы либо команду; документация предпочитает массив arguments, чтобы не вносить ошибки shell escaping. Для Faset это связующее звено с анализом кода и редактором C++, но не полное описание упаковки и ресурсов игры. Clang compilation database.

Собственный launcher также должен запускать executable с массивом аргументов и явным рабочим каталогом. Отображаемая командная строка — диагностическое представление; её не следует повторно парсить как единственный источник данных. Структурированные ошибки сохраняют файл, позицию, этап и исходный текст сообщения, чтобы человек и MCP видели один и тот же результат.

SchemaExporter и граница редактора. После компиляции отдельная служебная target выполняет C++ registration entrypoints и выдаёт TypeId/FieldId/defaults/value types/constraints/UI hints с build fingerprint. Она не запускает игровой мир, lifecycle или renderer, не содержит MCP и не попадает в игровой пакет. Inspector читает проверенный декларативный manifest; custom inspectors принадлежат отдельному Editor module. Сбой exporter оставляет прежнюю схему. Исполнение C++ в другом процессе изолирует crash, но не является sandbox.

Зависимости модулей. Gameplay статически связан с Player в development/release; исправления C++ требуют stop → build → restart. Editor DLL/SO имеют manifest с зависимостями и точным SDK/build fingerprint, загружаются при старте и обновляются через restart Editor. Toolchain fingerprint фиксирует target/architecture, compiler, stdlib/CRT и настройки; Windows SDK/UCRT/VCRuntime являются отдельными компонентами, а не частью обещанной полностью независимой поставки LLVM. Стабильного ABI любых компиляторов и C++ hot reload нет. Clang: Windows headers/libraries.

6. Результат сборки — описанный набор файлов

UE TargetReceipt содержит target/platform/configuration, launch executable, build products и runtime dependencies. Writer упорядочивает списки; пути внутри engine/project могут заменяться переменными, но вне этих каталогов InsertPathVariables возвращает абсолютный путь. Это не готовый переносимый формат Faset. ModuleManifest отдельно хранит build ID и соответствие имён модулей файлам. Это не универсальная гарантия ABI-совместимости: manifest обозначает состав и идентичность, а проверка совместимости требует отдельного контракта. TargetReceipt.Write, правила путей, ModuleManifest.

Результат Faset — проектируемый ArtifactManifest: target, build ID, snapshot ID, toolchain identity, entry point, список файлов с хешами и назначением, графический профиль, версии runtime-форматов, ссылки на symbols и отчёт проверки. В нём нет зависимости от абсолютного каталога разработчика. Для сторонних runtime-библиотек нужен явный список поставки; загрузка библиотек по вычисляемому имени требует отдельного правила включения.

BuildCookRun в UE последовательно вызывает Build, Cook, CopyBuildToStagingDirectory, Package, Archive, Deploy и Run. Разделение шагов полезно, но код orchestration сам по себе не доказывает атомарность публикации. DoBuildCookRun.

Для первого Faset принят следующий протокол:

  1. Зафиксировать входной snapshot/revision, материализовать его в изолированном рабочем каталоге worker и запретить изменение входов на время job. Compiler/cooker читают этот snapshot, а не живые файлы редактора. Build tree имеет единственного writer; разные worker не используют один CMake binary directory одновременно. Каталог можно повторно использовать между совместимыми job после контролируемого обновления snapshot. Одного revision ID или проверки timestamps после сборки для изоляции недостаточно.
  2. Компилировать C++, выполнять schema export, компилировать шейдеры и готовить binary cooked assets, сохраняя прогресс и использованные зависимости.
  3. Собрать candidate в новом каталоге build ID, проверить обязательные файлы, хеши и согласованность manifest.
  4. Запустить минимальную проверку Player на целевой ОС. Результат сборки и результат запуска — разные статусы.
  5. После успеха опубликовать ссылку на готовый build ID. Старые результаты сохраняются до явной очистки.

Этот подход позволяет не перезаписывать бинарники запущенного Player. Обновление маленького указателя на актуальную сборку через временный файл и rename/replace требует платформенной реализации и fault-тестов. Атомарная видимость не тождественна сохранности при потере питания. Для первого прототипа можно возвращать путь неизменяемого каталога без понятия глобального current, ещё больше сокращая протокол.

7. Одинаковая сборка руками, из CLI и через MCP

Предлагаемые сущности: BuildRequest, BuildJob, BuildEvent, ArtifactManifest. Запрос содержит проект, target/configuration, snapshot, выбранные сцены/профиль и ключ повторного запроса. Job получает устойчивый ID; события имеют порядковый номер. Конкретные имена инструментов и wire format ещё не утверждены.

MCP-адаптер Editor/headless editor service запускает job, читает статус/editor-диагностику с позиции журнала, запрашивает отмену и получает артефакты. Play/Stop управляет процессом; runtime world/session inspection/mutation не предоставляется, MCP в Player/SchemaExporter отсутствует. Длительная работа не должна зависеть от жизни одного HTTP-запроса или открытого окна редактора. В пределах документированного scope и срока хранения журнала повторный вызов с тем же ключом и тем же содержимым возвращает существующую работу; с другим содержимым — конфликт. Это проект прикладного API, не утверждение о встроенных гарантиях протокола MCP.

Отмена означает переход через cancelling: worker прекращает новые действия, завершает или останавливает дочерние процессы, закрывает файлы и только затем подтверждает конечное состояние. Candidate отменённой сборки не становится последней успешной игрой. После перезапуска сервиса незавершённые job требуют reconciliation по журналу и состоянию workers; наличие каталога не означает успех.

В GUI полезны этап, текущая задача, elapsed time, причины пересборки и ссылки на ошибки. Процент допустим при известном знаменателе; динамически обнаруживаемые зависимости не оправдывают выдуманную точность. Для первой версии хватит локального worker текущей ОС. Вторую ОС следует проверять собственным worker/CI; переключатель target не является доказательством готовой кросс-компиляции.

8. Минимальный набор испытаний принятой реализации

  • Чистая и повторная сборка. Удалить только производные outputs; проверить порядок генерации. Повторить без правок и увидеть, какие стадии действительно не выполнялись.
  • Изменения по одному. .cpp, общий header, схема компонента, shader include, исходная текстура, параметры importer, версия cooker, compiler flags. Проверить правильный результат и объяснение цепочки.
  • Отсутствующий output. Удалить schema manifest, dependency-list, runtime-библиотеку или cooked mesh; получить восстановление либо явную ошибку, а не ложный успех.
  • Кэш. Испытать verify-cache на специально пропущенной зависимости и намеренно нестабильном преобразовании. Классификации должны различаться.
  • Сбой публикации. Отмена, завершение процесса cooker, ошибка компилятора, нехватка места, прерывание записи manifest. Последняя успешная игра остаётся доступной.
  • Повтор и конкуренция. Повторить запрос MCP, запустить два разных snapshot, изменить проект во время cook. Не смешать outputs и не выполнить одну логическую job повторно без причины.
  • Доставка. Запустить development и release Player на чистых Linux/Windows окружениях без Editor, исходников, Blender, MCP и SDK. Проверить пути с пробелами/Unicode, case-sensitive файловую систему, runtime-библиотеки и отсутствие editor/MCP targets/listeners в обоих Player.
  • Схема и плагины. Проверить сбой SchemaExporter без потери прежней схемы, отсутствие lifecycle вызовов при export, несовместимый plugin fingerprint и запрещённую зависимость runtime→editor. Проверить, что GUI/editor MCP видят одну authoring-схему и не умеют читать игровой мир.

Измерять следует cold/warm/no-op wall time, время стадий, причины cache hit/miss, объём пересозданных данных и корректность результата. Численные бюджеты появятся после измерения прототипа на зафиксированном проекте и оборудовании.

Куда перейти дальше

Нативные компоненты и метаданные определяют входы явной регистрации/schema export и совместимость модулей. Импорт и обмен с Blender определяют происхождение артефактов, их идентификаторы и поведение reimport. Графический pipeline определяет требования runtime к данным и GPU-профилю. Канонические решения остаются в архитектуре Faset, этапы до MVP и после — в PLAN.md. Все испытания движка ещё предстоят.