qtqt-installer

Workaround for Qt Installer Framework not overwriting existing installation


This question is about version 2.0 of the Qt Installer Framework.

At this point, it is common knowledge for people using the Qt Installer Framework that, without customization, you simply can't overwrite an existing installation through your installer. This was apparently done to resolve some issues that occurred when this was done with the Qt framework.

However, for smaller, relatively simple projects, overwriting is perfectly fine and much more convenient than having to manually run the maintenance tool beforehand.

I am looking for a solution involving a custom UI + component script that adds a button to the target directory page that allows the user to either

  1. Remove the specified directory if it exists, or
  2. Run the maintenance tool in that directory.

It would be preferable to be able to run the maintenance tool in the target directory, having it automatically remove a given package, but I realize that that is asking for a little too much.

I have read answers to other questions on this site about solving the same problem, but none of the solutions work correctly. I would also like to mention that I have a component script up and running, but no custom UIs.


Solution

  • I finally found a workable solution.

    You need three things to pull this off:

    1. A component script,
    2. A custom UI for the target directory page, and
    3. A controller script that clicks through the uninstaller automatically.

    I will now list verbatim what is working for me (with my project specific stuff). My component is called 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>
    

    custom component script packages/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");
        }
    }
    

    Custom target directory widget packages/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);
    }
    

    The idea here is to detect if the current directory has an installation in it or not, and, if it does, run the maintenance tool in that directory with a controller script that just clicks through it.

    Note that I put the controller script in a scripts directory that is part of the actual component data. You could probably do something cleaner if you have multiple components, but this is what I am using.

    You should be able to copy these files for yourself and just tweak the strings to make it work.