Давно интересовал вопрос возможности кросскомпиляции под macOS. Как оказалось, на самом деле это не составит особых проблем.

В этой статье мы установим тулчейн OSXCross на Ubuntu 24.04, а так же кросскомпилируем пару учебных примеров на языке C (кстати, проделывать то же самое с Rust я тоже пробовал, но об этом как-нибудь в другой раз). В качестве target будет выступать macOS 14 Sonoma на Apple Silicon.

Поднимаем OSXCross

Для сборки OSXCross нам потребуется clang. Если его еще нет в системе, то устанавливаем с помощью apt:

$ sudo apt install clang

Клонируем git-репозиторий osxcross и запускаем build.sh:

$ git clone https://github.com/tpoechtrager/osxcross
$ cd osxcross && ./build.sh
no SDK found in tarballs/. please see README.md

Как мы видим, в следствие причин, связанных с копирастией вопросами интеллектуальной собственности авторы osxcross решили не связываться с распостранением macOS SDK. Вместо этого в README.md нам предлагается самостоятельно выдрать SDK из xcode.dmg, который нужно скачать с сайта Apple.

К счастью, в итоге я нашел репозиторий, авторы которого просто выкладывают уже запакованные тарболлы с SDK на Github. Так что пока не прикрыли, просто качаем оттуда:

$ wget -P tarballs/ 'https://github.com/joseluisq/macosx-sdks/releases/download/14.0/MacOSX14.0.sdk.tar.xz'
$ ./build.sh

Запускаем Hello World

Если все хорошо, то где-то через 10-30 минут мы получим готовый OSXCross. На этом этапе у нас должно быть все необходимое для кросскомпиляции Hello World. Поэтому пробуем скомпилировать:

$ export PATH=$HOME/osxcross/target/bin/:$PATH
$ aarch64-apple-darwin23-cc hello.c -o hello

Копируем собранный бинарник на реальную macOS и запускаем. Как мы видим, все работает:

user@macbook ~ % ./hello
Hello World!

Собираем что-нибудь посерьезнее

Конечно, одним Hello World-ом сыт не будешь. Поэтому попробуем разобраться что делать в случае, если у проекта есть какие-либо внешние завимости.

В качестве подопытного образца у меня заготовлен небольшой учебный пример, делающий запрос информации об устройстве с мобильного телефона по протоколу PTP/MTP с помощью libusb.

Скрытый текст
#include <stdio.h>
#include <stdlib.h>
#include <libusb.h>
#include <assert.h>

void exit_with_error(const char *msg) {
    fprintf(stderr, "ERROR: %s\n", msg);
    exit(-1);
}

char *ucs2_to_ascii(const unsigned char *d, size_t char_count) {
    char *ret = malloc(char_count);
    for(int i = 0; i < char_count; i++) {
        ret[i] = d[i * 2];
    }
    return ret;
}

