Введение
В предыдущей статье мы рассмотрели общую структуру проекта 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) может потребоваться явная логика миграции состояния, а не предположение, что у всех пользователей одинаковое «чистое» начальное состояние.