# Что такое система сборки ya make

## Что такое система сборки

В мире существует достаточно много различных систем сборки, часть из них интегрирована в язык, как в go или rust, а часть являются универсальными как `make`, `CMake`, `gradle`  или `bazel`.
Но в любом случае цель системы сборки —  собрать из исходного кода программу или библиотеку с учётом всех зависимостей.

Системы сборки обычно предоставляют следующий набор возможностей:

- **Сборка по зависимостям.** Если, например, программа зависит от модуля, то при изменении в модуле и при сборке программы они будут пересобраны оба.

- **Инкрементальная сборка.** Системы сборки так или иначе стараются обнаруживать изменения и перестраивать только то, что затронуто этими изменениями.

- **Параллельная сборка.** Зная *граф зависимостей* между сборочными командами, система сборки может запускать независимые его команды параллельно.

- **Не только сборка.** Исполнение команд по зависимостям важно не только для сборки. Обычно системы сборки позволяют исполнять любые команды, в частности запускать тесты при изменении зависимостей среди которых может быть не только код, но и входные данные.


Системы сборки можно классифицировать по разным критериям.

По анализу зависимостей системы сборки делятся на

- **Статические**, которые анализируют зависимости перед исполнением сборки и не могут обнаруживать новые зависимости уже в процессе сборки. Таким системам надо заранее знать всё, что может понадобиться для сборки. Не все найденные зависимости могут быть важными: статическая система сборки может игнорировать часть зависимостей в процессе исполнения, она лишь не может найти новые. Это снижает гибкость, но повышает предсказуемость как по времени, так и по результату. Такую сборку проще кэшировать.
- **Динамические**, которые могут обнаруживать новые зависимости уже в процессе сборки. Такая система сборки может, например, обнаружить, что для сборки какого-то модуля нужен ещё один пакет и прямо во время исполнения привезти его и собрать. Понятно, что такой процесс менее предсказуем как по времени (мы заранее не знаем сколько пакетов нам может понадобиться), так и по результату: мы можем не знать заранее что и где изменилось. Нам надо исполнить или проэмулировать сборку, чтобы понять, что ничего не надо пересобирать.

По описанию системы сборки делятся на

- **Встроенные**, где, по сути, нет отдельного описания, вся информация содержится в исходном коде. Обычно такие системы предназначены для проектов на одном языке, но могут иметь возможность описывать запуск инструментов, для генерации исходного кода. Например, в `go`.
- **Высокоуровневые**, где описание делается в терминах правил и связей, за которыми скрыты реальные сборочные команды и зависимости между ними. Примерами являются `CMake`, `gradle` и `bazel`.
- **Низкоуровневые**, где явно описываются команды преобразования исходных файлов в результат, связи между этими командами и т.п. Примеры —  это `make` и `ninja`.

По языку описания сборки системы сборки делятся на

- **Императивные**, язык описания сборки содержит непосредственно исполняемые конструкции, с помощью которых можно выяснять зависимости, получать и настраивать сборочные инструменты, настраивать сборочное окружение. Такое описание даёт гибкость, но снижает предсказуемость.
- **Декларативные**, язык описания сборки содержит правила и зависимости, а все действия по анализу, настройке инструментов и окружения скрыты в коде самой системы сборки. Такое описание ограничивает возможности системы сборки, но делает её более предсказуемой по времени.

## Система сборки ya make

Система сборки `ya make` может быть описана как

- Полностью *статическая*. Все зависимости анализируются заранее и изменения фиксируются в *графе команд*. На основе анализа каждая команда получает уникальный идентификатор *UID*, который фиксирует её результат на данном состоянии входных данных и зависимостей. Неизменность *UID* говорит о неизменности её результата и потому служит ключом в *кэше результатов*, а также используется при *анализе изменений* для исключения команды из исполнения.