int main() {
    libusb_context *ctx;
    assert(libusb_init(&ctx) == 0);
    libusb_device_handle *dev = libusb_open_device_with_vid_pid(NULL, 0x04e8, 0x6860);
    if(!dev) exit_with_error("failed to open device");

    if(libusb_kernel_driver_active(dev, 0)) {
        assert(libusb_detach_kernel_driver(dev, 0) == 0);
    }

    assert(libusb_claim_interface(dev, 0) == 0);

    printf("Everything OK\n");

    unsigned char cmd[] = {
        0x0c, 0x00, 0x00, 0x00, // length = 12
        0x01, 0x00,                 // container_type = COMMAND
        0x01, 0x10,                 // opcode = 0x1001 (GetDeviceInfo)
        0x01, 0x00, 0x00, 0x00, // transaction_id = 1
    };

    int actual;
    assert(libusb_bulk_transfer(dev, (1 | LIBUSB_ENDPOINT_OUT), cmd, sizeof(cmd), &actual, 0) == 0);
    printf("%d bytes written\n", actual);

    unsigned char buf[4096];
    assert(libusb_bulk_transfer(dev, (1 | LIBUSB_ENDPOINT_IN), buf, sizeof(buf), &actual, 0) == 0);
    printf("%d bytes read\n", actual);

    printf("\n");
    uint16_t standardVersion = *((uint16_t*) &buf[12]); // будем считать, что мы на little endian-архитектуре
    uint32_t vendorExtensionID = *((uint32_t*) &buf[14]);
    uint16_t vendorExtensionVersion = *((uint16_t*) &buf[18]);

    printf("standardVersion: %d.%d\n", standardVersion / 100, standardVersion % 100);
    printf("vendorExtensionID: %d\n", vendorExtensionID);
    printf("vendorExtensionVersion: %d.%d\n", vendorExtensionVersion / 100, vendorExtensionVersion % 100);

    uint8_t vendorExtensionDescLength = buf[20];
    char *vendorExtensionDesc = ucs2_to_ascii(&buf[21], vendorExtensionDescLength);
    printf("vendorExtensionDesc: %s\n", vendorExtensionDesc);
    unsigned int p = 21 + vendorExtensionDescLength * 2;

    uint16_t functionalMode = *((uint16_t*) &buf[p]);
    printf("functionalMode: %04x\n", functionalMode);
    p += 2;

    printf("\n");

    /* Местами нарушаем принцип DRY. Но для демонстрационного примера покатит */
    uint32_t opcodesCount = *((uint32_t*) &buf[p]);
    p += 4 + opcodesCount * 2;

    uint32_t eventsSupportedCount = *((uint32_t*) &buf[p]);
    p += 4 + eventsSupportedCount * 2;

    uint32_t devicePropertiesSupportedCount = *((uint32_t*) &buf[p]);
    p += 4 + devicePropertiesSupportedCount * 2;

    uint32_t captureFormatsSupportedCount = *((uint32_t*) &buf[p]);
    p += 4 + captureFormatsSupportedCount * 2;

    uint32_t imageFormatsSupportedCount = *((uint32_t*) &buf[p]);
    p += 4 + imageFormatsSupportedCount * 2;

    char *manufacturer = ucs2_to_ascii(&buf[p+1], buf[p]);
    p += 1 + buf[p] * 2;
    printf("manufacturer: %s\n", manufacturer);

    char *model = ucs2_to_ascii(&buf[p+1], buf[p]);
    p += 1 + buf[p] * 2;
    printf("model: %s\n", model);

    char *deviceVersion = ucs2_to_ascii(&buf[p+1], buf[p]);
    p += 1 + buf[p] * 2;
    printf("deviceVersion: %s\n", deviceVersion);

    char *serialNumber = ucs2_to_ascii(&buf[p+1], buf[p]);
    p += 1 + buf[p] * 2;
    printf("serialNumber: %s\n", serialNumber);

    // Забираем статус MTP-операции, иначе у конечного автомата устройства съедет крыша и его придется переподключать.
    // Кстати, теоретически, эти байты могут "приклеиться" к концу предыдущего буфера, если длина предыдущей передачи кратна
    // wMaxPacketSize для данного endpoint (обычно 512), и мы имеем дело с ушлепским девайсом который не производит передачу
    // пакета нулевой длины с целью обозначения конца транзакции в данной ситуации - в силу того, как работает bulk transfer.
    //
    // Поэтому в production-коде по-хорошему мы должны обрабатывать данную ситуацию с использованием свойств конкретного
    // прикладного протокола, в данном случае - поля length MTP-заголовка.
    assert(libusb_bulk_transfer(dev, (1 | LIBUSB_ENDPOINT_IN), buf, sizeof(buf), &actual, 0) == 0);
}

Поскольку в составе тулчейна OSXCross есть штатный пакетный менеджер в виде самописного клиента macports, сначала пробуем установить через него:

$ export PATH=$HOME/osxcross/target/bin/:$PATH
$ export MACOSX_DEPLOYMENT_TARGET=14.0
$ ./tools/osxcross-macports install libusb libusb-devel
searching package libusb ...
getting libusb-1.0.27_0.darwin_23.x86_64.tbz2 ...
getting macports public key ...
verifying file integrity of libusb-1.0.27_0.darwin_23.x86_64.tbz2 ...
installing libusb ...
installed libusb
searching package libusb-devel ...
getting libusb-devel-20220904-f3619c40_0.darwin_23.x86_64.tbz2 ...
verifying file integrity of libusb-devel-20220904-f3619c40_0.darwin_23.x86_64.tbz2 ...
installing libusb-devel ...
installed libusb-devel

