Обходной путь для Qt Installer Framework, не перезаписывающий существующую установку

Этот вопрос касается версии 2.0 Qt Installer Framework.

На данный момент всем, кто пользуется Qt Installer Framework, известно, что без настройки вы просто не сможете перезаписать существующую установку через ваш установщик. По-видимому, это было сделано для решения некоторых проблем, которые возникли, когда это было сделано с помощью инфраструктуры Qt.

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

Я ищу решение, включающее пользовательский скрипт компонента UI +, который добавляет кнопку на целевую страницу каталога, которая позволяет пользователю

  1. Удалить указанный каталог, если он существует, или
  2. Запустите инструмент обслуживания в этом каталоге.

Было бы предпочтительнее иметь возможность запускать инструмент обслуживания в целевом каталоге, с его автоматическим удалением определенного пакета, но я понимаю, что это требует слишком много.

Я прочитал ответы на другие вопросы на этом сайте о решении той же проблемы, но ни одно из решений не работает правильно. Я также хотел бы отметить, что у меня запущен и работает скрипт компонента, но нет пользовательских интерфейсов.

6 ответов

Решение

Я наконец нашел подходящее решение.

Вам нужно три вещи, чтобы осуществить это:

  1. Компонентный скрипт,
  2. Пользовательский интерфейс для целевой страницы каталога и
  3. Скрипт контроллера, который автоматически выбирает программу удаления.

Я сейчас перечислю дословно, что работает для меня (с моим конкретным проектом). Мой компонент называется Atlas4500 Tuner

config.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Installer>
    <Name>Atlas4500 Tuner</Name>
    <Version>1.0.0</Version>
    <Title>Atlas4500 Tuner Installer</Title>
    <Publisher>EF Johnson Technologies</Publisher>
    <StartMenuDir>EF Johnson</StartMenuDir>
    <TargetDir>C:\Program Files (x86)\EF Johnson\Atlas4500 Tuner</TargetDir>
</Installer>

packages / Atlas4500 Tuner / meta / package.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Package>
    <DisplayName>Atlas4500Tuner</DisplayName>
    <Description>Install the Atlas4500 Tuner</Description>
    <Version>1.0.0</Version>
    <ReleaseDate></ReleaseDate>
    <Default>true</Default>
    <Required>true</Required>
    <Script>installscript.qs</Script>
    <UserInterfaces>
        <UserInterface>targetwidget.ui</UserInterface>
    </UserInterfaces>
</Package>

пакеты сценариев пользовательских компонентов / Atlas4500 Tuner / meta / installscript.qs:

var targetDirectoryPage = null;

function Component() 
{
    installer.gainAdminRights();
    component.loaded.connect(this, this.installerLoaded);
}

Component.prototype.createOperations = function() 
{
    // Add the desktop and start menu shortcuts.
    component.createOperations();
    component.addOperation("CreateShortcut",
                           "@TargetDir@/Atlas4500Tuner.exe",
                           "@DesktopDir@/Atlas4500 Tuner.lnk",
                           "workingDirectory=@TargetDir@");

    component.addOperation("CreateShortcut",
                           "@TargetDir@/Atlas4500Tuner.exe",
                           "@StartMenuDir@/Atlas4500 Tuner.lnk",
                           "workingDirectory=@TargetDir@");
}

Component.prototype.installerLoaded = function()
{
    installer.setDefaultPageVisible(QInstaller.TargetDirectory, false);
    installer.addWizardPage(component, "TargetWidget", QInstaller.TargetDirectory);

    targetDirectoryPage = gui.pageWidgetByObjectName("DynamicTargetWidget");
    targetDirectoryPage.windowTitle = "Choose Installation Directory";
    targetDirectoryPage.description.setText("Please select where the Atlas4500 Tuner will be installed:");
    targetDirectoryPage.targetDirectory.textChanged.connect(this, this.targetDirectoryChanged);
    targetDirectoryPage.targetDirectory.setText(installer.value("TargetDir"));
    targetDirectoryPage.targetChooser.released.connect(this, this.targetChooserClicked);

    gui.pageById(QInstaller.ComponentSelection).entered.connect(this, this.componentSelectionPageEntered);
}

Component.prototype.targetChooserClicked = function()
{
    var dir = QFileDialog.getExistingDirectory("", targetDirectoryPage.targetDirectory.text);
    targetDirectoryPage.targetDirectory.setText(dir);
}

Component.prototype.targetDirectoryChanged = function()
{
    var dir = targetDirectoryPage.targetDirectory.text;
    if (installer.fileExists(dir) && installer.fileExists(dir + "/maintenancetool.exe")) {
        targetDirectoryPage.warning.setText("<p style=\"color: red\">Existing installation detected and will be overwritten.</p>");
    }
    else if (installer.fileExists(dir)) {
        targetDirectoryPage.warning.setText("<p style=\"color: red\">Installing in existing directory. It will be wiped on uninstallation.</p>");
    }
    else {
        targetDirectoryPage.warning.setText("");
    }
    installer.setValue("TargetDir", dir);
}

