Сегодня широко используют cmake add_subdirectory и сборку через Gradle для кросс-компиляции сишных библиотек под андроид. Мы же рассмотрим более продвинутый и универсальный способ, который работает со множеством систем сборки и позволяет любым проектам найти свои зависимости без написания отдельных костылей под каждый проект.

Кратко что такое кросс-компиляция

Сборка исполняемых файлов и библиотек под системы, отличающиеся от той, где вы собираете.

Общий план

В интернете вам могло попадаться CC=gcc или что-то такое. Так вот, первая вещь которую делают при любых сложных компиляциях - определение environment переменных. Для начала мы их определим для ndk тулчейна, предварительно конечно же его установив. Тулчейном называют набор утилит и предсобранных библиотек для сборки под систему, например архив с NDK, где лежат компиляторы, обертки, библиотеки - вполне можно назвать тулчейном.

Вторым шагом мы определим несколько дополнительных переменных, сделав разные независимые друг от друга секции, для autoconf, cmake, meson, rust.

Завершающим шагом мы воспользуемся нашими трудами чтобы что-нибудь собрать.

Что нужно?

От вас требуется установить Android SDK и через sdkmanager или его графическую альтернативу установить Android NDK, а также убедиться что $ANDROID_HOME указывает на установленный Android SDK. Вы можете сделать это любым удобным для вас способом, например я писал об автоматизации установки SDK и запуска sdkmanager через mise в своей предыдущей статье https://habr.com/ru/articles/1058494/ , там можно добавить к командам sdkmanager установку ndk. Честно сказать - неудобно это все, поэтому и нормальной документации дать не могу, очень хотелось бы чтобы был какой-нибудь curl something | sh который все настроит, поднимет и экспортирует ANDROID_HOME, но я такого не нашел.

Чтобы дать небольшую точку опоры, покажу какие директории у меня лежат в $ANDROID_HOME:

c-android-utils HEAD ❯ ls $ANDROID_HOME
cmdline-tools
licenses
ndk
platform-tools

Шаг 0. Определяем пути к NDK

NDK - отдельная задача в плане определения путей. Вот как я решаю ее:

_ndk_ver="$(for _ndk_cand in "$ANDROID_HOME/ndk"/*; do
  [ -d "$_ndk_cand" ] || continue
  printf '%s\n' "${_ndk_cand##*/}"
done | sort -V | tail -1)"
[ -n "$_ndk_ver" ] || { echo "в каталоге $ANDROID_HOME/ndk нет ни одной версии NDK" >&2; return 1 2>/dev/null || exit 1; }
export NDK="$ANDROID_HOME/ndk/$_ndk_ver"
unset _ndk_ver _ndk_cand