Как мы видим, пакет с libusb, находящийся в репозиториях macports содержит только сборку для x86_64, и нам не подходит. Что ж, раз так, то соберем libusb самостоятельно.

В принципе, процесс сборки не очень-то отличается от обычного, но нужно будет передать системе сборки triplet архитектуры, путь до SDK и, если мы хотим использовать pkg-config, нужный префикс.

$ wget 'https://github.com/libusb/libusb/releases/download/v1.0.27/libusb-1.0.27.tar.bz2'
$ tar -xvf libusb-1.0.27.tar.bz2 && cd libusb-1.0.27
$ ./configure --host=aarch64-apple-darwin --includedir=$HOME/osxcross/target/SDK/MacOSX14.0.sdk/usr/include/ CC="aarch64-apple-darwin23-cc" CFLAGS="-I$HOME/osxcross/target/SDK/MacOSX14.0.sdk/usr/include/ -D__MAC_OS_X_VERSION_MIN_REQUIRED=1400" CXXFLAGS="-I$HOME/osxcross/target/SDK/MacOSX14.0.sdk/usr/include/ -D__MAC_OS_X_VERSION_MIN_REQUIRED=1400" --prefix=$HOME/osxcross/target/SDK/MacOSX14.0.sdk/
$ make
$ make install

Если все прошло хорошо, то в директории с динамическими библиотеками тулчейна OSXCross должен появиться файл libusb-1.0.0.dylib.

Теперь пробуем скомпилировать get_device_info.c. Помимо $PATH, потребуется так же выставить переменную окружения $PKG_CONFIG_PATH:

$ export PKG_CONFIG_PATH=$HOME/osxcross/target/SDK/MacOSX14.0.sdk/lib/pkgconfig/
$ aarch64-apple-darwin23-cc get_device_info.c `pkg-config --cflags --libs libusb-1.0` -o get_device_info

Копируем получившийся бинарник на macOS и пробуем запустить его. Однако, теперь при запуске мы получаем странную ошибку:

user@notebook ~ % ./get_device_info 
dyld[78519]: Library not loaded: /home/user/osxcross/target/SDK/MacOSX14.0.sdk/lib/libusb-1.0.0.dylib
  Referenced from: <D23A4640-AB4D-3A87-8F05-2057928A6830> /private/tmp/get_device_info
  Reason: tried: '/home/user/osxcross/target/SDK/MacOSX14.0.sdk/lib/libusb-1.0.0.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/home/user/osxcross/target/SDK/MacOSX14.0.sdk/lib/libusb-1.0.0.dylib' (no such file), '/home/user/osxcross/target/SDK/MacOSX14.0.sdk/lib/libusb-1.0.0.dylib' (no such file)
zsh: abort      ./get_device_info

А все потому, что в macOS принято использовать абсолютные пути библиотек при динамической линковке. Как следствие, загрузчик исполняемых файлов пытается искать библиотеку по абсолютному пути который был в системе, на которой происходила сборка.

Поэтому фиксим проблему при помощи штатной утилиты для таких случаев, указав путь до libusb-1.0.0.dylib установленного через brew:

user@macbook ~ % install_name_tool -change /home/user/osxcross/target/SDK/MacOSX14.0.sdk/lib/libusb-1.0.0.dylib /opt/homebrew/lib/libusb-1.0.0.dylib get_device_info

Теперь подключаем андроидофон к маку и снова запускаем get_device_info. И - вуаля:

user@macbook ~ % ./get_device_info 
Everything OK
12 bytes written
505 bytes read

standardVersion: 1.0
vendorExtensionID: 6
vendorExtensionVersion: 1.0
vendorExtensionDesc: microsoft.com: 1.0; android.com: 1.0; samsung.com/kies: 6.0; samsung.com/devicestatus: 1;
functionalMode: 0000

manufacturer: samsung
model: SM-A325F
deviceVersion: A325FXXU2BVF1
serialNumber: FFADXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Заключение

Как мы видим, несмотря на тяготение Apple Way к максимальной обособленности компиляция под данную платформу из под других ОС вполне возможна и имеет практический смысл - за что спасибо титаническому труду авторов opensource-тулчейнов. Возможно что кому-то (в т.ч. будущему мне) эта статья сбережет час-другой времени, избавит от необходимости содержания build-ферм или даже станет стартом для улучшения своих CD-пайплайнов. Всем дочитавшим спасибо!

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