background-shape
feature-image

Qt Series S01E02: QtInstallerFramework & GitHub Actions

Previously in Qt Series, we have seen how to combine the cross-platform power of Qt with the CI/CD Github Actions framework to produce binaries for Windows, macOS, and Linux. In this episode, we will see how to produce an installer to deliver our program to the customers. It is the primary distribution mode on Windows, but it can also be made for MacOs and Linux.

The Qt Installer Framework supports two types of installers: Offline: a simple executable that installs the binary and its dependencies. Online: an executable that will fetch a web server, compare the program’s version with the current one, and install the relevant version from the webserver. This version allows simple updates for the user.

In this episode, we will construct a simple installer with only one package and a custom component. The installer will perform three tasks: License acceptation. Program installation to a specified location. Program and documentation opening.

Architecture

First, we need to create the necessary folders and configuration files that will follow this architecture (for multiple packages, encapsulate each package in a subfolder inside the packages directory):

ProgramInstaller 
│
└───packages
│   │
│   └───Program
│       │
│       └───data
│       │   │ data.7z 
│       │        
│       └───meta
│           │ license.txt
│           │ installscript.qs
│           │ package.xml
│           │ readmecheckboxform.ui
│
└───config
    │   config.xml

License.txt

The software license that the user needs to accept to install the program.

Install.qss

The following two functions will display a custom component that will open the program and the documentation if the user checks the boxes. The install.qss file define the component shown during this installation process. In the Component.prototype.createOperations function, we define two shortcuts, one to the program executable and the second to the Qt MaintenanceTool that will appear in the start menu.

function Component()
{
    installer.installationFinished.connect(this, Component.prototype.installationFinishedPageIsShown);
    installer.finishButtonClicked.connect(this, Component.prototype.installationFinished);
}

Component.prototype.createOperations = function()
{
    component.createOperations();
    if (systemInfo.productType === "windows") {
        component.addOperation("CreateShortcut", "@TargetDir@/Program.exe", "@StartMenuDir@/Program.lnk",
            "workingDirectory=@TargetDir@","iconPath=@TargetDir@/icon.ico", "description=Program Icon");
        component.addOperation("CreateShortcut", "@TargetDir@/maintenancetool.exe", "@StartMenuDir@/ProgramUpdater.lnk",
            "workingDirectory=@TargetDir@", "description=Program Updater");
        installer.setDefaultPageVisible(QInstaller.LicenseCheck, false);
    }
}

Component.prototype.installationFinishedPageIsShown = function()
{
    try {
        if (installer.isInstaller() && installer.status == QInstaller.Success) {
            installer.addWizardPageItem( component, "ReadMeCheckBoxForm", QInstaller.InstallationFinished );
        }
    } catch(e) {
        console.log(e);
    }
}

Component.prototype.installationFinished = function()
{
    try {
        if (installer.isInstaller() && installer.status == QInstaller.Success) {
            var isReadMeCheckBoxChecked = component.userInterface( "ReadMeCheckBoxForm" ).readMeCheckBox.checked;
            if (isReadMeCheckBoxChecked) {
                QDesktopServices.openUrl("https://siteofdoc.html");
            }
            var isStartCheckBoxChecked = component.userInterface( "ReadMeCheckBoxForm" ).startCheckBox.checked;
            if (isStartCheckBoxChecked) {
                installer.executeDetached("@TargetDir@/Program.exe");
            }
        }
    } catch(e) {
        console.log(e);
    }
}

Package.xml

The package.xml file is the configuration file of the only package contained in our installer.

<?xml version="1.0" encoding="UTF-8"?>
<Package>
    <DisplayName>Program package</DisplayName>
    <Description>Program package</Description>
    <Version>0.0.0</Version>
    <ReleaseDate></ReleaseDate>
    <Licenses>
    <License name="GNU GENERAL PUBLIC LICENSE" file="license.txt" />
    </Licenses>
    <Default>true</Default>
    <Script>installscript.qs</Script>
    <UserInterfaces>
        <UserInterface>readmecheckboxform.ui</UserInterface>
    </UserInterfaces>
</Package>

Readmecheckboxform.ui