Глоб "$ANDROID_HOME/ndk"/* раскрывает сам шелл и отдаёт только имена каталогов (файлы отсеиваем проверкой -d), sort отсортирует версии по-человечески (-V правильно сравнивает и 27.0.0, и 28.2.13676358), tail откусит самую свежую — таким образом переменная NDK указывает на директорию с самой последней версией NDK. Специально не использую тут ls: у многих ls подменён алиасом или функцией вроде eza с иконками, цветами и выводом в несколько колонок — такой вывод парсить нельзя, он развалится. А без ls вообще никакой алиас не страшен: ls тут просто не вызывается. Обратите внимание, что уже здесь мы использовали переменную $ANDROID_HOME! Без нее уже на этом моменте все пойдет не по плану. Если каталог ndk пуст, получим понятную ошибку вместо молчаливого битого пути.

от NDK уже доберемся к его тулчейну:

export TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64"

Обратите внимание, что если у вас не linux x86_64 как у меня установлен на компьютере, то последняя директория в этом пути у вас будет отличаться, выставляйте внимательно.

Чтобы не указывать полные пути, добавим бинарники для нашей системы из тулчейна в PATH:

export PATH="$TOOLCHAIN/bin:$PATH"

Если вы сделаете ls $TOOLCHAIN/bin вы найдете много скриптов-компиляторов для сборки под разные цели. Например aarch64-linux-android21-clang - это С-компилятор clang который собирает под платформу android с api level 21 (и выше, платформы обратно совместимые до определенной степени) под архитектуру процессора aarch64 . Другие утилиты, вроде llvm-nm, работают для любой архитектуры и api level, поэтому для них нет множества оберток с разными префиксами.

Шаг 1. Определяем базовые переменные

Переменных будет довольно много, и если бы я просто привел список, какую переменную как задавать - читать это было бы уныло, понимания бы это особо не прибавило, кроме того подобные сухие списки вы можете найти в интернете, например вот тут https://docs.conan.io/en/1.19/systems_cross_building/cross_building.html . Я же пойду максимально подробно и буду объяснять каждую переменную настолько подробно, насколько смогу.

Итак, поехали.

Компиляторы

Чтобы компилировать, нужны компиляторы. Обычно компилятор для вас может вызывать различные дополнительные утилиты вроде линковщика и вам не нужно ничего кроме компилятора, однако когда вы кросс-компилируете, кто знает какой линковщик вам нужен?

Так что считаем что в тулчейне компилятор играет роль сборки файла .c или .cpp в .o и ничего более:

export CC="aarch64-linux-android21-clang"
export CXX="aarch64-linux-android21-clang++"

Архиватор

Архиватор упаковывает ваш .o в статическую библиотеку .a (никакой линковки и проверки связей с другими библиотеками тут не происходит, это только упаковка)

export AR="llvm-ar"

Индексатор архивов

Задача индексатора - взять наш архив .a, посмотреть какие в нем определены символы, создать индекс для быстрого поиска этих символов и дозаписать в этот .a . Благодаря нему линковщик быстрее находит символы в наших .a

export RANLIB="llvm-ranlib"

Линковщик

Линковщик ваш .o или .a склеивает в бинарник или .so библиотеку. Вот он уже ищет по линкуемым объектам и разбирается, какие символы есть, какие нужны, каких не хватает. Помимо склейки объектов в один большой блоб, линковщик может разместить символы в нужном порядке и с нужной для целевой системы структурой, чтобы можно было результат прям запустить или загрузить через ffi.

export LD="ld.lld"

Ассемблер

Иногда приходится собирать файлы на ассемблере причем тот ассемблер который у LLVM - llvm-as - это абсолютно не то что нам нужно, эта утилита читает llvm-специфичный ассемблер в файлах .ll и переводит его в бинарный формат .bc, который потом надо прогонять через команду opt для оптимизации, и потом через команду llc -march=aarch64 мы преобразуем этот суперуниверсальный LLVM ассемблер в нормальный .S ассемблер для нормальной компиляции. Так вот, ожидаемый системами сборки ассемблер - это такая программа, которая возьмет ассемблер в файлах .S или .s и соберет его в .o . И в LLVM этим занимается знаете кто? clang. Тот же самый, который компилятор си, да.

export AS="$CC"

Strip

strip - это утилита для вырезания отладочных символов из .so, .a или бинарника. Не смог придумать, как перевести, не припомню чтобы был какой-то русскоязычный термин для данной утилиты

export STRIP="llvm-strip"

ObjCopy

objcopy - это более продвинутый strip, помимо удаления отладочных секций он может добавлять/удалять иные секции и менять формат целевого .so/.a/.exe

export OBJCOPY="llvm-objcopy"

ReadELF

Диагностическая утилита. Читалка бинарных заголовков и .so-зависимостей.

export READELF="llvm-readelf"

NM

Диагностическая утилита, дает список символов defined/undefined в .o/.a/.so. Быстрая инвентаризация при undefined reference.

export NM="llvm-nm"

ObjDump

Дизассемблер, утилита для разбора бинарника, позволяет разобраться что там в ассемблере этого бинарника, чего навертел оптимизатор си, что мы имеем в итоге. Дизассемблеры используют для детального анализа, иногда даже для реверс-инженеринга программ, для которых нет исходного кода.

export OBJDUMP="llvm-objdump"

SYSROOT

Наконец мы закончили с базовыми утилитами и переходим уже к директориям! SYSROOT указывает на системные заголовки и предсобранные библиотеки вроде crt или libc, NDK поставляет вот такой SYSROOT:

export SYSROOT="$TOOLCHAIN/sysroot"

PREFIX

Это очень важная директория, туда будут устанавливаться собираемые программы, там будут искаться уже установленные на этапе определения зависимостей. Так что создайте себе где-нибудь префикс, в данном случае пусть будет называться prefix-aarch64-android21:

mkdir -p prefix-aarch64-android21
export PREFIX="$PWD/prefix-aarch64-android21" # полный путь на всякий случай

Если бы вы собирали под windows, вам бы еще понадобилась переменная WINDRES, это компилятор windows-ресурсов .rc, обычно он поставляется вместе с соответствующими тулчейнами или используется llvm-rc

CFLAGS, CXXFLAGS

Приступаем к аргументам для утилит. CFLAGS - это аргументы которые будут переданы сишному компилятору. CXXFLAGS - аргументы для компилятора С++. Здесь мы говорим компилятору чтоб искал в префиксе и в sysroot заголовочные файлы, чтобы он определил дефайнами ANDROID, а целевую версию апи определяет используемая нами обертка.

export CFLAGS="-O2 -fPIC -I$PREFIX/include -DANDROID -isystem $SYSROOT/usr/include"
export CXXFLAGS="-O2 -fPIC -I$PREFIX/include -DANDROID -isystem $SYSROOT/usr/include"

Про CPPFLAGS

CPPFLAGS - это флаги для препроцессора, которые обрабатывает #include, #define, условную компиляцию и прочее подобное, обычно именно ему нужны флаги -I -D и тому подобные, а во флаги компиляторов передают -O2 -fPIC и прочее такое, но поскольку у меня и так все работает, не будем его задавать.

LDFLAGS

Аргументы для линковщика. “-Wl,” передает то что после запятой в линкер, такой формат видимо нужен на случай если вы захотите линкером назначить сам clang а не lld, но и для нашего ld.lld он работает.

-L$PREFIX/lib - ищи те библиотеки, которые тебе говорят линковать, в $PREFIX/lib

-rpath-link,$PREFIX/lib - если у найденных библиотек есть зависимости, то эти зависимости (непрямые) тоже ищи в $PREFIX/lib.

--undefined-version - сегодня у многих утилит есть неприятная штука, делать не libname.so, а libname.so.2.4 , с указанием каких-нибудь версий которые потом попробуй отыщи. Данный флаг линковщика ослабляет требования по искомым версиям зависимостей для линковки.

export LDFLAGS="-L$PREFIX/lib -Wl,-rpath-link,$PREFIX/lib -Wl,--undefined-version"

Флаги для pkg-config

pkg-config отвечает за поиск зависимостей. Если кратко - при сборке многие библиотеки для установки генерируют файлик .pc, в котором записаны флаги к компиляторам и линковщикам для того чтоб слинковаться с той или иной библиотекой. Еще в .pc файлах хранятся метаданные, информация о зависимостях, флаги для которых потом тоже можно запросить через pkg-config. Много всего. pkg-config - это стандартная линуксовая штука для поиска зависимостей, именно благодаря ней autoconf, meson проекты, иногда cmake, некоторые gnu make штуки ищут установленные зависимости и флаги для например линковки с ними. CMake иногда использует свои, иные механизмы, что впрочем часто заканчивается плачевно. Итак:

# pkg-config lookup path: ищем .pc сначала в префиксе, потом в sysroot 
export PKG_CONFIG_LIBDIR="$PREFIX/lib/pkgconfig:$PREFIX/share/pkgconfig:$SYSROOT/usr/lib/pkgconfig:$SYSROOT/usr/share/pkgconfig"

# игнорировать наши системные .pc файлы чтобы не подтягивать в андроид библиотеки из нашего линукса, которые для него не подходят
export PKG_CONFIG_PATH=""

Шаг 2. Доп переменные для систем сборки и для удобства

В основном это просто удобные переменные чтоб не забывать, что передавать в систему сборки.

autoconf

export AUTOCONF_CONFIGURE_FLAGS="--host=aarch64-linux-android --prefix=$PREFIX"

Применяем так:

./configure $AUTOCONF_CONFIGURE_FLAGS

в конец команды конечно же можно дописать еще флаги, а после конфигурации запускать как обычно make; make install

cmake

Вообще для CMake у NDK есть свой тулчейн-файл, но для глубокого понимания я решил что надо сделать свой aarch64-linux-android21-toolchain.cmake:

# Если тут поставить Android, то cmake начнет вытворять свою магию интеграции с NDK,
# а мы ее тут руками делаем, так что пусть будет Linux
set(CMAKE_SYSTEM_NAME Linux)

# архитектура процессора
set(CMAKE_SYSTEM_PROCESSOR "aarch64")

# задаем компилятор си из системных переменных если он не был задан
if(NOT CMAKE_C_COMPILER)
  set(CMAKE_C_COMPILER "$ENV{CC}" CACHE FILEPATH "C compiler" FORCE)
endif()
# Аналогично для С++
if(NOT CMAKE_CXX_COMPILER)
  set(CMAKE_CXX_COMPILER "$ENV{CXX}" CACHE FILEPATH "C++ compiler" FORCE)
endif()

set(CMAKE_AR "$ENV{AR}" CACHE FILEPATH "archiver" FORCE)             # наш $AR
set(CMAKE_RANLIB "$ENV{RANLIB}" CACHE FILEPATH "ranlib" FORCE)       # наш $RANLIB
set(CMAKE_STRIP "$ENV{STRIP}" CACHE FILEPATH "strip" FORCE)          # наш $STRIP
set(CMAKE_READELF "$ENV{READELF}" CACHE FILEPATH "readelf" FORCE)    # наш $READELF
set(CMAKE_OBJDUMP "$ENV{OBJDUMP}" CACHE FILEPATH "objdump" FORCE)    # наш $OBJDUMP
set(CMAKE_OBJCOPY "$ENV{OBJCOPY}" CACHE FILEPATH "objcopy" FORCE)    # наш $OBJCOPY
set(CMAKE_NM "$ENV{NM}" CACHE FILEPATH "nm" FORCE)                   # наш $NM
set(CMAKE_ASM_COMPILER "$ENV{AS}" CACHE FILEPATH "assembler" FORCE)  # наш $AS
set(CMAKE_LINKER "$ENV{LD}" CACHE FILEPATH "linker" FORCE)           # наш $LD
set(CMAKE_SYSROOT "$ENV{SYSROOT}")                                   # наш $SYSROOT
set(CMAKE_INSTALL_PREFIX "$ENV{PREFIX}" CACHE PATH "install prefix") # наш $PREFIX

# для find_library/find_path ищем сначала PREFIX, потом SYSROOT
set(CMAKE_FIND_ROOT_PATH "$ENV{PREFIX};$ENV{SYSROOT}")

# Не ищем утилиты для запуска в CMAKE_FIND_ROOT_PATH, то есть в SYSROOT или PREFIX
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# Библиотеки ищем только в SYSROOT или PREFIX
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# Заголовочные файлы ищем только в SYSROOT или PREFIX
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# CMake пакеты (я так понимаю его собственная система зависимостей, не для pkg-config а для find_package) 
# ищем тоже только в SYSROOT или PREFIX
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

И задаем наши переменные для cmake:

# путь к файлу тулчейна (полный)
export CMAKE_TOOLCHAIN_FILE="$PWD/aarch64-linux-android21-toolchain.cmake" # полный путь

# префикс для cmake find_package
export CMAKE_PREFIX_PATH="$PREFIX"

# для удобства
export CMAKE_FLAGS="-DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE -DCMAKE_INSTALL_PREFIX=$PREFIX -DCMAKE_PREFIX_PATH=$PREFIX"

Соответственно пользуемся так:

# в директории с исходниками
mkdir -p build; cd build
cmake $CMAKE_FLAGS ..
make
make install

meson

Для кросс-компиляции meson настаивает на том чтоб был cross file, причем внутри него env-переменные - не работают, так что тут мы воспользуемся тем что мы добавили всё нужное в PATH и создадим для meson crossfile-aarch64-android21.ini

[binaries]
# компилятор си
c = 'aarch64-linux-android21-clang'
# компилятор с++
cpp = 'aarch64-linux-android21-clang++'
# ar
ar = 'llvm-ar'
# strip
strip = 'llvm-strip'
# линковщик
ld = 'ld.lld'
# наш хостовый pkg-config с правильными переменными
# вполне работает
pkgconfig = 'pkg-config'

[host_machine]
# целевая os как meson ее называет; google: meson host_machine system android.
system = 'android'
# архитектура процессора
cpu_family = 'aarch64'
# не всегда, но в данном конкретном случае совпадает
cpu = 'aarch64'
# Порядок байтов, он же endianness
endian = 'little'

[properties]
# все остальное мы определили нашими флагами и больше ничего не надо

Переменные для запуска:

export MESON_CROSS_FILE="$PWD/crossfile-aarch64-android21.ini" # полный путь
export MESON_FLAGS="--prefix=$PREFIX --cross-file $MESON_CROSS_FILE"

Пользуемся:

# в директории с исходниками:
meson setup builddir $MESON_FLAGS
cd builddir
ninja
ninja install

rust

Rust позволяет кросс-компилировать в том числе сишные собираемые через cargo подзависимости использующие крейт cc.

# цель сборки для cargo
export CARGO_BUILD_TARGET="aarch64-linux-android"
# linker и другие утилиты, выбираемые когда у нас именно
# такой таргет
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" 
export CARGO_TARGET_AARCH64_LINUX_ANDROID_AR="llvm-ar"

# Это уже для крейта cc
export CC_aarch64_linux_android="$CC"
export CFLAGS_aarch64_linux_android="$CFLAGS"
export CXX_aarch64_linux_android="$CXX"
export CXXFLAGS_aarch64_linux_android="$CXXFLAGS"

# для крейта pkg-config
export PKG_CONFIG_ALLOW_CROSS=1

Устанавливаются cargo проекты в PREFIX только руками:

# в директории с проектом
cargo build --release --target "$CARGO_BUILD_TARGET"
cp "target/$CARGO_BUILD_TARGET/release/libfoo.a" "$PREFIX/lib/" # .so для cdylib
mkdir -p "$PREFIX/include/foo"
cp path/to/headers/*.h "$PREFIX/include/foo/" # заголовки — вне модели cargo, только руками
pkg-config --modversion foo 2>/dev/null || echo "no .pc — напиши сам, cargo его не генерит"

Шаг 3. Кросскомпилируем

Соберем большой env.sh (ls мой модный дистрибутив заменил на eza и навертел флагов, поэтому путь к NDK определяю без ls):

#!/bin/sh
# Конфигурация кросс-компиляции из статьи.
# Использование: . ./env.sh
# После сорсинга можно приступать к шагу 3: собираем autoconf/cmake/meson/cargo.
# Генерирует в $PWD: aarch64-linux-android21-toolchain.cmake, crossfile-aarch64-android21.ini

# --- Шаг 0. Пути к NDK ---
: "${ANDROID_HOME:?установите ANDROID_HOME (см. статью, раздел «Что нужно?»)}"
# Без ls: его вывод ломают алиасы/функции вроде eza с иконками и сеткой.
# Глоб раскрывает сам шелл, ls вообще не вызывается, так что никакой алиас не страшен.
_ndk_ver="$(for _ndk_cand in "$ANDROID_HOME/ndk"/*; do
  [ -d "$_ndk_cand" ] || continue
  printf '%s\n' "${_ndk_cand##*/}"