- Универсальная и *высокоуровневая*. Описание системы сборки делается на уровне модулей, макросов и зависимостей между модулями. Наша система сборки скрывает очень многое.
  * Она сама строит межфайловые зависимости как прямые *(a.cpp включает b.h)*, так и индуцированные генерацией *(если x.proto импортирует y.proto, то x.pb.h будет включать y.pb.h)*. Эти зависимости внутри отображаются на команды: команда компиляции `a.cpp` будет перезапущена при изменении `b.h`, а команда изменение в `y.proto` повлечёт не только перегенерацию для `x.proto`, но и перекомпиляцию `z.cpp`, который включает `x.pb.h`.
  * Она сама строит цепочки обработки файлов - включение в модуль `GO_LIBRARY()` файла `w.proto` повлечёт генерацию из него .pb.go и дальнейшую трансляцию этого файла в составе пакета.
  * Она может связывать результаты работы макросов по именам: макрос, генерирующий `m.json` и макрос складывающий его в программу как ресурс в рамках одного модуля связываются просто по имени файла.

- *Декларативная*, но не совсем. В описании сборки большая часть конструкций фиксирует свойства модулей и команд и связи между ними. Однако, часть конструкций выполняется последовательно: установка и вычисление локальных переменных, условные конструкции — это то, что зависит от порядка, в котором написано в ya.make-файле.

Для корректной работы нашей системы сборки требуется *герметичность*: команды не должны неявным образом зависеть ни от окружения (файлов в системе, настроек системы и т.п.) ни от результата или порядка исполнения других команд.
Все зависимости должны быть известны системе сборки, чтобы сформировать корректный *UID* для каждой команды. Наша система сборки предоставляет средства для герметичного исполнения: все инструменты, используемые в сборке или
зафиксированы в бинарном виде, или строятся из исходного кода в рамках сборки. Единственными допустимыми источниками входных данных являются репозиторий Аркадия и хранилище Sandbox, ссылки в которое по ID фиксируются в репозитории.
Сам Sandbox гарантирует неизменность данных для ID.

Такая герметичность обеспечивает *воспроизводимость* —  сборка даст *функционально эквивалентный* результат в любой среде при фиксированном состоянии репозитория и запрошенной конфигурации.
Это обеспечивает как простую возможность воспроизводить проблемы, так и организовывать распределённую сборку и кэширование. Для обеспечения воспроизводимости мы стараемся не использовать
директории и шаблоны имён (globs) как входные параметры сборки. Явное описание входов позволяет избегать нежелательных эффектов при наличии в рабочей копии посторонних файлов (отладочных логов, недописанного кода и т.п.).

Важно понимать, что система сборки не гарантирует *бинарную воспроизводимость* и не опирается на неё в своей работе. Меняющаяся информация (времена, пути исходного кода на файловой системе и т.п.) могут изменять артефакты, но, если мы не считаем их существенными для функционирования кода, мы не засчитываем их в *UID*.
Это повышает инкрементальность и делает сборку устойчивой к *сборочному шуму* - случайной информации, вносимой инструментами в артефакты.

Вторым важным свойством нашей системы сборки является *масштабируемость*. Наша система сборки построена таким образом, что:

- Активно использует кэширование как информации, используемой в процессе анализа зависимостей, так и кэширование результатов. Воспроизводимость позволяет использовать в локальной работе распределённый кэш, прогреваемый на кластере распределённой сборки.

- Наша система легко шардируется: в сценарии CI, когда нам надо проверить влияние изменений на все тесты в репозитории система анализирует зависимости и исполняет сборки частями. Даже при наличии большого количества горизонтальных связей
  (зависимостей между проектами в разных частях репозитория) заметно снижает время на анализ зависимостей  ускоряет сборку. Для каждой части (partition) система сборки строит свой замкнутый *сборочный граф*, который исполняется отдельно
  независимо на кластере распределённой сборки, а развитое кэширование позволяет не дублировать работу.

## Почему у нас своя система сборки

У нас в Яндексе очень большой монорепозиторий Аркадия с политикой trunk-based development, зависимостями по исходному кода и статической сборкой. C Аркадией одновременно работают тысячи человек
и им важно, чтобы код, который они пишут собирался и проверялся быстро как на их рабочих машинках, так и в системе CI, где исполняются проверки на каждый пулл-реквест и каждое вливание в транк.
На наших масштабах (а это десятки тысяч проектов, миллионы файлов и в пике до 10 вливаний в транк в минуту) практически любой системе сборки придётся тяжко даже сейчас.