Component.prototype.componentSelectionPageEntered = function()
{
    var dir = installer.value("TargetDir");
    if (installer.fileExists(dir) && installer.fileExists(dir + "/maintenancetool.exe")) {
        installer.execute(dir + "/maintenancetool.exe", "--script=" + dir + "/scripts/auto_uninstall.qs");
    }
}

Пользовательский пакет виджетов целевых каталогов / Atlas4500 Tuner / meta / targetwidget.ui:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>TargetWidget</class>
 <widget class="QWidget" name="TargetWidget">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>491</width>
    <height>190</height>
   </rect>
  </property>
  <property name="sizePolicy">
   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
    <horstretch>0</horstretch>
    <verstretch>0</verstretch>
   </sizepolicy>
  </property>
  <property name="minimumSize">
   <size>
    <width>491</width>
    <height>190</height>
   </size>
  </property>
  <property name="windowTitle">
   <string>Form</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <widget class="QLabel" name="description">
     <property name="text">
      <string/>
     </property>
    </widget>
   </item>
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout">
     <item>
      <widget class="QLineEdit" name="targetDirectory">
       <property name="readOnly">
        <bool>true</bool>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QToolButton" name="targetChooser">
       <property name="sizePolicy">
        <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
         <horstretch>0</horstretch>
         <verstretch>0</verstretch>
        </sizepolicy>
       </property>
       <property name="minimumSize">
        <size>
         <width>0</width>
         <height>0</height>
        </size>
       </property>
       <property name="text">
        <string>...</string>
       </property>
      </widget>
     </item>
    </layout>
   </item>
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout_2">
     <property name="topMargin">
      <number>0</number>
     </property>
     <item>
      <widget class="QLabel" name="warning">
       <property name="enabled">
        <bool>true</bool>
       </property>
       <property name="text">
        <string>TextLabel</string>
       </property>
      </widget>
     </item>
     <item>
      <spacer name="horizontalSpacer">
       <property name="orientation">
        <enum>Qt::Horizontal</enum>
       </property>
       <property name="sizeHint" stdset="0">
        <size>
         <width>40</width>
         <height>20</height>
        </size>
       </property>
      </spacer>
     </item>
    </layout>
   </item>
   <item>
    <spacer name="verticalSpacer">
     <property name="orientation">
      <enum>Qt::Vertical</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>20</width>
       <height>122</height>
      </size>
     </property>
    </spacer>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>

packages / Atlas4500 Tuner / data / scripts / auto_uninstall.qs:

// Controller script to pass to the uninstaller to get it to run automatically.
// It's passed to the maintenance tool during installation if there is already an
// installation present with: <target dir>/maintenancetool.exe --script=<target dir>/scripts/auto_uninstall.qs.
// This is required so that the user doesn't have to see/deal with the uninstaller in the middle of
// an installation.

function Controller()
{
    gui.clickButton(buttons.NextButton);
    gui.clickButton(buttons.NextButton);

    installer.uninstallationFinished.connect(this, this.uninstallationFinished);
}

Controller.prototype.uninstallationFinished = function()
{
    gui.clickButton(buttons.NextButton);
}

Controller.prototype.FinishedPageCallback = function()
{
    gui.clickButton(buttons.FinishButton);
}

Идея здесь состоит в том, чтобы определить, есть ли в текущем каталоге инсталляция или нет, и, если это так, запустить инструмент обслуживания в этом каталоге с помощью скрипта контроллера, который просто щелкает по нему.

Обратите внимание, что я поместил скрипт контроллера в каталог скриптов, который является частью фактических данных компонента. Возможно, вы могли бы сделать что-то более чистое, если у вас есть несколько компонентов, но это то, что я использую.

Вы должны быть в состоянии скопировать эти файлы для себя и просто настроить строки, чтобы он работал.

Я нашел способ использовать решение рационального кодера без встроенного скрипта контроллера!

Все, что вам нужно сделать, это запустить инструмент обслуживания с purge командовать и отправить yesк стандартному вводу. Так что замените эту строку installer.execute(dir + "/maintenancetool.exe", "--script=" + dir + "/scripts/auto_uninstall.qs"); из скрипта компонента этой строкой installer.execute(dir + "/maintenancetool.exe", ["purge"], "yes");

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

Для пользователей Windows: убедитесь, что исходный установочный каталог не открывается терминалом. Если это так, каталог не будет удален, и установка завершится ошибкой. Каталог останется в ошибочном состоянии, и вы не сможете удалить его или получить к нему доступ, пока не перезапустите сеанс.

В дополнение к ответам рационального кодера и Скейрда , при использовании команды purge с версией 4 среды установки вы не можете просто пройтиYesна стандартный ввод, вы получитеoutput device is not associated with a terminalошибка, и инструмент обслуживания ничего не сделает. Вам необходимо использовать-cили--confirmаргумент.