done | sort -V | tail -1)"
if [ -z "$_ndk_ver" ]; then
  echo "env.sh: в каталоге $ANDROID_HOME/ndk нет ни одной версии NDK" >&2
  unset _ndk_ver _ndk_cand
  return 1 2>/dev/null || exit 1
fi
export NDK="$ANDROID_HOME/ndk/$_ndk_ver"
unset _ndk_ver _ndk_cand
export TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64"
export PATH="$TOOLCHAIN/bin:$PATH"

# --- Шаг 1. Базовые переменные ---
export CC="aarch64-linux-android21-clang"
export CXX="aarch64-linux-android21-clang++"
export AR="llvm-ar"
export RANLIB="llvm-ranlib"
export LD="ld.lld"
export AS="$CC"
export STRIP="llvm-strip"
export OBJCOPY="llvm-objcopy"
export READELF="llvm-readelf"
export NM="llvm-nm"
export OBJDUMP="llvm-objdump"

export SYSROOT="$TOOLCHAIN/sysroot"

mkdir -p "$PWD/prefix-aarch64-android21"
export PREFIX="$PWD/prefix-aarch64-android21" # полный путь на всякий случай

export CFLAGS="-O2 -fPIC -I$PREFIX/include -DANDROID -isystem $SYSROOT/usr/include"
export CXXFLAGS="-O2 -fPIC -I$PREFIX/include -DANDROID -isystem $SYSROOT/usr/include"
export LDFLAGS="-L$PREFIX/lib -Wl,-rpath-link,$PREFIX/lib -Wl,--undefined-version"

