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.
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
The software license that the user needs to accept to install the program.
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);
}
}
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>
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>
The data.7z archive will contain the binary and dependencies of the package that will be unpacked in the installation folder.
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>
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.
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?