Итак, замените:

      installer.execute(dir + "/maintenancetool.exe", "--script=" + dir + "/scripts/auto_uninstall.qs");

или

      installer.execute(dir + "/maintenancetool.exe", ["purge"], "yes");

с

      installer.execute(dir + "/maintenancetool.exe", ["purge", "-c"]);

Я сделал обход. Поместите его в конец вашего installscript.qs.

component.addOperation("AppendFile", "@TargetDir@/cleanup.bat", "ping 127.0.0.1 -n 4\r\ndel /F /Q maintenancetool.exe && for /F %%L in ('reg query HKEY_USERS /v /f \"@TargetDir@\\maintenancetool.exe\" /d /t REG_SZ /s /e') do reg query %%L /v DisplayName && reg delete %%L /f\r\ndel /F /Q cleanup.bat && exit\r\n")
component.addOperation("Execute", "workingdirectory=@TargetDir@", "cmd", "/C", "start", "/B", "Cleaning up", "cmd /C ping 127.0.0.1 -n 2 > nul && cleanup.bat > nul")

Это приведет к удалению файла maintenancetool.exe через 3 секунды, в результате чего установщик просто предупредит, что целевая папка не пуста, вместо того, чтобы отказаться от установки. Также он удаляет запись в реестре для удаления программы, чтобы она не накапливалась в программах добавления / удаления. Очевидно, что после удаления средства обслуживания вы больше не можете использовать его для таких вещей, как удаление или обновление, но я поддерживаю это только путем повторного запуска установщика. Инструмент обслуживания записывается только после завершения установки иcmd start cmdХакерство - сделать так, чтобы установщик не заметил, что какой-то этап все еще выполняется. Если у вас есть несколько дополнительных компонентов, вам может потребоваться увеличить задержку или сделать ее более надежной, чтобы проверить, работает ли что-то еще.

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

Есть несколько вещей, которые вам нужно сделать:

  • Чтобы пройти мимо TargetDirectoryPage, чего вы можете достичь, добавив этот код installer.setValue("RemoveTargetDir", false)

  • Пользовательский интерфейс (или окно сообщения), позволяющий запустить этот код. Этот интерфейс должен быть вставлен после TargetDirectoryPage. // you need to append .exe on the maintenance for windows installation installer.execute(installer.findPath(installer.value("MaintenanceToolName"), installer.value("TargetDir")));

Хорошо, этот ответ основан на последней версии установщика (3.2.2), я не уверен, работает ли он для более старых версий.

Чтобы перезаписать целевой каталог, вам просто нужно установить для RemoveTargetDir значение false в вашем файле конфигурации:

<RemoveTargetDir>false</RemoveTargetDir>

Это сработало.

Официальный документ объясняет этот элемент следующим образом:

Установите значение false, если целевой каталог не следует удалять при удалении.

Это немного сбивает с толку, если вы не знаете, как это используется:

bool TargetDirectoryPage::validatePage()
{
    m_textChangeTimer.stop();

    if (!isComplete())
        return false;

    if (!isVisible())
        return true;

    ///
    /// NOTICE HERE:
    /// If you set RemoveTargetDir to false, function return true here.
    const QString remove = packageManagerCore()->value(QLatin1String("RemoveTargetDir"));
    if (!QVariant(remove).toBool())
        return true;

    const QString targetDir = this->targetDir();
    const QDir dir(targetDir);
    // the directory exists and is empty...
    if (dir.exists() && dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty())
        return true;

    const QFileInfo fi(targetDir);
    if (fi.isDir()) {
        QString fileName = packageManagerCore()->settings().maintenanceToolName();
#if defined(Q_OS_MACOS)
        if (QInstaller::isInBundle(QCoreApplication::applicationDirPath()))
            fileName += QLatin1String(".app/Contents/MacOS/") + fileName;
#elif defined(Q_OS_WIN)
        fileName += QLatin1String(".exe");
#endif

        QFileInfo fi2(targetDir + QDir::separator() + fileName);
        ///
        /// AND NOTICE HERE:
        /// Do exists check here.
        if (fi2.exists()) {
            return failWithError(QLatin1String("TargetDirectoryInUse"), tr("The directory you selected already "
                "exists and contains an installation. Choose a different target for installation."));
        }

        return askQuestion(QLatin1String("OverwriteTargetDirectory"),
            tr("You have selected an existing, non-empty directory for installation.\nNote that it will be "
            "completely wiped on uninstallation of this application.\nIt is not advisable to install into "
            "this directory as installation might fail.\nDo you want to continue?"));
    } else if (fi.isFile() || fi.isSymLink()) {
        return failWithError(QLatin1String("WrongTargetDirectory"), tr("You have selected an existing file "
            "or symlink, please choose a different target for installation."));
    }
    return true;
}

Обратите внимание на комментарии "УВЕДОМЛЕНИЕ ЗДЕСЬ:".

Другие вопросы по тегам