# pkg-config lookup path: ищем .pc сначала в префиксе, потом в sysroot
export PKG_CONFIG_LIBDIR="$PREFIX/lib/pkgconfig:$PREFIX/share/pkgconfig:$SYSROOT/usr/lib/pkgconfig:$SYSROOT/usr/share/pkgconfig"
# игнорировать наши системные .pc файлы чтобы не подтягивать в андроид библиотеки из нашего линукса
export PKG_CONFIG_PATH=""

# --- Шаг 2. autoconf ---
export AUTOCONF_CONFIGURE_FLAGS="--host=aarch64-linux-android --prefix=$PREFIX"

# --- Шаг 2. cmake ---
export CMAKE_TOOLCHAIN_FILE="$PWD/aarch64-linux-android21-toolchain.cmake" # полный путь
export CMAKE_PREFIX_PATH="$PREFIX"
export CMAKE_FLAGS="-DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE -DCMAKE_INSTALL_PREFIX=$PREFIX -DCMAKE_PREFIX_PATH=$PREFIX"

cat > "$CMAKE_TOOLCHAIN_FILE" <<'EOF'
# Если тут поставить Android, то cmake начнет вытворять свою магию интеграции с NDK,
# а мы ее тут руками делаем, так что пусть будет Linux
set(CMAKE_SYSTEM_NAME Linux)