Уже в 2012, когда репозиторий был сильно меньше, анализ зависимостей с помощью тогдашней системы сборки CMake занимал больше часа. В то время системы сборки для такого масштаба на рынке
не было. bazel, buck и pants появились позже. Поэтому было принято делать свою систему сборки. К ней предъявлялись следующие требования:

- Скорость и масштабируемость
- Поддержка широкого спектра конфигураций и платформ, включая кросс-сборку
- Работа на Linux, macOS и Windows
- Воспроизводимость
- Простота описания и близость синтаксиса к CMake для упрощения миграции

Так и появился ya make. Нынешняя система сборки не идеальна, но она действительно эффективна - позволяет проанализировать все зависимости во всём нынешнем репозитории за 10 минут,
80% сборок в CI укладываются в 25 минут вместе с тестированием. И это при том, что возможности оптимизации ещё не исчерпаны.

## Возможности системы сборки ya make

Система сборки ya make позволяет собирать и тестировать код на 4х основных языках - C++, Python, Java и go; она имеет развитые средства работы с protobuf.
Сборка может исполняться локально с локальным кэшированием, локально с удалённым кэшированием, а также на кластере распределённой сборки.

**Подробности:**

- [Как работает система сборки ya make, её плюсы и минусы](./how_it_works.md)
- [Как описывается сборка для ya make](../manual/index.md)


Ниже приведены особенности поддержки различных языков и технологий в системе сборки ya make.

### Особенности сборки C++

Система сборки ya make предоставляет развитые средства сборки C++. Помимо обычной сборки кода в библиотеки и линковки их в программы, для модулей на C++ есть следующие возможности:

- Любой модуль С++ по умолчанию зависит не только от стандартной библиотеки C++, но и от нашей собственной библиотеки базовых примитивов [util](https://a.yandex-team.ru/arc/trunk/arcadia/util).
- Можно указывать дополнительные флаги сборки (как на модуль целиком, так и для компиляции отдельного файла). Можно указать дополнительные флаги линковки даже в библиотеке, они будут действовать на все зависящие программы.
- В качестве исходного кода можно указывать не только C/C++, но и ассемблер (.asm, .yasm, .masm и .s), ragel (.rl5 и .rl6), lex и bizon (.lex и .y) а также несколько собственных форматов кодогенерации.
- Можно указать, что файл из библиотеки должен линковаться непосредственно в программу для гарантированного запуска статической инициализации.
- Для тестирования поддержаны фреймворки gtest и наш собственный unittest, реализована возможность написание бенчмарок на двух фреймворках.

**И это далеко не всё. Подробнее про сборку С++:**

- [Пошаговое руководство по описанию сборки на С++](../tutorials/cpp.md)
- [Раздел про описание сборки на C++](../manual/cpp/index.md)

### Особенности сборки Python

В системе сборки ya make реализована сборка Python в бинарный код. Во внешнем мире Python обычно не требует сборки вовсе или собирается в wheel-пакеты. В Аркадии реализована сборка Python в исполняемые
[*программы*](../manual/python/index.md#arcaida_python). Такая сборка замкнута - все необходимые модули попадают внутрь программы, поэтому системе сборки надо знать весь исходный код и зависимости программы.

Результатом такой сборки являются исполняемые файлы, который содержат в себе прекомпилированный и исходный код всех необходимых модулей на Python и интерпретатор с модифицированным загрузчиком,
который, собственно, отвечает за разрешение импортов не на файловую систему, а внутрь ресурсов программы.

**Ключевые возможности для Python в ya make**

- На данный момент ya make поддерживает сборку кода как на 2м, так и на 3м Python, а также модули для 2-3-совместимого кода, который может использоваться по зависимостям как из программ на 2м, так и на 3м питоне.
Для таких модулей поддержано тестирование, которое может запускаться для обеих версий.
- Ya make поддерживает простые средства интеграции С/С++ кода в Python с использованием Cython и SWIG. Файлы .pyx и .swg можно указывать как исходный код для библиотек и программ на Python наравне с обычным кодом на Python.
- В сборку ya make встроены линтеры pep8 и flakes, контроль доступности импортов и фреймворк для тестирования pytest.

Кроме этого, ya make поддерживает расширений для Python - бинарных [*модулей*](../manual/python/index.md#py_modules), которые можно загружать во внешний интерпретатор питона простым импортом из кода.

**Подробнее про сборку Python:**

- [Пошаговое руководство по описанию сборки Python](../tutorials/python.md)
- [Раздел про описание сборки Python](../manual/python/index.md)


### Особенности сборки Java/Kotlin

### Особенности сборки Go

Интеграция сборки `GO` в ya make реализована с помощью собственной обёртки к низкоуровневым инструментам go (таким как `compile`, `link`, `asm`, `cgo`).
Поэтому сборка пакетов для `GO`, так же как и для других языков, поддержанных в Аркадии, описывается в ya.make файлах с перечислением исходных файлов. Однако,
в отличие от других языков для go в большинстве случаев не требуется указание зависимостей макросом `PEERDIR`. Все зависимости система сборки получит сама из исходного кода.

Специальные комментарии для сборки, которые умеет обрабатывать стандартный `GO` тулинг, игнорируются в аркадийной сборке и соответствующие им действия должны быть явно описаны
в ya.make файлах: например, включение исходных файлов в сборку в зависимости от целевой архитектуры или операционной системы, дополнительные флаги сборки или кодогенерация.


**Ключевые возможности для go в ya make**

- Система сборки поддерживает такие особенности go как CGO, internal и external тесты. В рамках поддержки protobuf для go поддерживается не только генерация gRPC сервисов, но и gRPC gateway и swagger-схем. Всё это не нужно генерировать и коммитить в репозиторий.
- Генерация кода на go произвольными скриптами и инструментами легко интегрируется в сборку ya make.
- Стандартный `GO` тулинг работает с аркадийным кодом. В репозитории Аркадия внедрён режим `GO модулей` и при правильной настройке стандартные инструменты могут работать с Аркадией,
как с обычной структурой go-проектов.
- С другой стороны утилита ya предоставляет инструмент [`ya tool yo`](https://a.yandex-team.ru/arc/trunk/arcadia/library/go/yo/README.md#avtomaticheskaya-generaciya-ya.make) для конверсии стандартной сборки в ya make.
- В сборку ya make встроены линтер `go vet`, поддержка стандартных средств тестирования и тестирование с помощью `go race`.


**Подробнее про сборку go:**

- [Пошаговое руководство по описанию сборки go](../tutorials/go.md)
- [Раздел про описание сборки go](../manual/go/index.md)
- [Настройка Аркадии для работы с go](https://wiki.yandex-team.ru/devrules/Go/getting-started).


### Поддержка protobuf

Система сборки ya make поддерживает protobuf специальным типом модуля `PROTO_LIBRARY`. В терминологии ya make это *мультимодуль* —  он собирается по-разному в зависимости от того, откуда его используют.
`PROTO_LIBRARY` может становиться библиотекой на C++, Python 2, Python 3, Java и Go, порождая код на соответствующем языке из одних и тех же исходных `.proto` файлов. 

{% note tip %}

В поддержке каждого из языков есть свои небольшие особенности и их все надо учитывать, делая действительно универсальную `PROTO_LIBRARY`. 
Для случаев, когда добиться универсальности не получается есть средства для отключения определённых вариантов.

{% endnote %}

**Ключевые возможности PROTO_LIBRARY**

- Поддержана генерация gRPC сервисов, а также использование различных плагинов, код которых лежит в Аркадии и собирается из неё.
- Система сборки учитывает не только import-зависимости .proto-файлов, но и индуцированные зависимости между генерируемыми файлами.
- Для использования .proto-файлов как данных доступен вариант библиотеки, предоставляющий исходный код.

**Подробнее про поддержку protobuf:**

- [Пошаговое руководство по описанию и использованию `PROTO_LIBRARY`](../tutorials/protobuf.md)
- [Раздел про описание `PROTO_LIBRARY`](../manual/proto/index.md)

### Особенности тестирования

В систему сборки ya make встроены развитые средства тестирования. ya make позволяет использовать тесты на 10 различных фреймворках. Для подготовки тестового окружения предусмотрен механизм рецептов,
который позволяет исполнить произвольный код перед тестами. Например, рецептами можно настроить таблицы в базах данных или даже поднять микросервисы.

Для нескольких фреймворков система сборки ya make позволяет использовать сравнение с *каноническими данными*. Для этого достаточно вернуть ожидаемые данные из теста и *канонизировать*
их закоммитив их рядом с тестами. После этого результаты запуска тестов будут сравниваться с запомненными и при несовпадении тест будет считаться проваленным.

В тестах разрешено использовать данные из репозитория Аркадия и хранилища Sandbox, а также инструменты, построенные ya make из Аркадии.

- [Раздел про тестирование](../manual/tests/index.md)
- [Подробнее о рецептах](../manual/tests/recipe.md)
- [Подробнее о канонических данных](../manual/tests/canon.md)
- [Использование данных в тестах и сборке](../manual/common/data.md)

### Расширенные возможности ya make

Сборка и тестирование исполняются командой `ya make`. Однако, кроме этих функций система сборки предоставляет ещё ряд возможностей.

- Сборка пакетов в нескольких форматах из результатов сборки. Для этого служит команда ([`ya package`](../usage/ya_package/index.md).
- Генерация проектов для нескольких IDE – команды [`ya ide`](../usage/ya_ide/index.md)
- Анализ зависимостей на основе описания сборки с помощью набора команд [`ya dump`](../usage/ya_dump.md)
- Анализ профиля сборки – команда [`ya analyze make`](../usage/ya_analyze.md)

## Где можно улучшиться

Есть несколько аспектов в которых наша система сборки могла бы стать ещё лучше:

- **Инкрементальность.** Наш анализ зависимостей, хоть и опирается на кэши, ещё не полностью инкрементальный. Заметная часть его работы сейчас зависит от полного размера запрошенной сборки, а не только от размера
   и влияния изменений. Мы работаем над тем, чтобы улучшиться здесь и наша финальная цель - полная инкрементальность анализа зависимостей.

- **Эффективность.** Замкнутость по заранее рассчитанным UID-ам приводит к тому, что даже незначительные изменения могут приводить к очень большим переборкам. Так изменение в коде собираемых инструментов безусловно (по UID-ам)
  влияют на результат их работы и дальше по зависимостям до самых результатов сборки. Однако, зачастую эти изменения не влияют на результат их работы и это можно было бы обнаружить и не выполнять ненужные перекомпиляции и перелинковки.
  Внедрение такой схемы может повысить зависимость системы от бинарной воспроизводимости, однако повышение эффективности того стоит.

- **Выразительность.** Наша система сборки обладает развитыми средствами для сборки и тестирования для поддерживаемых в Аркадии языков, однако есть как минимум два аспекта, в которых можно было бы расширить её возможности.
  * Пакетирование - сейчас наша система сборки сама не позволяет собирать пакеты с заданным размещением артефактов. Мы предоставляем отдельную команду для пакетирования ([`ya package`](../usage/ya_package/index.md)).
    Однако, пакеты не являются сборочными целями и потому не могут быть протестированы в рамках нашей системы сборки. Кроме того, для простых случаев иметь отдельные описания сборки и пакетирования просто неудобно.
    Нам бы очень хотелось расширить свои возможности в этом месте.
  * Мультиплатформенные артефакты - в современные форматы бинарного кода зачастую позволяют собрать один артефакт для работы на разных платформах. Таковы Android APK с нативным кодом или macOS Fat binaries. ya make на данный
    момент не позволяет описать сборку такого рода артефактов. Получить такой артефакт можно используя пакетирование с помощью [`ya package`](../usage/ya_package/index.md). Однако это не очень логично и,
    как было отмечено выше, не пригодно для тестирования в рамках системы сборки.
