GitHub Actions is a truly nice tool. Since I tried it the first time, I can no
longer do without it… and it’s still only available in public beta!
However, it has some shortcomings, both in terms of packages and documentation,
which make it difficult to use in some cases. This is because it’s young, I
don’t blame the tool but we still have to face the problem.
With these notes I hope to help another me who’s about to compile a Qt software and create an installer on the new service offered by GitHub.
Introduction
Long story short, this post is nothing but a set of notes about compiling a Qt project based on cmake and creating an installer via IFW (or rather CMakeIFW) on the Windows platform.
Why Windows? Mainly because Windows was the one that gave me the most trouble in
many ways. If you understand how to do it on this platform, you understand how
to do it on all the others.
Also, keep in mind that on Linux and MacOS you can install the Qt libraries via
official channels (eg apt
), even if going through the installer frees you
from the constraints on the packaging of the specific distribution. In general,
I would recommend the use of the official installer rather than the system
packages and, in this case, these notes will help even on those platforms.
I don’t pretend to have found the smartest nor best solution ever, but at least
I’ve the CI up and running now and it creates an installer whenever I cut a new
tag. The installer is then published directly in the GitHub releases, where my
users can safely download it having access to the repository.
Not bad at least as a starting point.
GitHub Actions greets Qt, CMake and IFW
With this post I’ve also published (or rather, I made public) a new
repository on GitHub (which I will
NOT update over time, in case there were doubts).
It’s a template project that can be used to bootstrap a CMake/Qt based
software, if you want to compile it using GH Actions. At the very least, it can
serve as an inspiration.
I apologize if the project may seem imperfect or if it contains apparently
superfluous parts. I extracted the content from a real world software and tried
to clean it of everything that wasn’t necessary.
Unfortunately (or fortunately, it depends on the point of view), the original
repository is private and contains code that I cannot publish because I’m under
contract. Therefore it wasn’t possible to use it as an example, although I would
have liked to do it.
I hope I haven’t made too many mistakes in preparing a minimal repository that can make clear what I’m about to tell.
Headless Installer
Unfortunately, the Qt installer doesn’t have a headless mode. We must find a
workaround to face this limitation if we want to run it silently.
It’s not that hard actually. In fact, the Qt installer is based on the Qt
Installer Framework (also known as
Qt IFW) and it’s therefore
scriptable.
In short, when you run the installer, you can pass a file containing a script
that will simulate the user and his/her clicks on the various Ok, Next,
Accept buttons and so on.
This becomes a little more difficult when it comes to selecting the components
you want to install, but not so hard.
This is the
documentation to follow if you want to write your own custom script.
At the end of the day, your file will contain a Controller
that exposes a
bunch of functions that will be called by the installer itself. As an example:
Controller.prototype.WelcomePageCallback = function() {
gui.clickButton(buttons.NextButton, 5000);
}
This helps to simulate a click on the Next button with a delay of 5 seconds,
nothing less and nothing more. It’s not the user but behaves as if it were.
The delay is there because the welcome page takes some time to contact the
server and only at the end will enable the button for the user, so we worry
about simulating also the wait.
Another interesting example is the callback for the component selection page:
Controller.prototype.ComponentSelectionPageCallback = function() {
var selection = gui.pageWidgetByObjectName("ComponentSelectionPage");
gui.findChild(selection, "Latest releases").checked = true;
gui.findChild(selection, "FetchCategoryButton").click();
var widget = gui.currentPageWidget();
widget.deselectAll();
widget.selectComponent("qt.qt5.5131.win64_msvc2017_64");
widget.selectComponent("qt.tools.ifw.31");
widget.selectComponent("qt.tools.openssl");
widget.deselectComponent("qt.license");
widget.deselectComponent("qt.installer");
gui.clickButton(buttons.NextButton);
}
In our case, we are interested in installing the latest release from Qt and
therefore we have to check the right box in the category section. Once this is
done, a button will be pressed (from script, of course) that will force the
refresh of the list of available components. Then, from the latter we will
select and install the components of interest and press Next.
Sounds simple, doesn’t it?
Sadly, the (little) documentation I found online isn’t up to date and writing a
complete script can be frustrating. The good news is that it can easily be done
locally, with the installer for our system.
To facilitate the task, the repository associated with this post contains also a
script that is
complete and working when I’m writing this. You’ll only need to update it when
necessary, from now on.
CMake & CPack
As for CMake
, there’s not much to say. If you know its syntax, nothing special
is required for compilation under Windows and then via GH Actions.
The same isn’t true for CPack
though. This isn’t due to GH Actions however,
but rather to Qt in general and Qt IFW in particular.
When we prepare an installer, we don’t want to install only our executable.
Instead, we need to deploy also the set of libraries required to make it run.
To do that, the windeployqt
executable comes in handy:
windeployqt
takes an.exe
file or a directory that contains an.exe
file as an argument, and scans the executable for dependencies.
The only thing we have to do is to run it under the hood by means of CPack
,
then install (as in CPack
command install
) what it returns:
set(CPACK_GENERATOR IFW)
include(CPack REQUIRED)
include(CPackIFW REQUIRED)
# ...
if(CMAKE_BUILD_TYPE MATCHES Release)
set(BINARIES_TYPE --release)
else()
set(BINARIES_TYPE --debug)
endif()
add_custom_command(
TARGET gh-greets-qt POST_BUILD
COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/windeployqt_stuff
COMMAND $ENV{QTDIR}/bin/windeployqt.exe ${BINARIES_TYPE} --compiler-runtime --dir ${CMAKE_BINARY_DIR}/windeployqt_stuff $<TARGET_FILE:gh-greets-qt>
)
install(
DIRECTORY ${CMAKE_BINARY_DIR}/windeployqt_stuff/
DESTINATION gh-greets-qt
COMPONENT gh-greets-qt_component
)
This is a simplified excerpt from the
CMakeList.txt
file of the accompanying project. For the sake of completeness, I must say that
I’ve used the
CMake IFW Generator for
my purposes. Here is a quote from its documentation:
CPack IFW generator prepares project installation and generates configuration and meta information for Qt IFW tools.
It’s not strictly necessary but it makes life easier when you want to make Qt
IFW and CMake work together.
It’s quite used and pretty solid, in the past I’ve also had the chance to
contact the author to help him correct some bugs, a person who proved to be very
helpful and determined to improve this module more and more. So, I find it worth
a try.
That said, in the snippet above we’re literally asking windeployqt
to fill the
windeployqt_stuff
directory with all is needed to make our executable run
correctly. We don’t care much of what it will put in this directory because we
trust the tool. However, something in particular deserves a special mention.
From the previous section, you saw that we installed the
qt.qt5.5131.win64_msvc2017_64
component. It will result in windeployqt
putting in the bundle the vc_redist.x64
executable.
This is particulary important and brings us directly to the next section and to
yet another script. However, this time the purpose is to extend our installer
rather than turning the Qt one into a non-interactive tool.
The Component and the Visual C++ Redistributable
By using CMake IFW we can easily setup one or more Qt IFW
Component
s in the
form of some scripts to use to extend an installer. Usually, these scripts are
aimed to create menu shortcuts or things like that:
if(systemInfo.productType === "windows") {
// ...
component.addOperation(
"CreateShortcut",
"@TargetDir@/gh-greets-qt/gh-greets-qt.exe",
"@StartMenuDir@/Hi-Qt.lnk",
"workingDirectory=@TargetDir@"
);
}
However, we can use the same tool for everything. In our case, we want to use it to execute the Visual C++ Redistributable executable so as to install what is needed to run our software on the user system. In fact, it will install runtime components required by the applications compiled with Microsoft Visual Studio, that is exaclty our case:
if(systemInfo.productType === "windows") {
component.addElevatedOperation("Execute", "{0,3010,1638,5100}", "@TargetDir@/gh-greets-qt/vc_redist.x64.exe", "/quiet", "/norestart");
// ...
}
Unfortunately, we cannot do run this executable as a normal user. That’s why
this time we use addElevatedOperation
rather than addOperation
(refer to the
official documentation for Qt IFW for further details on these functions).
What’s the difference? Long story short, Windows will warn the user that we are
going to do something risky and therefore we need higher privileges to proceed.
If you’re a Windows user, you know what I’m talking about: that’s the Ok
button you press usually between an uff and a damn it, again?.
As a side note, you’ll not need to take this step if you decide to compile your
software using mingw
or any other alternative. However, it was one of the
steps I spent the most time on and I found it interesting to mention this
explicitly rather than hurry up with an you can look into it and figure it out
for yourself.
GitHub Workflow
Finally, it’s time also for GH Actions. This is the simplest part in a sense, so simple and user-friendly this tool is.
I didn’t spend much time to make the example project configuration as flexible
as possible. Rather, I tried to write it so that it was simple to understand and
to modify. At the very least It case serve as a basis for your project.
I don’t know how confident you are with the syntax of this tool, but I think
it’s simple enough to be understood in large part at first glance:
jobs:
windows:
runs-on: windows-2019
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Prepare
working-directory: build
run: |
curl -vLo qt-unified-windows-x86-online.exe http://download.qt.io/official_releases/online_installers/qt-unified-windows-x86-online.exe
qt-unified-windows-x86-online.exe --verbose --script ..\ci\qt.qs
- name: Configure
working-directory: build
run: cmake -DCPACK_IFW_ROOT=Qt/Tools/QtInstallerFramework/3.1 -DCMAKE_BUILD_TYPE=Release -G"Visual Studio 16 2019" ..
- name: Compile
working-directory: build
run: cmake --build . --config Release -j 4
- name: Package
working-directory: build
run: cmake --build . --config Release --target package
This is a simplified exceprt from the
build.yml
file of the accompanying project.
We have only one job, the goal of which is to compile our project for Windows
and prepare an installer.
The first step is obviously the checkout of the project. Next, the Qt installer
is downloaded and executed in non-interactive mode.
Do you remember the script mentioned above? This is where it comes into play and
does its dirty work.
The third step executes the configuration of the project, which can be
summarized as launches CMake
and that’s all. Finally, we build the
executable and create the package.
As you can see, much of what is needed to have everything up and running is
contained in the files mentioned in the previous sections. Therefore, our
workflow does nothing more than download the installer and launch the programs
we need to get the job done.
However, there is a question that may arise now: once the package is created,
how is it retrieved?
It would be great if our installer was published directly between the releases
available in the GitHub project.
In this we are helped by the fact that anyone can write an action. There are
already dozens of them on the web and they exist for every taste, without
necessarily having to reinvent the wheel every time.
In the specific case, for the project related to this post, I used
this action available directly
on GitHub. Its goal is simple:
GitHub Action for creating GitHub Releases
And using it is just as simple, fortunately:
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: build/gh-greets-qt_installer.exe
env:
GITHUB_TOKEN: $
The way I configured it and thanks to the tools provided by GitHub, this runs
only when a tag is created instead of for each push.
When this happens, I’ll find a new release with the gh-greets-qt_installer.exe
file published directly online, without having to worry anymore. Nothing easier
and definitely convenient.
Conclusion
All in all, compiling your project and creating a Windows installer with IFW via
GH Actions wasn’t all that difficult. It’s mainly a matter of putting the pieces
together from previous experiences.
Unfortunately, however, if you have no experience at all with this kind of
tools, I admit that it can be a little tricky.
For this reason, I hope that these notes can help those users who are taking the first steps in this direction. Maybe this post won’t tell you everything but I’m sure it will tell you enough to make you create your workflow.
Let me know that it helped
I hope you enjoyed what you’ve read so far.
If you liked this post and want to say thanks, consider to star the GitHub project that hosts this blog. It’s the only way you have to let me know that you appreciate my work.
Thanks.