# архитектура процессора
set(CMAKE_SYSTEM_PROCESSOR "aarch64")

# задаем компилятор си из системных переменных если он не был задан
if(NOT CMAKE_C_COMPILER)
  set(CMAKE_C_COMPILER "$ENV{CC}" CACHE FILEPATH "C compiler" FORCE)
endif()
# Аналогично для С++
if(NOT CMAKE_CXX_COMPILER)
  set(CMAKE_CXX_COMPILER "$ENV{CXX}" CACHE FILEPATH "C++ compiler" FORCE)
endif()

set(CMAKE_AR "$ENV{AR}" CACHE FILEPATH "archiver" FORCE)             # наш $AR
set(CMAKE_RANLIB "$ENV{RANLIB}" CACHE FILEPATH "ranlib" FORCE)       # наш $RANLIB
set(CMAKE_STRIP "$ENV{STRIP}" CACHE FILEPATH "strip" FORCE)          # наш $STRIP
set(CMAKE_READELF "$ENV{READELF}" CACHE FILEPATH "readelf" FORCE)    # наш $READELF
set(CMAKE_OBJDUMP "$ENV{OBJDUMP}" CACHE FILEPATH "objdump" FORCE)    # наш $OBJDUMP
set(CMAKE_OBJCOPY "$ENV{OBJCOPY}" CACHE FILEPATH "objcopy" FORCE)    # наш $OBJCOPY
set(CMAKE_NM "$ENV{NM}" CACHE FILEPATH "nm" FORCE)                   # наш $NM
set(CMAKE_ASM_COMPILER "$ENV{AS}" CACHE FILEPATH "assembler" FORCE)  # наш $AS
set(CMAKE_LINKER "$ENV{LD}" CACHE FILEPATH "linker" FORCE)           # наш $LD
set(CMAKE_SYSROOT "$ENV{SYSROOT}")                                   # наш $SYSROOT
set(CMAKE_INSTALL_PREFIX "$ENV{PREFIX}" CACHE PATH "install prefix") # наш $PREFIX