This file is a declarative user interface for our custom component that will open the program and the documentation at the end of the install process if the user checks the boxes.

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>ReadMeCheckBoxForm</class>
 <widget class="QWidget" name="ReadMeCheckBoxForm">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>412</width>
    <height>179</height>
   </rect>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <property name="leftMargin">
    <number>0</number>
   </property>
   <property name="topMargin">
    <number>0</number>
   </property>
   <property name="rightMargin">
    <number>0</number>
   </property>
   <property name="bottomMargin">
    <number>0</number>
   </property>
   <item>
    <widget class="QCheckBox" name="readMeCheckBox">
     <property name="text">
      <string>Open the User manual</string>
     </property>
     <property name="checked">
      <bool>true</bool>
     </property>
     <property name="tristate">
      <bool>false</bool>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QCheckBox" name="startCheckBox">
     <property name="text">
      <string>Start Program</string>
     </property>
     <property name="checked">
      <bool>true</bool>
     </property>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>

Data.7z

The data.7z archive will contain the binary and dependencies of the package that will be unpacked in the installation folder.

Config.xml

The config.xml is the configuration file for the installer. If you want an online installer, it is necessary to add an URL accessible by the installer in the tag. We will see in the next step what to add at the endpoint of this URL.

<?xml version="1.0" encoding="UTF-8"?>
<Installer>
    <Name>Program</Name>
    <Version>0.0.0</Version>
    <Title>Program Installer</Title>
    <Publisher>MyCompany</Publisher>
    <StartMenuDir>Program</StartMenuDir>
    <TargetDir>@HomeDir@/Program Files (x86)/Program</TargetDir>
    <RemoteRepositories>
    <Repository>
        <Url>url to remote repo</Url>
    </Repository>
</RemoteRepositories>
</Installer>

GitHub Actions

We will now automate the creation of this installer by using GitHub Actions and by reusing concepts we saw in episode 1 of the Qt Series.

As usual, we create a new file .github/workflows/installer.yml.

│.github
│
└───workflows
│   │   installer.yml
│
ProgramInstaller 
│
└───packages
│   │
│   └───Program
│       │
│       └───data
│       │   │ data.7z 
│       │        
│       └───meta
│           │ license.txt
│           │ installscript.qs
│           │ package.xml
│           │ readmecheckboxform.ui
│
└───config
    │   config.xml

installer.yml:

name: Continous Builds

on:
  push:
    branches: [master]

jobs:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
        with:
          python-version: '3.8'
      - name: install qt6
        run: |
          pip install aqtinstall
          python3 -m aqt install-qt -O ${{ github.workspace }}/Qt/ windows desktop 6.2.0 win64_msvc2019_64
          python3 -m aqt install-tool -O ${{ github.workspace }}/Qt/ windows desktop tools_ifw
          echo "${{ github.workspace }}/Qt/6.2.0/msvc2019_64/bin/" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
          echo "${{ github.workspace }}/Qt/Tools/QtInstallerFramework/4.1/bin/" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append          
      - name: build
        shell: cmd
        run: |
          repogen.exe -p .\ProgramInstaller\packages OnlineRepository
          binarycreator.exe -p .\ProgramInstaller\packages -c .\Program\config\config.xml ProgramInstaller          
      - name: Windows artefact
        uses: actions/upload-artifact@v1
        with:
          name: ProgramInstaller
          path: ./ProgramInstaller.exe
      - name: Windows artefact
        uses: actions/upload-artifact@v1
        with:
          name: OnlineRepository
          path: ./OnlineRepository

The first steps of the Actions are similar to what we saw in S01E01. An additional step is the installation of the Qt Installer Framework using aqtinstall with the command install-tool.

We use repogen.exe to generate the online repository that we have to place on a webserver at the endpoint of the URL indicated in the config.xml file. Then we use binarycreator to create the installer. It is possible to create an offline-only installer by adding - offline-only parameter and an online-only installer that does not contain any component and needs an internet connection by adding the - online-only parameter. The installer will, by default, include all the components and can access the online repository if a URL is specified. As a result, it can be installed with and without an internet connection.

Conclusion

Automatically producing offline and online installers using the Qt Installer Framework and GitHub Actions is easy and will simplify the delivery process of our application to the customer. In the next episode, we will make a detailed tour of the online repository that we created with the online installer. What to do with it? How to manage it? How can customers upgrade the Program?