Каждый, кто когда-либо пробовал собрать программу на C/C++ через кросс-компиляцию знает, насколько болезненным может быть этот процесс. Главными причинами столь печального положения вещей являются недружелюбность систем сборки при конфигурации кросс-компиляции, а также запутанность процесса настройки набора утилит (тулчейна). Одним из основных виновников этих проблем, по моему опыту, является тулчейн GNU — древний мамонт, на котором много десятилетий строится весь мир POSIX. Подобно многим компиляторам былых времён, семейство GCC и binutils никогда не ориентировалось на поддержку множества различных целей сборки в одной установке, и единственным способом хоть как-то добиться желаемого была настройка полной кросс-билд-системы для каждой целевой платформы на каждом хосте. Например, если вы хотите собрать что-то для FreeBSD на машине под Linux с помощью GCC, вам потребуется: Некоторые дистрибутивы Linux, а также некоторые разработчики оборудования могут облегчить этот процесс, предоставляя готовые наборы для компиляции, однако их никогда не хватает, ввиду огромного количества различных комбинаций сборочных/целевых платформ. Зачастую это означает, что вам необходимо самостоятельно полностью собрать тулчейн с нуля, что приводит к масштабным затратам времени и ресурсов, если конечно у вас не самый мощный процессор. Эти досадные ограничения побудили меня обратить внимание на LLVM (и Clang), который изначально создан как полноценный тулчейн для кросс-компиляции, и при этом практически полностью совместим с GNU. Один единственный инстанс LLVM способен собирать и компилировать код для каждой поддерживаемой платформы; помимо него для сборки нужен лишь sysroot. Несмотря на то, что он ещё не может сравниться по удобству с тулчейнами современных языков (такими, как gc и GOARCH/GOOS в Go), это всё же настоящий глоток свежего воздуха по сравнению со сложностями настроек тулчейнов GNU. Вы можете просто установить его из пакетов для вашего дистрибутива (если только он не сильно старый), и сразу избежать всех сложностей с множественными установками GCC. Ещё несколько лет назад весь процесс не был настолько хорошо настроен. Поскольку LLVM ещё не включал в себя весь тулчейн, вам нужно было по-прежнему откуда-то брать binutils, специфичный для вашей целевой платформы. И даже хотя решить эту проблему было гораздо проще, чем собрать весь компилятор целиком (binutils собирается значительно быстрее), этот факт всё же добавлял хлопот. Однако к настоящему моменту llvm-mc (интегрированный ассемблер LLVM) и lld (универсальный линкер) уже стабильны и настолько же гибки, как весь остальной LLVM. Когда весь тулчейн доступен, для компиляции и сборки вашего проекта необходим ещё sysroot, содержащий все необходимые библиотеки и заголовочные файлы. Быстрее всего раздобыть работающую системную папку для нужной ОС можно, скопировав её напрямую из существующей системы (для этого зачастую подходит контейнер Docker). Например, вот так я скопировал работающий sysroot из виртуалки с FreeBSD 13-CURRENT AArch64 с помощью tar и ssh При копировании по сети, а не локально, неплохо также сжать получившийся tarball Когда всё готово, остаётся лишь запустить Clang с правильными аргументами: В приведённом примере я собрал исходник C++ в программу для платформы AArch64 FreeBSD, и всё это с использованием лишь clang и lld, которые я уже установил на своей машине GNU/Linux. Чуть-чуть деталей: К сожалению, macOS больше не поддерживается в LLD, ввиду того, что поддержка формата Mach-O была заброшена несколько лет назад. В связи с этим единственным способом собрать исполняемый файл формата Mach-O является использование линкера ld64 (нативного, или в кросс-системе, если вы сами его соберёте). Хотя утилита ld.bfd из binutils всё ещё его поддерживает. В качестве заключительного аккорда, мы скопируем собраный бинарь на нашу целевую систему (т.е. на виртуалку, откуда мы извлекли чуть раньше sysroot), и убедимся, что всё работает: Всё получилось! Теперь мы можем использовать этот кросс-тулчейн для сборки более крупных программ, и ниже я расскажу, как использовать его для сборки реальных проектов. LLVM предоставляет практически полностью совместимые альтернативы для всех утилит, входящих в binutils (за исключением разве что as), с префиксом llvm- в имени. llvm-mc можно использовать как (очень громоздкий) ассемблер, но он плохо документирован. Подобно gcc, фронтэнд clang также может использоваться как ассемблер, делая as зачастую ненужным. Наиболее критичной из них является LLD, который полностью подменяет системный линковщик для целевой платформы и может заменить одновременно как ld.bfd из GNU, или gold из GNU/Linux или BSD, так и LINK.EXE от Microsoft при сборке под MSVC. Он поддерживает сборку на (практически) каждой платформе, поддерживаемой LLVM, таким образом устраняя необходимость в нескольких специфических линковщиках. Оба компилятора, GCC и Clang поддерживают использование ld.lld вместо системного линковщика (которым также может быть и lld, например, на FreeBSD) с помощью ключа -fuse-ld=lld. На опыте я сталкивался с тем, что драйвер Clang иногда не может найти верный линковщик для некоторых редких платформ, особенно до версии 11.0. По какой-то причине иногда clang открыто игнорирует ключ -fuse-ld=lld и вызывает системный линковщик (в моём случае ld.bfd), который не умеет собирать для AArch64. Быстрым решением для этой проблемы будет создание папки тулчейна, содержащей символические ссылки, которые переименовывают утилиты LLVM в стандартные программы из binutils: Также можно использовать ключ -B, который заставляет Clang (или GCC) искать нужные утилиты в этой папке, так что подобная проблема даже не возникает: Я заметил, что некоторые системы сборки (и под "некоторыми" я имею в виду некоторые кривые Makefile, и иногда Autotools) склонны ломаться, если значения переменных окружения $CC, $CXX или $LD содержат пробелы или несколько параметров. Такое может периодически случаться, если нам необходимо вызвать clang с несколькими аргументами (не говоря уже о тех преступниках, которые хардкодят вызовы gcc в свои билд-скрипты. Впрочем, это уже совсем другая история.) Понимая, насколько громоздко и сложно каждый раз не забыть выписать все параметры корректно в каждом из случаев, я обычно пишу короткие обёртки для clang и clang++ с целью упростить сборку для конкретной целевой платформы: Если этот скрипт доступен внутри $PATH, его можно повсеместно использовать как отдельную команду: Autotools, Cmake и Meson, возможно, самые популярные системы сборки для open-source проектов на C и C++ (извини, SCons). Все три поддерживают кросс-компиляцию прямо "из коробки", хотя с некоторыми особенностями.  В течение многих лет Autotools славится своей ужасной неуклюжестью и хрупкостью. И хотя эту репутацию он заработал вполне обосновано, он по-прежнему широко используется для большинства крупных проектов GNU. Будучи в строю уже не одно десятилетие, многие его проблемы, если что-то вдруг пошло криво, можно разрешить поиском в сети (хотя если вы пишете свой .ac файл, это не всегда так). По сравнению с более современными системами, для кросс-компиляции ему не нужен специфичный файл тулчейна или какая-то особая конфигурация; всё настраивается исключительно параметрами командной строки. Скрипт ./configure (созданный утилитой autoconf, или включённый в тарболл с исходниками) обычно поддерживает флаг --host, позволяя пользователю задать триплет целевой системы, на которой предполагается запускать собранные программы и прочие артефакты. Этот флаг активирует кросс-компиляцию, после чего множество утилит auto-что-то-там начинают выяснять правильный компилятор для целевой платформы, который, как правило, зовётся по именам some-triple-gcc или some-triple-g++. Попробуем сконфигурировать сборку binutils версии 2.35.1 для платформы aarch-pc-freebsd, используя написанную выше обёртку для вызова Clang: Вызов скрипта ./configure выше означает, что я хочу от autotools следующего: Я также задал сборку линковщика Gold, который написан на С++ и может использоваться как неплохой тест на то, как наш импровизированный тулчейн способен компилировать программы на C++. Если стадия конфигурации не сломается по какой-то причине (вроде не должно), мы можем далее запустить GNU Make и собрать binutils: Здесь мы должны получить исполнимые файлы и библиотеки внутри целевой папки, созданной с помощью make install. Быстрая проверка с помощью file подтверждает, что все программы корректно собраны для платформы aarch64-pc-freebsd: Проще всего настроить CMake для сборки на конкретную платформу можно с помощью файла тулчейна. Он обычно состоит из настроек, которые указывают CMake, как он должен работать с данным тулчейном, определяя такие параметры, как целевая операционная система, архитектура CPU, имя компилятора C++ и т.д. Для сборки под триплет aarch64-pc-freebsd тулчейн файл может быть таким: В этом файле я назначил вышеупомянутую обёртку как кросс-компилятор кода C и C++ для целевой платформы. Также можно использовать напрямую Clang с подходящими параметрами, но это не настолько прямолинейно и потенциально более подвержено ошибкам. В любом случае, исключительно важно задать правильные значения для переменных CMAKE_SYSROOT и CMAKE_FIND_ROOT_PATH_MODE_*, иначе CMake может ошибочно взять пакеты от текущей платформы, с закономерно неверным результатом. Далее остаётся лишь указать путь к этому файлу, при конфигурировании сборки, через переменную CMAKE_TOOLCHAIN_FILE, либо через параметр --toolchain. Для примера я соберу пакет {fmt} (это удивительная библиотека C++, которую вам непременно стоит попробовать) для платформы aarch64-pc-freebsd: По сравнению с Autotools, командная строка, переданная в cmake очень проста и не требует дополнительных объяснений. После конфигурирования остаётся лишь скомпилировать проект, а затем запустить ninja или make, чтобы установить куда-нибудь получившиеся артефакты. Подобно CMake, Meson полагается на файлы тулчейнов (так называемые "cross files"), которые определяют, какие программы должны быть использованы для сборки под текущую целевую платформу. Благодаря тому, что они написаны на TOML-подобном языке, они очень просты и понятны: Этот кросс-файл далее нужно указать при вызове meson setup с помощью ключа --cross-file. Остальные настройки абсолютно такие же, как для любой другой сборки с помощью Meson. Подобным образом можно настроить нативный тулчейн на текущей машине, используя нативный файл с ключом --native-file.  И на этом, пожалуй, всё: подобно CMake весь процесс относительно безболезненен и безошибочен. Для полноты расскажу, как собрать dav1d, декодер VideoLAN AV1, для платформы aarch64-pc-freebsd: Статическая сборка программ на C и C++ иногда может спасти вас от множества проблем совместимости библиотек, особенно когда вы не можете контролировать, что именно будет установлено на той целевой платформе, под которую вы планируете сборку. Однако сборка статических бинарников на GNU/Linux довольно непроста, поскольку Glibc всячески препятствует попыткам слинковаться с ним статически. Система разрешения имён (NSS), встроенная в glibc, является одной из главных причин интенсивного использования функций dlopen()/dlsym(). Это связано с использованием сторонних плагинов, вроде mDNS, используемых для разрешения имён. Musl — это альтернанивный вариант стандартной библиотеки для Linux, которая гораздо терпимее к статической линковке, и нынче включена в большинство крупных дистрибутивов. Этих пакетов зачастую вполне достаточно для статической сборки вашего кода, по крайней мере пока вы планируете оставаться в рамках чистого C. Однако если вы планируете собирать C++, либо если вам необходимы дополнительные компоненты, ситуация становится гораздо сложнее и интереснее. Любая библиотека, входящая в GNU/Linux (такая, как libstdc++, libz, libffi и другие) обычно собрана только с Glibc, что означает, что любая библиотека, которую вы хотите использовать, должна быть пересобрана для цели Musl. Это так и для libstdc++, что неизбежно означает либо перекомпиляцию GCC, либо сборку копии libc++ от LLVM. К счастью, существует несколько дистрибутивов, собранных для платформы "Musl-plus-Linux", наиболее известный из которых Alpine Linux. А потому возможно использовать освоенный нами подход: извлекаем sysroot для платформы x86_64-pc-linux-musl, в котором находится полный комплект библиотек и пакетов собранных для Musl, а затем используем его вместе с Clang для сборки 100% статических программ. Хорошей отправной точкой является тарболл с minirootfs от Alpine, который предназначен специально для контейнеров, а потому очень маленький: Теперь мы можем сделать chroot внутрь образа ~/alpine_tree и настроить его, установив все нужные нам пакеты. Обычно я предпочитаю использовать systemd-nspawn вместо chroot, поскольку это значительно проще и меньше склоняет к ошибкам. system-nspawn может также служить лёгкой альтернативой виртуальным машинам. С ключом --boot он умеет запускать процесс init внутри контейнера. Вот в этом весьма полезном gist можно научиться создавать загрузочный контейнер для дистрибутивов, основанных на OpenRC (например, Alpine) Здесь мы можем (по желанию) отредактировать /etc/apk/repositories, чтобы переключиться на бранч edge с самыми свежими пакетами, а затем установить нужные пакеты Alpine, содержащие любые статические библиотеки для сборки кода, который мы хотим собрать: В данном примере я установил g++ и libc-dev, чтобы получить статические копии libstdc++, статическую libc.a (Musl), и их соответствующие заголовочные файлы. Я также установил zlib-dev и zlib-static, чтобы получить заголовки zlib и библиотеку zlib.a. Как общее правило — Alpine обычно предоставляет статические версии в пакетах *-static, и заголовочные файлы в пакетах somepackage-dev. К сожалению, по неизвестным мне причинам Alpine не предоставляет статические версии некоторых библиотек (например, libfmt). Поэтому внедрять копию зависимостей в проект является вполне обычной практикой для C++, и потому это довольно несложно. Также не забывайте каждый раз запускать apk upgrade внутри sysroot, чтобы поддерживать локальную копию Alpine в актуальном состоянии. Когда всё готово, остаётся лишь вызвать clang++с верным --target и --sysroot: Дополнительный ключ --gcc-toolchain в данном случае опционален. Он поможет, если Clang не найдёт внутри sysroot GCC и различные файлы crt*.o. Дополнительный ключ -L для /lib необходим, поскольку в Alpine библиотеки лежат как в /usr/lib, так и в /lib, а последний не рассматривается по умолчанию в clang, который ожидает, что все библиотеки находятся только в $SYSROOT/usr/lib. В пакетах Musl обычно есть готовые обёртки musl-gcc и musl-clang, которые вызывают системные компиляторы для сборки и линковки с альтернативным libc. Для удобства я по-быстрому набросал вот такой скрипт на Perl: Это более "продвинутый" вариант обёртки, чем тот, что я привёл выше для FreeBSD AArch64. Например, она подразумевает компиляцию кода на С++, если вызвана по имени clang++, или всегда добавляет ключ -static, если вызвана по символической ссылке, содержащей в имени слово static: Таким образом, теперь возможно заставить Clang линковать только с ключом -static, если установить $CC в musl-clang-static, что может быть полезно для систем сборки, которые не очень хорошо работают со статической линковкой. По опыту могу сказать, что больше всего проблем с этим доставляют Autotools (иногда), а также кривые Makefiles. Кросс-компиляция кода на C и C++ является, и скорее всего навсегда останется непростой задачей, однако она стала значительно проще, покуда LLVM достиг стабильности и стал широко распространён. Параметр -target в Clang избавил меня от траты бесчисленного множества человеко-часов на бесконечную сборку и пересборку GCC и Binutils. К сожалению, не всё то золото, что блестит, и так бывает довольно часто. До сих пор встречаются костыли, которые собираются только с помощью GCC из-за грязных GNUизмов (да-да, Glibc, это про тебя). Кросс-компиляция под Windows/MSVC также невозможна ввиду сильной запутанности всего тулчейна Visual Studio. Кроме того, хотя сборка под целевую платформу путём указания в Clang верного триплета стала гораздо проще, чем когда-то была, она всё равно нервно курит, поглядывая на то, насколько тривиальна кросс-компиляция в случае с Rust или Go. Из новых языков отдельного упоминания заслуживает Zig, поскольку его целью является также облегчение сборки кода на C и C++ под другие платформы. Команды zig cc и zig c++ потенциально могут стать удивительным швейцарским ножом для кросс-компиляции, благодаря тому, что Zig содержит в себе clang и множество частей из разных проектов, такие как Glibc, Musl, libc++ и MinGW. Благодаря этому любая нужная библиотека собирается, если надо, прямо на лету: Пока, я думаю, это выглядит не так совершенно, однако уже работает прямо как магия. Осмелюсь утверждать, что это настоящая киллер-фича Zig, и она достойна внимания даже тех, кому совсем не интересен сам язык.