# для find_library/find_path ищем сначала PREFIX, потом SYSROOT
set(CMAKE_FIND_ROOT_PATH "$ENV{PREFIX};$ENV{SYSROOT}")

# Не ищем утилиты для запуска в CMAKE_FIND_ROOT_PATH, то есть в SYSROOT или PREFIX
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# Библиотеки ищем только в SYSROOT или PREFIX
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# Заголовочные файлы ищем только в SYSROOT или PREFIX
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# CMake пакеты (я так понимаю его собственная система зависимостей, не для pkg-config а для find_package)
# ищем тоже только в SYSROOT или PREFIX
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
EOF

# --- Шаг 2. meson ---
export MESON_CROSS_FILE="$PWD/crossfile-aarch64-android21.ini" # полный путь
export MESON_FLAGS="--prefix=$PREFIX --cross-file $MESON_CROSS_FILE"

cat > "$MESON_CROSS_FILE" <<'EOF'
[binaries]
# компилятор си
c = 'aarch64-linux-android21-clang'
# компилятор с++
cpp = 'aarch64-linux-android21-clang++'
# ar
ar = 'llvm-ar'
# strip
strip = 'llvm-strip'
# линковщик
ld = 'ld.lld'
# наш хостовый pkg-config с правильными переменными
# вполне работает
pkgconfig = 'pkg-config'

[host_machine]
# целевая os как meson ее называет; google: meson host_machine system android.
system = 'android'
# архитектура процессора
cpu_family = 'aarch64'
# не всегда, но в данном конкретном случае совпадает
cpu = 'aarch64'
# Порядок байтов, он же endianness
endian = 'little'

[properties]
# все остальное мы определили нашими флагами и больше ничего не надо
EOF

# --- Шаг 2. rust ---
export CARGO_BUILD_TARGET="aarch64-linux-android"
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang"
export CARGO_TARGET_AARCH64_LINUX_ANDROID_AR="llvm-ar"

# Это уже для крейта cc
export CC_aarch64_linux_android="$CC"
export CFLAGS_aarch64_linux_android="$CFLAGS"
export CXX_aarch64_linux_android="$CXX"
export CXXFLAGS_aarch64_linux_android="$CXXFLAGS"

# для крейта pkg-config
export PKG_CONFIG_ALLOW_CROSS=1

echo "env aarch64-linux-android21 ready: PREFIX=$PREFIX"

Убедитесь что ANDROID_HOME задан

Зайдите в директорию с этим env.sh и запустите именно так, чтобы PATH экспортировался в вашем shell.

. ./env.sh

Если бы вы просто запустили ./env.sh, то все экспортируемые переменные перестали бы действовать как только скрипт бы вышел.

Скрипт к слову также сгенерирует тулчейн файл и кроссфайл для cmake и meson.

Идем дальше, обратите внимание что я опростоволосился с CPPFLAGS, некоторые проекты его все же используют, поэтому я задаю его уже здесь, при сборке:

#!/bin/sh
# Шаг 3 из статьи «Кросс-компиляция под андроид и не только» — наконец собираем.
# env.sh разложил флаги по полочкам, теперь проверяем, что они работают:
# тянем цепочку zlib -> libpng -> freetype -> pngprobe кроссом из NDK.
# Первые три переезжают в $PREFIX, четвёртый никуда не ставится —
# он просто показывает, что с $PREFIX можно слинковаться:
#   zlib    1.3.1  — cmake    (корень, ни от кого не зависит)
#   libpng  1.6.48 — autoconf (ищет zlib через --with-zlib-prefix=$PREFIX)
#   freetype 2.13.3 — meson   (ищет zlib и libpng через pkg-config)
#   pngprobe 0.1.0 — cargo + libpng-sys 1.1.11 (линкует libpng через pkg-config)
#
# Запуск строго после окружения, из каталога article-draft:
#   . ./env.sh
#   sh after-env.sh
#
# Скрипт терпит повторные запуски: тарболл на месте — не качаем,
# маркер на месте (.pc в $PREFIX, бинарь в target/) — не пересобираем.
set -eu

cd "$(dirname "$0")"

: "${PREFIX:?PREFIX пуст — похоже, env.sh не подсорсили. Сначала: . ./env.sh}"
: "${AUTOCONF_CONFIGURE_FLAGS:?AUTOCONF_CONFIGURE_FLAGS пуст — без env.sh тут делать нечего. Сначала: . ./env.sh}"
: "${CMAKE_FLAGS:?CMAKE_FLAGS пуст — окружение не подхвачено. Сначала: . ./env.sh}"
: "${MESON_FLAGS:?MESON_FLAGS пуст — окружение не подхвачено. Сначала: . ./env.sh}"
: "${CARGO_BUILD_TARGET:?CARGO_BUILD_TARGET пуст — без env.sh цель неизвестна. Сначала: . ./env.sh}"
: "${READELF:?READELF пуст — без env.sh не знаем, чем смотреть бинари. Сначала: . ./env.sh}"

