DevOps

Создание установщика с QtIFW: компоненты, страницы, скрипты

Введение

В предыдущей статье мы рассмотрели общую структуру проекта QtIFW — теперь построим более развёрнутый, реалистичный установщик с несколькими опциональными компонентами (основное приложение и дополнительные языковые пакеты), кастомной страницей мастера установки и скриптом, выполняющим дополнительные действия после копирования файлов (создание ярлыка с собственными параметрами, регистрация в системе).

Концепция

Зависимости между компонентами объявляются через тег <Dependencies> в package.xml — это позволяет, например, сделать языковой пакет недоступным для выбора, пока не выбран основной компонент приложения, от которого он зависит. Кастомная логика на этапе установки реализуется через JavaScript-скрипт компонента, который может переопределять стандартные хуки жизненного цикла установки (Component.prototype.createOperations, вызываемый для добавления дополнительных операций установки помимо простого копирования файлов) и добавлять собственные страницы в мастер установки через installer.addWizardPage().

Пример кода: Создание установщика с QtIFW

<!-- packages/com.mycompany.myapp.langpack.ru/meta/package.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<Package>
    <DisplayName>Русский языковой пакет</DisplayName>
    <Description>Перевод интерфейса MyApp на русский язык</Description>
    <Version>1.0.0</Version>
    <ReleaseDate>2026-06-23</ReleaseDate>
    <Default>false</Default>
    <Dependencies>com.mycompany.myapp</Dependencies>
</Package>
// packages/com.mycompany.myapp/meta/installscript.qs
function Component()
{
    // Конструктор компонента — здесь регистрируются обработчики событий жизненного цикла
    component.loaded.connect(this, Component.prototype.onComponentLoaded);
}

Component.prototype.onComponentLoaded = function()
{
    // Добавляем собственную страницу мастера установки сразу после
    // стандартной страницы выбора компонентов
    installer.addWizardPage(component, "CustomShortcutPage", QInstaller.ComponentSelection);
};

Component.prototype.createOperations = function()
{
    // Сначала выполняем стандартные операции (копирование файлов и т.п.)
    component.createOperations();

    if (systemInfo.productType === "windows") {
        component.addOperation("CreateShortcut",
            "@TargetDir@/MyApp.exe",
            "@StartMenuDir@/MyApp.lnk",
            "workingDirectory=@TargetDir@");

        // Запись пути установки в реестр для последующего обнаружения приложением
        component.addOperation("Settings",
            "path=HKEY_CURRENT_USER\\Software\\MyCompany\\MyApp",
            "key=InstallPath",
            "value=@TargetDir@");
    } else if (systemInfo.productType === "osx") {
        component.addOperation("CreateDesktopEntry"); // условный пример для других платформ
    }
};
// Пример простой кастомной страницы выбора дополнительной опции при установке
// (упрощённо — в реальном проекте это отдельный .ui-файл, загружаемый через loadUi)
Component.prototype.onComponentLoaded = function()
{
    installer.addWizardPage(component, "CustomOptionsPage", QInstaller.TargetDirectory);
    var page = gui.pageWidgetByObjectName("DynamicCustomOptionsPage");
    if (page) {
        page.windowTitle = "Дополнительные настройки";
    }
};

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

package.xml языкового пакета объявляет <Dependencies>com.mycompany.myapp</Dependencies> — это гарантирует, что установщик не позволит пользователю выбрать установку русского языкового пакета без одновременной установки основного компонента приложения, и автоматически корректирует выбор компонентов в интерфейсе при попытке нарушить эту зависимость.

installscript.qs основного компонента переопределяет createOperations(): сначала вызывается базовая реализация (component.createOperations()), выполняющая стандартное копирование файлов из data/, а затем добавляются дополнительные, платформо-специфичные операции — создание ярлыка в меню «Пуск» через встроенную операцию CreateShortcut (доступную только на Windows, что и проверяется через systemInfo.productType) и запись пути установки в реестр Windows через операцию Settings, что может использоваться самим приложением впоследствии для определения, куда оно было установлено.

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

  • Циклические зависимости между компонентами (компонент A зависит от B, который зависит от A) — QtIFW не имеет защиты от логически некорректного описания такой ситуации в package.xml, и результат поведения интерфейса выбора компонентов в этом случае непредсказуем — зависимости должны образовывать строго ациклический граф, что нужно проверять самостоятельно при проектировании сложной структуры с множеством взаимосвязанных опциональных пакетов.
  • Платформо-специфичные операции (CreateShortcut, реестр Windows), вызванные без проверки systemInfo.productType, на других платформах могут либо тихо ничего не делать, либо выбросить ошибку скрипта, прерывающую установку — каждая операция, специфичная для конкретной ОС, должна быть явно обёрнута в соответствующую проверку, как показано в примере, а не вызываться безусловно для всех платформ из единого, формально кросс-платформенного описания.
  • Отладка JavaScript-скриптов установщика значительно сложнее обычной отладки C++/QML-кода — ошибки в .qs-файлах часто проявляются как неясные сбои на этапе установки без подробного стектрейса, удобного для анализа в обычной IDE; разработка и тестирование скриптов установщика обычно требует итеративного запуска самого собранного установщика с включённым подробным логированием (--verbose), а не привычной отладки через breakpoints.
  • Изменение поведения существующих, уже выпущенных пользователям версий установщика при обновлении логики скриптов. Если скрипт установки кардинально меняется между версиями (например, меняется набор операций реестра), нужно явно учитывать, что у части пользователей уже установлена версия с предыдущей логикой, и при последующем обновлении (через maintenance tool, статья 213) может потребоваться явная логика миграции состояния, а не предположение, что у всех пользователей одинаковое «чистое» начальное состояние.