DevOps

Кросс-платформенная сборка: Windows/Linux/macOS в одном CI-пайплайне

1 просмотров

Введение

Ранее мы расматривали матричную сборку концептуально — в этой статье разберём практические сложности именно кросс-платформенной сборки Qt-приложений: различия в командах установки зависимостей, путях, и платформенно-специфичном коде, требующем учёта при настройке единого CI-пайплайна, охватывающего все три основные десктопные платформы.

Концепция

Каждая платформа имеет собственный, отличный способ установки Qt и зависимостей (apt на Ubuntu, Homebrew на macOS, vcpkg/готовые установщики на Windows), отличные пути и разделители путей в файловой системе, и иногда платформенно-специфичный код приложения (#ifdef Q_OS_WIN/Q_OS_MACOS/Q_OS_LINUX), который должен быть протестирован реально на соответствующей платформе, а не только скомпилирован условно для неё. CI-пайплайн, охватывающий все три платформы (через матричную сборку или эквивалентный механизм GitLab CI), должен явно учитывать эти различия в каждом отдельном шаге, а не предполагать единую, общую для всех платформ команду.

Пример кода

# Условные шаги, специфичные для конкретной платформы в рамках одной матричной сборки
jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      - name: Установка зависимостей (Linux)
        if: runner.os == 'Linux'
        run: sudo apt-get install -y qt6-base-dev libgl1-mesa-dev

      - name: Установка зависимостей (macOS)
        if: runner.os == 'macOS'
        run: brew install qt6

      - name: Установка зависимостей (Windows)
        if: runner.os == 'Windows'
        run: choco install qt6 # концептуально, реальная установка Qt на Windows часто через install-qt-action

      - name: Сборка (кросс-платформенная команда CMake, общая для всех ОС)
        run: |
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
          cmake --build build --config Release
// Платформенно-специфичный код в самом приложении — должен быть реально протестирован
// на соответствующей платформе, а не только успешно скомпилирован "условно" для неё
#ifdef Q_OS_WIN
    QString configPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
#elif defined(Q_OS_MACOS)
    QString configPath = QDir::homePath() + "/Library/Application Support/MyApp";
#elif defined(Q_OS_LINUX)
    QString configPath = QDir::homePath() + "/.config/MyApp";
#endif
# Артефакты сборки именуются с учётом платформы, чтобы не перепутать результаты разных ОС
- name: Загрузка артефакта сборки
  uses: actions/upload-artifact@v4
  with:
    name: MyApp-${{ matrix.os }}
    path: build/MyQtApp*

Пояснения к коду

Условные шаги (if: runner.os == 'Linux'/'macOS'/'Windows') показывают необходимый, явный учёт различий в способе установки зависимостей между платформами — несмотря на то, что CMake-команда сборки (cmake -B build -S .) одна и та же для всех трёх платформ (это одна из главных практических выгод CMake, упоминавшаяся в основном цикле статей), шаг установки зависимостей, предшествующий сборке, принципиально различается и должен быть явно разделён для каждой платформы. Платформенно-специфичный код через #ifdef Q_OS_WIN/Q_OS_MACOS/Q_OS_LINUX показывает, почему матричная сборка должна именно реально выполняться на каждой платформе (а не быть имитацией через условную компиляцию на единственной платформе) — иначе платформенно-специфичные ветки кода (как разные пути конфигурации в примере) останутся непроверенными на реальной целевой платформе до тех пор, пока приложение не будет реально запущено пользователем именно на ней.

Подводные камни

  • Предположение, что успешная компиляция платформенно-специфичного кода для всех трёх платформ автоматически означает его корректную работу на каждой из них — компилятор может успешно обработать каждую отдельную #ifdef-ветку кода без ошибок, но реальное, корректное поведение этого кода (правильность вычисленного пути, корректность вызова платформенно-специфичного API) можно подтвердить только реальным выполнением (а в идеале — автоматизированными тестами) именно на соответствующей платформе, а не только успешностью компиляции.
  • Игнорирование различий в путях файловой системы между платформами (обратный слеш на Windows против прямого / на Linux/macOS) при написании кода, работающего с путями файлов «вручную», через простую конкатенацию строк, вместо использования платформенно-независимых методов Qt (QDir, QFileInfo) — этот подводный камень не специфичен именно для CI, но именно реальное выполнение на разных платформах в рамках кросс-платформенного CI (а не только локальная разработка на одной, привычной разработчику платформе) с высокой вероятностью обнаружит подобные проблемы, если они существуют в коде.
  • Чрезмерные различия в конфигурации CI между платформами, не отражающие реальную необходимость, а возникшие из-за недостаточной унификации (например, разные версии Qt случайно указаны для разных платформ матрицы без явного, осознанного намерения) — стоит явно стремиться к максимальной унификации конфигурации между платформами там, где реальных причин для различия нет, оставляя платформенно-специфичные шаги только там, где они действительно неизбежны (как установка зависимостей через разные пакетные менеджеры).
  • Отсутствие явного, согласованного именования артефактов сборки для разных платформ (как показано в примере с MyApp-${{ matrix.os }}), что при множественных, параллельных задачах матричной сборки может привести к путанице или конфликту имён артефактов, перезаписывающих друг друга, если все они назывались бы одинаково независимо от платформы, для которой собраны.