for cmd in curl cmake ninja meson cargo pkg-config make llvm-readelf; do
    command -v "$cmd" >/dev/null 2>&1 || {
        echo "error: нет '$cmd' в PATH — поставьте его, без него дальше никак" >&2
        exit 1
    }
done

JOBS="$(nproc 2>/dev/null || echo 4)"

fetch() {
    # fetch DEST URL [URL...]: качает DEST, перебирая зеркала по очереди.
    # Раз лежит — второй раз не качаем. Ключ -C - заодно подбирает оборванное.
    _dest="$1"
    shift
    if [ -f "$_dest" ]; then
        echo "-- $_dest уже скачан, снова не тянем"
        return 0
    fi
    for _url in "$@"; do
        echo "-- качаю $_url"
        if curl -fSL -C - -o "$_dest" "$_url"; then
            echo "-- $_dest скачался, всё честно"
            return 0
        fi
        echo "-- $_url не отдал, идём к следующему зеркалу"
    done
    echo "error: $_dest так и не скачался — гляньте сеть и зеркала" >&2
    return 1
}

# libpng и freetype ищут zlib.h препроцессором, а он читает только CPPFLAGS —
# CFLAGS ему мало (внутренняя генерация pnglibconf.h вообще зовёт компилятор
# без CFLAGS). А в sysroot NDK лежит свой древний zlib.h: без этого флага
# найдётся он, и заголовок разойдётся с библиотекой.
export CPPFLAGS="-I$PREFIX/include"

ZLIB_VER="1.3.1"
ZLIB_TARBALL="zlib-$ZLIB_VER.tar.gz"
ZLIB_SRC="zlib-$ZLIB_VER"

LIBPNG_VER="1.6.48"
LIBPNG_TARBALL="libpng-$LIBPNG_VER.tar.gz"
LIBPNG_SRC="libpng-$LIBPNG_VER"

FREETYPE_VER="2.13.3"
FREETYPE_TARBALL="freetype-$FREETYPE_VER.tar.gz"
FREETYPE_SRC="freetype-$FREETYPE_VER"

LIBPNG_SYS_VER="1.1.11"
PNGPROBE_VER="0.1.0"
PNGPROBE_SRC="pngprobe-$PNGPROBE_VER"

echo "== [1/4] zlib $ZLIB_VER (cmake — корень цепочки) =="
if [ -f "$PREFIX/lib/pkgconfig/zlib.pc" ] || [ -f "$PREFIX/share/pkgconfig/zlib.pc" ]; then
    echo "-- zlib уже в PREFIX, этот шаг пропускаем"
else
    fetch "$ZLIB_TARBALL" \
        "https://zlib.net/fossils/zlib-$ZLIB_VER.tar.gz" \
        "https://github.com/madler/zlib/releases/download/v$ZLIB_VER/zlib-$ZLIB_VER.tar.gz"
    rm -rf "$ZLIB_SRC"
    tar -xzf "$ZLIB_TARBALL"
    # $CMAKE_FLAGS без кавычек — там несколько флагов через пробел, так и надо
    cmake -S "$ZLIB_SRC" -B "$ZLIB_SRC/build-android" $CMAKE_FLAGS -DZLIB_BUILD_EXAMPLES=OFF
    cmake --build "$ZLIB_SRC/build-android" -j"$JOBS"
    cmake --install "$ZLIB_SRC/build-android"
    echo "-- zlib встал, корень есть"
fi

echo "== [2/4] libpng $LIBPNG_VER (autoconf — ему нужен наш zlib) =="
if [ -f "$PREFIX/lib/pkgconfig/libpng16.pc" ]; then
    echo "-- libpng уже на месте, второй раз не собираем"
else
    fetch "$LIBPNG_TARBALL" \
        "https://download.sourceforge.net/libpng/libpng-$LIBPNG_VER.tar.gz" \
        "https://github.com/pnggroup/libpng/archive/refs/tags/v$LIBPNG_VER.tar.gz"
    rm -rf "$LIBPNG_SRC"
    tar -xzf "$LIBPNG_TARBALL"
    (
        cd "$LIBPNG_SRC"
        # $AUTOCONF_CONFIGURE_FLAGS без кавычек — там --host и --prefix парой.
        # --with-zlib-prefix ведёт configure к нашему zlib с шага [1/4].
        # А CPPFLAGS (задан выше) достаёт даже до внутренней генерации
        # pnglibconf.h: та возьмёт наш zlib.h из $PREFIX, а не древний из sysroot NDK.
        ./configure $AUTOCONF_CONFIGURE_FLAGS --with-zlib-prefix="$PREFIX"
        make -j"$JOBS"
        make install
    )
    echo "-- libpng собрался против нашего zlib"
