Qt 5.1 и корректный deployment в Windows

    Добрый день!
    Для меня он вышел не очень добрым, 10 часов из жизни потрачено в поисках решения на простой вопрос, но в конечном счете я его нашел, и сейчас поделюсь с вами.
    Итак, краткая предыстория.
    Один мой заказчик попросил разработать kiosk-mode приложение на Qt. И для начала, сделать версию для POS-терминала с ОС Windows XP.
    Ok, за неделю я что-то сверстал по приложенным макетам и попробовал отдать версию заказчику. Приложив Qt5Core.dll, Qt5Gui.dll, ну и прочие .dll используемых модулей Qt.
    «Failed to load platform plugin „windows“» сказала японскаяфинская бензопила.

    Ага, идем в гугл.
    Во-первых, официальная документация:
    qt-project.org/doc/qt-5.0/qtdoc/deployment-windows.html
    Она не очень помогла, пошел читать форумы на qt-project.org. Вот предлагаемые решения:
    1) скопировать /qtbase/plugins/platforms/qwindows.dll в папку приложения в каталог platforms или platform
    попробовал, не работает
    2) скопировать /qtbase/plugins/platforms/qwindows.dll в папку приложения в каталог plugins/platforms, plugins/platform
    не работает
    3) выставить переменную окружения QT_QPA_PLATFORM_PLUGIN_PATH, указать в ней путь до папки с qwindows.dll
    заработало. Но: я не хочу модифицировать переменные среды при установке на компьютер пользователя. Во-первых, пользователь может удалить ее — а так как приложение падает при загрузке, проверить ее наличие я даже и не смогу без костылей. Во-вторых, пользователь может установить приложение с другой версией Qt — и привет, глюки и несовместимости.
    4) решение из официальной документации. При инициализации приложения в функции main() добавить строчку вида:
    qApp->addLibraryPath("C:/customPath/plugins");
    

    qApp это наш QApplication. Не заработало.
    5) использовать аргумент командной строки "-platformpluginpath \«путь_к_папке_с_qwindows.dll\»"
    проверил. работает. Вуаля! Вот решение! (что оказалось неправдой)

    Правим скрипт innosetup, вот так:

    [Files]
    ...
    Source: "..\build\deploy\platforms\qwindows.dll"; DestDir: "{app}\platforms"; Flags: ignoreversion
    
    [Icons]
    Name: "{group}\{#MyAppName}"; Filename: "{app}\TryumPosWin.exe"; Parameters:"-platformpluginpath \"{app}\platforms\"" ; WorkingDir: "{app}";
    


    отдаем заказчику, радуемся.
    Рано радуемся, не работает sqlite. подкладывание в plugins\sqldrivers, просто sqldrivers не помогло — не видит и не загружает, вот такой код:
        if (QSqlDatabase::isDriverAvailable("QSQLITE")){
            qDebug("QSqlite driver found.");
        } else {
            qFatal("QSqlite driver NOT found!");
        }
    

    выпадал с ошибкой. Ни переменной среды, ни аргумента командной строки для sql плагинов я не нашел.
    Вернулся к официальной документации и подумал. А собственно почему статический метод мы вызываем из экземпляра?
    А раз он статический, может его можно вызвать до создания экземпляра?

    Вот такой код наконец работает:
    int main(int argc, char *argv[])
    {
        Q_INIT_RESOURCE(resources);
    
        QStringList paths = QCoreApplication::libraryPaths();
        paths.append(".");
        paths.append("imageformats");
        paths.append("platforms");
        paths.append("sqldrivers");
        QCoreApplication::setLibraryPaths(paths);
    
        QApplication a(argc, argv);
    
        a.setQuitOnLastWindowClosed(false);
    
        QDbc::init();
    
        MainWindow w;
        w.showFullScreen();
    
        a.exec();
    
        QDbc::finalize();
    }
    

    Кстати, встречал еще вот такой способ задания пути:
    paths.append(QCoreApplication::applicationDirPath() + "/plugins");
    

    но он работает некорректно, так как QCoreApplication::applicationDirPath() выдает warning, если экземпляр QApplication еще не создан.

    И собственно секция «Files» в InnoSetup выглядит так:

    [Files]
    Source: "..\build\deploy\icudt51.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\icuin51.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\icuuc51.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\libEGL.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\libGLESv2.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\msvcp100.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\msvcr100.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\Qt5Core.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\Qt5Gui.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\Qt5Network.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\Qt5Sql.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\Qt5Widgets.dll"; DestDir: "{app}"; Flags: ignoreversion
    Source: "..\build\deploy\imageformats\qico.dll"; DestDir: "{app}\imageformats"; Flags: ignoreversion
    Source: "..\build\deploy\platforms\qwindows.dll"; DestDir: "{app}\platforms"; Flags: ignoreversion
    Source: "..\build\deploy\sqldrivers\qsqlite.dll"; DestDir: "{app}\sqldrivers"; Flags: ignoreversion
    


    Статья не претендует на всеобъемлющее исследование, но корректного работающего решения я не нашел и поэтому решил опубликовать свое.
    Спасибо за внимание!
    Метки:
    Поделиться публикацией
    Комментарии 37
    • +3
      Чтобы получить applicationDirPath до создания QApplication можете использовать этот код:
      QFileInfo(argv[0]).dir().path()

      PS: мой терминал на чистом QML: youtu.be/UTMOd2s9Vkk
      • 0
        Супер терминал получился. QML похоже отлично подходит для такого рода приложений. Интересно узнать подробности. Не хотите статью написать? Где будет использоваться. Используется ли уже? Какие сложности были? и т.п.
        • +1
          Сложности были только с заказчиками :)
          Программу написал на QML v1. Думаю теперь получиться намного лучше с использованием QML v2.
          Но и на QML v1 программа постоянноо выдавала 60 фпс (подлагивание мышки на видео это вина программы что записывала видео).
          Когда будет больше свободного времени то напишу.
          Терминалы еще не на улицах из-за этих самых сложностей :)
        • +1
          этот неловкий момент, когда понимаешь что еще есть куда расти. терминал не сложный, но на чистом qml?
          • +1
            Да, на чистом QML 1 и немножко JavaScript для упрощения настроек подключяемых карт.
            Qt использовался только для синхронизации переменных.
            А вы покажите свою программу?
          • +2
            Офигенский терминальчик, не хотят платить — забейте на тех заказчиков, выставьте на аукцион или продайте другому городу.
            • 0
              А вы уверены, что тачскрины на терминалах смогут нормально отрабатывать перетаскивание? Да и вообще возякать пальцем по грязному экрану на улице…
              Просто никогда такого не видел.
              • 0
                Вот более старое видео ранней версии с демонстрацией работы на самом тачскрине.
                • 0
                  А эти две программы разрабатывал в 2008 году, когда не было QML вовсе: первое, второе.
                  Терминалов с первого видео было штук 30, с перетаскиванием проблем никогда не было.
                  • 0
                    qss использовали?
                    • 0
                      В основном QGraphicsView.
                      Использовал StyleSheets для задания фонов.
              • +3
                Странно, когда я разбирался с такой же проблемой, мне хватило документированного способа с подкладыванием qwindows.dll в каталог platforms, созданный рядом с EXE-файлом. Впрочем, если приложение пытается искать плагины где-то в другом месте (кстати, где именно — легко определить через Process Monitor), то достаточно создать файл qt.conf, указав в нём все необходимые пути.
                • 0
                  а раньше в какой версии? в 4.7 у меня тоже работало. спасибо за подсказку про qt.conf, этого способа я не нашел
                  • 0
                    Разбираться с этим начал с версией 5.0.1 или 5.0.2, а после релиза 5.1.0 пересобрал программу и ещё раз проверил, что всё продолжает работать.
                • 0
                  А никто не делал пакетов для деплоя самой платформы Qt? Ну т.е. есть у меня десяток-другой приложений на Qt, самих по себе мелких. С каждым тащить все либы мегабайт на 20 не хочется. Как корректно задеплоить саму Qt, чтобы потом только ставить приложения и все?
                  • 0
                    Во первых, каждую длл можно сжать UPX'ом и получиться меньший размер.
                    Во вторых, можно сделать статик сборку и получиться один файл, а потом его UPX'ом, выйдет до 4 мб.
                    Мои примеры: раз, два.
                    • 0
                      Все-таки один раз поставить runtime и потом хоть 20 программ по сотне килобайт лучше, чем 20 ужатых программ по 4 МБ.
                      • 0
                        Вам нужно прописать папку с дллками в глобальною переменную PATH или закинуть все длл в папку system32.
                        Это ответ на ваш вопрос?
                        • 0
                          Да это-то понятно и очевидно. Но может есть более элегантное поддерживаемое решение. Уж чего-чего, а в system32 кидать не хотелось бы.
                          • 0
                            Но может есть более элегантное поддерживаемое решение.

                            Вам нужно прописать папку с дллками в глобальною переменную PATH

                            Куда элегантней то?
                            • 0
                              Честно, я боюсь конфликтов в сторонних приложениях. Особенно если они будут использовать одну и ту же ветку Qt. Пропишу я свой, возможно более новый рантайм, а тут привет, не работает сторонний софт.
                      • +2
                        Пожалуйста, не сжимайте их UPX'ом. А тем более в статик сборке. Это бессмысленно, разница между UPX + Inno (LZMA, Ultra) и просто Inno (LZMA, Ultra) будет в пользу последнего.
                        • 0
                          Все программы работают без инсталятора. Аля Mac OS установка style.
                          Тесты не показали различий в производительности с UPX и без, только с UPX больше оперативки на 10 мб используеться.
                          Используеться безопасное автоматическое обновление, в котором качаеться ехе файл и заменяет себя, а тут размер как раз и важен.
                          • 0
                            При таком раскладе, соглашусь, неплохой вариант. Вы весь свой софт в таком форм-факторе поставляете?
                            • 0
                              Open Source софт да.
                              А коммерческий в шаред сборке и с инсталлятором.
                      • 0
                        Ну в общем-то, при желании что-то такое изобразить можно… по аналогии с java runtime.
                        Сделать отдельный инсталлятор, будет, допустим, ставиться в c:\Program Files\Nokia Qt Runtime\5.1.0\
                        и прописывать веточку в реестре SOFTWARE\Nokia\Qt Runtime\
                        а в каждом своем приложении при запуске читать реестр, находить нужную веточку (допустим, одно приложение хочет 4.7, другое 5.0.1, третье 5.1.0) — и прописывать у себя пути через setLibraryPaths перед инциализацией QApplication.
                        В этом отдельном инсталляторе даже крыжики можно предусмотреть для отдельных модулей, а в приложениях проверять весь ли нужный набор установлен.
                        Остается вопрос: стоит ли этот велосипед потраченных усилий?
                      • 0
                        Извиняюсь, промахнулся. После редактирования комментария и кнопки браузера «Назад» оказываеться комментарий не редактируеться а создаеться новый.
                        • +2
                          Чтоб не было подобных проблем, что поправил одну зависимость, а у заказчика выскочила новая. Нужно у себя создавать виртуалку с чистой ос и там тестировать на зависимости.
                          • 0
                            это верно, зоопарк виртуалок у меня есть с разными ОС/сервис-паками/браузерами, остался после тестирования совместимости тулбара который я делал.
                            а тут что-то поторопился, перепроверять не стал
                            • 0
                              К сожалению, для полной картины нужны виртуалки со всеми распространенными версиями ОС. Недавно для меня стало сюрпризом, что некоторые библиотеки MSVC runtime, предустановленные в Windows 7, отсутствуют по умолчанию в Windows XP.
                              • 0
                                Правильно. Потому я у себя и держу:
                                — Windows XP (x86, x64)
                                — Windows Vista (x86, x64)
                                — Windows 7 (x86, x64)
                                — Windows 8 (x86, x64)
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • +1
                                Так я же писал про это.
                                Не работает, потому что addLibraryPath добавляется уже после создания QApplication, когда приложение уже упало на строчке
                                QApplication app(argc, argv);
                                А во-вторых, метод-то статический. Мне кажется неправильным вызывать его через экземпляр, надо как-то так:
                                QCoreApplication::addLibraryPath();
                                • 0
                                  И чтобы не быть голословным, привожу исходный код QGuiApplication
                                  вот конструктор:
                                  #ifdef Q_QDOC
                                  QGuiApplication::QGuiApplication(int &argc, char **argv)
                                  #else
                                  QGuiApplication::QGuiApplication(int &argc, char **argv, int flags)
                                  #endif
                                      : QCoreApplication(*new QGuiApplicationPrivate(argc, argv, flags))
                                  {
                                      d_func()->init();
                                  
                                      QCoreApplicationPrivate::eventDispatcher->startingUp();
                                  }
                                  


                                  в методе init() идет разбор аргументов командной строки, он длинный, весь приводить его не буду. а еще там есть такое:
                                      if (platform_integration == 0)
                                          createPlatformIntegration();
                                  


                                  Посмотрим на этот метод:
                                  void QGuiApplicationPrivate::createPlatformIntegration()
                                  {
                                      // Use the Qt menus by default. Platform plugins that
                                      // want to enable a native menu implementation can clear
                                      // this flag.
                                      QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, true);
                                  
                                      // Load the platform integration
                                      QString platformPluginPath = QLatin1String(qgetenv("QT_QPA_PLATFORM_PLUGIN_PATH"));
                                  
                                  
                                      QByteArray platformName;
                                  #ifdef QT_QPA_DEFAULT_PLATFORM_NAME
                                      platformName = QT_QPA_DEFAULT_PLATFORM_NAME;
                                  #endif
                                      QByteArray platformNameEnv = qgetenv("QT_QPA_PLATFORM");
                                      if (!platformNameEnv.isEmpty()) {
                                          platformName = platformNameEnv;
                                      }
                                  
                                      // Get command line params
                                  
                                      int j = argc ? 1 : 0;
                                      for (int i=1; i<argc; i++) {
                                          if (argv[i] && *argv[i] != '-') {
                                              argv[j++] = argv[i];
                                              continue;
                                          }
                                          QByteArray arg = argv[i];
                                          if (arg == "-platformpluginpath") {
                                              if (++i < argc)
                                                  platformPluginPath = QLatin1String(argv[i]);
                                          } else if (arg == "-platform") {
                                              if (++i < argc)
                                                  platformName = argv[i];
                                          } else {
                                              argv[j++] = argv[i];
                                          }
                                      }
                                  
                                      if (j < argc) {
                                          argv[j] = 0;
                                          argc = j;
                                      }
                                  
                                      init_platform(QLatin1String(platformName), platformPluginPath);
                                  }
                                  


                                  На init_platform все и падает

                                  Наследование классов выглядит так:
                                  QCoreApplication->QGuiApplication->QAppplication

                                • 0
                                  man qt.conf уже же наконец.
                                  harmattan-dev.nokia.com/docs/library/html/qt4/qt-conf.html
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                  • 0
                                    Вроде в документации достаточно понятно написано, что по умолчанию будет искать плигины в каталогах рядом с исполняемым файлом. Т.е. в каталоге с приложением создаем platforms, в него кидаем qwindows.dll и все. И для остальных плагинов таким же образом.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.