Nixpkgs — это масштабный функциональный каталог, который служит рецептом построения десятков тысяч открытых проектов с открытым исходным кодом. В масштабах такого обширного и постоянно растущего программного репозитория разрабатывается и поддерживается ПО множеством авторов, что накладывает серьёзные требования на структуру и организацию системы. Одной из ключевых задач является создание интуитивно понятных и изолированных пакетов, чтобы разработчики могли работать эффективно и не увязать в комплексности глобальной инфраструктуры. Важность концепции пакетной изоляции особенно проявляется в граничных случаях, таких как кросс-компиляция. Изначально Nixpkgs допускал ситуацию, когда для каждой конкурентной архитектуры создавались отдельные пакеты, что привело к значительному дублированию кода и усложнило управление.
Примером служат отдельные пакеты binutilsMips, gcc41mipsboot и helloCross, которые требовали параметризации под конкретные платформы вручную. Такой подход почти сразу показал свою непрактичность: количество платформ, поддерживающих различные архитектуры CPU, операционные системы и библиотеки, слишком велико для подобного масштабирования ручного повторения. Решение этой проблемы пришло в виде унификации в конструкции пакетов с введением глобального параметра cross, благодаря чему стало возможно создавать универсальные кросс-пакеты, которые адаптируются к целевой платформе автоматически. Такой подход значительно уменьшил дублирование и облегчил поддержку. Однако даже после этой реорганизации оставались отдельные узкие места.
Одним из них было расхождение между обычным binutils и binutilsCross, что налагало на разработчиков необходимость выбирать между двумя аналогичными, но различающимися инструментами. Возникали вопросы, почему binutils доступны по умолчанию в stdenv, а binutilsCross — нет, и как правильно определять целевую платформу для каждого из инструментов. В 2017 году благодаря усилиям сообщества, включая автора проекта Obsidian, произошел значительный сдвиг в архитектуре кросс-компиляции в Nixpkgs. Была разработана система, где stdenv.cc автоматически предоставляет компилятор, ориентированный на целевую платформу, будь то родная или кросс-платформа.
Эта унификация означает, что пакеты сами по себе теперь не заботятся о том, какой именно тип компилятора им нужен — им просто предоставляется правильный комплект инструментов для специфичной платформы. Это позволило свести к минимуму условные конструкции и специальные случаи в пакетах, обеспечив «прозрачность» кросс-компиляции и улучшив читаемость рецептов. Однако история усложняется, когда речь заходит о libc и связанной с ним кросс-компиляции. libc — одна из ключевых частей любого системного стека, не являющаяся единым пакетом, а скорее платформоориентированным алиасом. Длительное время существовал особый пакет libcCross, служивший для кросс-компиляции, что противоречило идее отказа от платформозависимых специальностей.
Сложности с устранением libcCross были связаны с множеством тонкостей, включая «бесконечные рекурсии» в схемах сборки компиляторов и стандартных библиотек. Только спустя почти десять лет этот элемент удалось интегрировать в единый подход, избавившись от необходимости явно указывать боковые альтернативы в зависимости от платформы. Компиляторы и их стандартные библиотеки часто объединяются в одном репозитории, что легитимно с точки зрения организационной модели разработки, но на практике создает проблемы для бутстраппинга. В частности, компиляторы, такие как GCC, LLVM, GHC и rustc, нередко строят свои собственные стандартные библиотеки, которые зависят от libc и других системных компонентов. Это создает сложный граф зависимостей, где компилятор и библиотеки взаимозависимы, а в случае кросс-компиляции важно разделять фазы сборки для предотвращения порочных циклов.
При кросс-компиляции ситуация осложняется тем, что создается несколько версий компилятора, например build, host и target, где build — это то, на чьей платформе строятся инструменты, host — где они будут запускаться, а target — платформа назначения. В таких условиях традиционная практика «внутренняя» сборка стандартных библиотек новым компилятором приводит к необходимости строить дополнительные компиляторы только ради промежуточных частей и тратить при этом ресурсы впустую. Существует мнение, что пакеты не должны принимать решения, касающиеся бутстраппинга, а новые артефакты, в том числе компиляторы, не должны запускаться внутри одной и той же сборки для построения других компонентов. Этот взгляд, хотя и не нов, получил широкое распространение и практическую реализацию в Nixpkgs. Следуя этому принципу, значительные улучшения произошли с LLVM, где стала возможной независимая сборка компонентов и пересмотр структуры пакетов в соответствии с графом зависимостей.
Такой подход позволяет иметь несколько версий инструментов и библиотек, аккуратно разделяет сборочные фазы, а пакетная система Nix поддерживает строгую ацикличность зависимостей, что ведет к более «чистому» бутстраппингу. Для GCC, одного из ключевых компиляторов сообщества, долгое время сохранялись технические долги, связанные с бутстраппингом. Однако усилия нескольких участников, включая @alexfmpe, @philiptaron, @cloudripper и @RossComputerGuy, привели к созданию обновленного набора пакетов «GCC NG», который направлен на устранение подобных трудностей и модернизацию процесса сборки компилятора. Этот проект открыт для дальнейшего развития, в частности тестирование проводится на менее популярном gfortran, что позволяет минимизировать риски и в то же время совершенствовать координацию между сообществами разработчиков. Важно понимать, что GCC — лишь часть большого экосистемного пазла, куда входят другие компиляторы, такие как GHC для Haskell, rustc для Rust, Go и многие другие.
Они также нуждаются в аналогичных предпринятых мерах для улучшения бутстраппинга и оптимизации кросс-компиляции. В частности, для GHC уже реализуется упрощённая система сборки с независимыми компонентами, что напоминает подходы, опробованные на LLVM и GCC. В сообществе Rust активно развивается функция build-std, призванная упростить сборку стандартных библиотек, что может значительно снизить сложность bootstrap-процесса и снизить объем связанного с этим «ленивого» кода. Такая эволюция приближает ситуацию к идеалу, когда компилятор и компоненты стандартной библиотеки собираются и тестируются как независимые модули, исключающие лишнюю дубликацию и избыточность. В конечном счете, можно ожидать, что через несколько лет станет общепринятой практикой траспорт компилятора отдельно от рантайм-библиотек с акцентом на мульти-таргетность и оптимальное разделение стадий сборки.
Этот подход не зависит от используемого инструментария и, вероятно, станет стандартом не только в Nix, но и в других системах управления сборкой и пакетирования. Опыт развития кросс-компиляции и бутстраппинга компиляторов в Nixpkgs демонстрирует, насколько важно сочетание чистой архитектуры, четких абстракций и постоянного взаимодействия сообщества разработчиков и upstream проектов. Тщательное устранение технических задолженностей и поддержка новых методов сборки обеспечивает стабильность, масштабируемость и удобство для всех, кто работает над поддержкой мультиплатформенных систем и широкого спектра программного обеспечения с открытым исходным кодом.