fi

echo "== [3/4] freetype $FREETYPE_VER (meson — тянет zlib и libpng) =="
if [ -f "$PREFIX/lib/pkgconfig/freetype2.pc" ]; then
    echo "-- freetype уже стоит, meson можно не гонять"
else
    fetch "$FREETYPE_TARBALL" \
        "https://download.savannah.gnu.org/releases/freetype/freetype-$FREETYPE_VER.tar.gz" \
        "https://github.com/freetype/freetype/archive/refs/tags/VER-2-13-3.tar.gz"
    rm -rf "$FREETYPE_SRC"
    tar -xzf "$FREETYPE_TARBALL"
    (
        cd "$FREETYPE_SRC"
        # $MESON_FLAGS без кавычек — там --prefix и --cross-file парой.
        # zlib и libpng находятся сами через pkg-config (PKG_CONFIG_LIBDIR из env.sh),
        # а brotli/bzip2/harfbuzz гасим явно — нечего meson шарить по хосту.
        meson setup build-android $MESON_FLAGS \
            -Dzlib=system -Dpng=enabled \
            -Dbrotli=disabled -Dbzip2=disabled -Dharfbuzz=disabled \
            -Dtests=disabled
        ninja -C build-android
        ninja -C build-android install
    )
    echo "-- freetype встал, сишная цепочка целиком"
fi

echo "== [4/4] pngprobe $PNGPROBE_VER (cargo + libpng-sys $LIBPNG_SYS_VER — проверка линковки) =="
# Подметаем за старым шагом [4/4]: xcadd ушёл, а хвосты его остались.
rm -f "$PREFIX/lib/libxcadd.a" "$PREFIX/lib/pkgconfig/xcadd.pc"
rm -rf "$PREFIX/include/xcadd"
if [ -f "$PNGPROBE_SRC/target/$CARGO_BUILD_TARGET/release/pngprobe" ]; then
    echo "-- pngprobe уже собран, cargo отдыхает"
else
    # pngprobe качать неоткуда — это наш собственный проверочный код,
    # рожаем его прямо здесь. Версия libpng-sys прибита выше строгим «=».
    mkdir -p "$PNGPROBE_SRC/src"
    cat > "$PNGPROBE_SRC/Cargo.toml" <<EOF
[package]
name = "pngprobe"
version = "$PNGPROBE_VER"
edition = "2021"

[dependencies]
libpng-sys = "=$LIBPNG_SYS_VER"
EOF
    cat > "$PNGPROBE_SRC/src/main.rs" <<'EOF'
fn main() {
    // Спрашиваем у libpng его версию — с кем слинковались, с тем и говорим.
    // Сам бинарь андроидный, тут его не запустить — важна лишь линковка,
    // её чуть ниже подтвердит проверка readelf.
    let ver = unsafe { libpng_sys::ffi::png_access_version_number() };
    println!("libpng version: {ver}");
    assert!(ver >= 10648, "unexpected libpng version: {ver}");
}
EOF
    (
        cd "$PNGPROBE_SRC"
        # libpng находится через pkg-config (PKG_CONFIG_LIBDIR из env.sh:
        # $PREFIX там первый, хостового /usr нет вовсе),
        # кросс для pkg-config разрешён через PKG_CONFIG_ALLOW_CROSS=1.
        # Линкер — из CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER, руками ничего не надо.
        cargo build --release --target "$CARGO_BUILD_TARGET"
    )
    # Установки тут нет — вместо неё доказательство линковки: этому шагу
    # в $PREFIX класть нечего (libpng-sys — rlib-only крейт без своих заголовков:
    # все .h уже лежат в $PREFIX/include с шага [2/4], а rlib живёт в target/;
    # рукописный .pc тем более не пишем — он бы загородил настоящий libpng16.pc).
    # В pkg-config-режиме бинарь динамически тянет libpng16.so, а при тихом
    # откате build.rs на вендорный static никакого NEEDED libpng16 не было бы —
    # тогда падаем с понятной ошибкой.
    if ! "$READELF" -d "$PNGPROBE_SRC/target/$CARGO_BUILD_TARGET/release/pngprobe" | grep -q "libpng16"; then
        echo "error: pngprobe не зацепил libpng16 из \$PREFIX — похоже, pkg-config его не нашёл" >&2
        rm -f "$PNGPROBE_SRC/target/$CARGO_BUILD_TARGET/release/pngprobe"
        exit 1
    fi
    echo "-- pngprobe собрался и держится за libpng16 из \$PREFIX"
fi

echo
echo "== всё собралось — смотрим, что осело в PREFIX=$PREFIX =="
pkg-config --modversion zlib libpng16 freetype2

У меня скрипты успешно выполнились, таким образом мы собрали cmake, autoconf, meson и rust проекты, и rust нашел нужные ему сишные зависимости в нашем $PREFIX.

Комментарии (0)