Continuous deployment for Qt applications
This article shows you how to set up a continuous deployment pipeline to build and deploy Qt applications. It demonstrates this with template scripts on Travis CI and AppVeyor.
The templates are configuration files and build scripts compatible with Travis CI for macOS and Ubuntu and AppVeyor for Windows.
You can reference the build scripts that inspired this article in the Kryvo (https://github.com/adolby/Kryvo) and Dialogue (https://github.com/adolby/Dialogue) repositories.
Qt specifics
Installing Qt on your continuous deployment platform
Travis CI
Qt can be installed with package managers if you are willing to accept the Qt version available on each platform.
If you want to use a different Qt version, you’ll need to find a build of Qt for your operating system.
The Ubuntu template script uses my GitHub repo that provides unofficial Qt builds (https://github.com/adolby/qt-more-builds). The macOS build script acquires Qt with brew.
Other Options
-
You can build Qt (or install Qt from the official installer) on a matching OS and archive the files. You’ll then need to host the archive and download and unpack it in your build script. Finally, add the binaries to your source folder and add the binary path to your PATH variable.
-
Ben Lau details how to run the Qt Project installers on Linux with the Qt-CI project. It looks like a great alternative solution.
AppVeyor
AppVeyor has current versions of Qt installed on their build images, so you’ll just need to add the Qt binary path to your PATH variable.
Qt dependencies
Qt provides tools that copy dependency files for deployment on macOS and Windows.
The Qt documentation on deployment is a great resource to use when you’re setting the command line parameters up for macdeployqt and windeployqt. The parameters are similar between the tools, but note that the syntax is slightly different.
There is a third-party linuxdeployqt but I haven’t tried it yet.
QML apps
The deployment tools can also determine QML dependencies from your QML source files if you pass the directory containing the QML files in your project as a parameter.
To deploy a QML app on Linux, you’ll need to note your dependencies yourself or compare the output from macdeployqt or windeployqt.
The Dialogue repo shows how to copy the necessary files for deploying a QML project on Linux.
There is also a third-party linuxdeployqt but I haven’t tried it yet.
Linux packages
Travis CI runs Ubuntu 12.04 or Ubuntu 14.04; the latter is available by specifying dist: trusty in your .travis.yml configuration file. If you are developing your application on a different version of Ubuntu or another operating system, some software packages may not be available in the same version you developed your application with.
To fix this, you can create your own package repositories or build your application’s missing dependencies from source.
Templates
Below are example build config files for Travis CI and AppVeyor and build scripts.
The config file demonstrates deployment to GitHub Releases. There are other deployment targets available on Travis CI and AppVeyor.
The templates use the Git tag name as a version number, which allows you to differentiate between deployed builds with semantic versioning (or whatever versioning scheme you prefer).
Travis CI
The Travis CI config file specifies building and deployment on macOS and Ubuntu 14.04.
To get started on Travis CI you’ll need to create a GitHub (or other Git hosting provider) repository and then enable it on Travis CI.
To allow Travis to deploy to GitHub Releases, you’ll need to create a personal access token on GitHub. Next, you’ll use the Travis tool to encrypt (hash) it as a secure environment variable. Then copy the output over the secure key in the deployment section with your encrypted (hashed) personal access key.
This process is detailed under the Environment Variable section in the Travis CI documentation.
Alternatively, you could add the unencrypted personal access token as an environment variable through the Travis CI settings page for your repository.
Travis CI is configured to build on all branches and will only deploy on tagged commits.
.travis.yml
dist: trusty
sudo: required
git:
depth: 1
language: cpp
matrix:
include:
- os: osx
compiler: clang
osx_image: xcode8
- os: linux
compiler: gcc
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- gcc-6
- g++-6
script:
- if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then chmod +x build_macOS.sh ; fi
- if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then TAG_NAME=${TRAVIS_TAG} ./build_macOS.sh ; fi
- if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-6 100 ; fi
- if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-6 100 ; fi
- if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then chmod +x build_linux.sh ; fi
- if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then TAG_NAME=${TRAVIS_TAG} CXX="g++-6" CC="gcc-6" ./build_linux.sh ; fi
deploy:
- provider: releases
api_key:
secure: NDZ/RLvQzTWOkUnFhRqDFlBUgWJUtre5f12aBcolDMoIxyFfoZ/87GWXBk+OD4zYH7kQa0TY21Ap8zKJOpLW5vqbqPXtHW2cEHYez5l8FBxF1JpEUL0TLplbNbgjGGKkTatXYG9M0jCTKb8cXm3X7er3AAIKxJUhsLUdQDjR5SlJgqdShcOMuha1SXRuOhxV85Tp6ZEwXFnKeBQI9k4v1TyJMHY6bS6Sv8uMmpExVdznFgHNiDrdtLbqPyIyw4cJbOPCxlSmdynUkG7jvm0bHlMqzwsxVrWREL/QX0A1L5Orux+Wc187XCZunKCw7Eg05lesXySj3E3v0DoChjNuTIm7hBR6zCxHkHTgqdyfuiN1fKwLbZ2zHrhxIz206V/wddrFwYhmly243ayAua4ExJbQj+N1s47ttqQWUUJz/sOHorPsT9fG97SneyyEeGwtt/q7DKVpX/g4SA2UIIHxJxLlqw5TVpAatimhd09mg5HFt1i4yZExoBQSNQePCB7bsu5hvd2Wfs9l+3b10VAwbTsT1+n6oEHcjzt25nS7ekGuzSj7LYRUxbqeEdRzKWNGgQ16UEcldgAQlFPeqRcjVS5g+KQwIQwk8IPg5C8b5E4/r+Ig40O51aU2fXI33fMpJhhpQXoDc1id13G6vvuMMAKS/0O/jFP1184dPQ/hjiA=
file:
- build/macOS/clang/x86_64/release/YourApp_${TRAVIS_TAG}_macos.zip
overwrite: true
skip_cleanup: true
on:
tags: true
condition: $TRAVIS_OS_NAME = osx
- provider: releases
api_key:
secure: NDZ/RLvQzTWOkUnFhRqDFlBUgWJUtre5f12aBcolDMoIxyFfoZ/87GWXBk+OD4zYH7kQa0TY21Ap8zKJOpLW5vqbqPXtHW2cEHYez5l8FBxF1JpEUL0TLplbNbgjGGKkTatXYG9M0jCTKb8cXm3X7er3AAIKxJUhsLUdQDjR5SlJgqdShcOMuha1SXRuOhxV85Tp6ZEwXFnKeBQI9k4v1TyJMHY6bS6Sv8uMmpExVdznFgHNiDrdtLbqPyIyw4cJbOPCxlSmdynUkG7jvm0bHlMqzwsxVrWREL/QX0A1L5Orux+Wc187XCZunKCw7Eg05lesXySj3E3v0DoChjNuTIm7hBR6zCxHkHTgqdyfuiN1fKwLbZ2zHrhxIz206V/wddrFwYhmly243ayAua4ExJbQj+N1s47ttqQWUUJz/sOHorPsT9fG97SneyyEeGwtt/q7DKVpX/g4SA2UIIHxJxLlqw5TVpAatimhd09mg5HFt1i4yZExoBQSNQePCB7bsu5hvd2Wfs9l+3b10VAwbTsT1+n6oEHcjzt25nS7ekGuzSj7LYRUxbqeEdRzKWNGgQ16UEcldgAQlFPeqRcjVS5g+KQwIQwk8IPg5C8b5E4/r+Ig40O51aU2fXI33fMpJhhpQXoDc1id13G6vvuMMAKS/0O/jFP1184dPQ/hjiA=
file:
- build/linux/gcc/x86_64/release/YourApp_${TRAVIS_TAG}_linux_x86_64_portable.zip
- installer/linux/YourApp_${TRAVIS_TAG}_linux_x86_64_installer
overwrite: true
skip_cleanup: true
on:
tags: true
condition: $TRAVIS_OS_NAME = linux
notifications:
email:
recipients:
- andrewdolby@gmail.com
on_success: change
on_failure: change
macOS
This macOS script builds and run tests for CI, then packages the application as a dmg file archive for deployment. $TAG_NAME is a Travis CI-specific environment variable containing the tag name of the current build.
build_macOS.sh
#!/bin/bash
set -o errexit -o nounset
# Hold on to current directory
project_dir=$(pwd)
# Output macOS version
sw_vers
# Update platform
echo "Updating platform..."
brew update
# Install p7zip for packaging
brew install p7zip
# Install npm appdmg if you want to create custom dmg files with it
# npm install -g appdmg
# Install Qt
echo "Installing Qt..."
brew install qt
# Add Qt binaries to path
PATH="/usr/local/opt/qt/bin/:${PATH}"
# Build your app
echo "Building YourApp..."
cd "${project_dir}"
qmake -config release
make
# Build and run your tests here
# Package your app
echo "Packaging YourApp..."
cd "${project_dir}/build/macOS/clang/x86_64/release/"
# Remove build directories that you don't want to deploy
rm -rf moc
rm -rf obj
rm -rf qrc
echo "Creating dmg archive..."
macdeployqt YourApp.app -qmldir=../../../../../src -dmg
mv YourApp.dmg "YourApp_${TAG_NAME}.dmg"
# You can use the appdmg command line app to create your dmg file if
# you want to use a custom background and icon arrangement. I'm still
# working on this for my apps, myself. If you want to do this, you'll
# remove the -dmg option above.
# appdmg json-path YourApp_${TRAVIS_TAG}.dmg
# Copy other project files
cp "${project_dir}/README.md" "README.md"
cp "${project_dir}/LICENSE" "LICENSE"
cp "${project_dir}/Qt License" "Qt License"
echo "Packaging zip archive..."
7z a "YourApp_${TAG_NAME}_macos.zip" "YourApp_${TAG_NAME}.dmg" "README.md" "LICENSE" "Qt License"
echo "Done!"
exit 0
Ubuntu 14.04
This Ubuntu script builds and run tests for CI, then packages the application as a portable archive and creates an installer executable for deployment. $TAG_NAME is a Travis CI-specific environment variable containing the tag name of the current build.
build_linux.sh
#!/bin/bash
set -o errexit -o nounset
# Update platform
echo "Updating platform..."
# Install p7zip for packaging archive for deployment
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install p7zip-full
# Need to install chrpath to set up proper rpaths. This is necessary
# to allow Qt libraries to be visible to each other. Alternatively,
# you could use qt.conf, which wouldn't require chrpath.
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install chrpath
# Hold on to current directory
project_dir=$(pwd)
# Define your Qt install path for later use
qt_install_dir=/opt
# Install Qt
echo "Installing Qt..."
cd ${qt_install_dir}
echo "Downloading Qt files..."
sudo wget https://github.com/adolby/qt-more-builds/releases/download/5.7/qt-opensource-5.7.0-linux-x86_64.7z
echo "Extracting Qt files..."
sudo 7z x qt-opensource-5.7.0-linux-x86_64.7z &> /dev/null
# Install Qt Installer Framework
echo "Installing Qt Installer Framework..."
sudo wget https://github.com/adolby/qt-more-builds/releases/download/qt-ifw-2.0.3/qt-installer-framework-opensource-2.0.3-linux.7z
sudo 7z x qt-installer-framework-opensource-2.0.3-linux.7z &> /dev/null
# Add Qt binaries to path
echo "Adding Qt binaries to path..."
PATH="${qt_install_dir}/Qt/5.7/gcc_64/bin/:${qt_install_dir}/Qt/QtIFW2.0.3/bin/:${PATH}"
# Build YourApp
echo "Building YourApp..."
cd "${project_dir}"
# Output qmake version info to make sure we have the right install
# directory in the PATH variable
qmake -v
qmake -config release -spec linux-g++-64
make
# Build and run your tests here
# Package YourApp
echo "Packaging YourApp..."
cd "${project_dir}/build/linux/gcc/x86_64/release/YourApp/"
# Remove build directories that you don't want to deploy
rm -rf moc
rm -rf obj
rm -rf qrc
echo "Copying files for archival..."
# Copy ICU libraries
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libicui18n.so.56.1" "libicui18n.so.56"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libicuuc.so.56.1" "libicuuc.so.56"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libicudata.so.56.1" "libicudata.so.56"
# Copy YourApp's Qt dependencies, including QML dependencies if your
# app uses QML. If your app doesn't use QML, you'll probably need to
# include libQt5Widgets.so.5. You will need additional library
# dependency files if your app uses Qt features found in other
# modules. You can find out your app's library and QML dependencies by
# checking the Qt docs or by referencing the library and QML files
# that are copied by macdeployqt or windeployqt on macOS or Windows.
mkdir platforms
mkdir -p Qt/labs/
# You'll always need these libraries on Linux.
cp "${qt_install_dir}/Qt/5.7/gcc_64/plugins/platforms/libqxcb.so" "platforms/libqxcb.so"
cp "${qt_install_dir}/Qt/5.7/gcc_64/plugins/platforms/libqminimal.so" "platforms/libqminimal.so"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libQt5Core.so.5.7.0" "libQt5Core.so.5"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libQt5Gui.so.5.7.0" "libQt5Gui.so.5"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libQt5DBus.so.5.7.0" "libQt5DBus.so.5"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libQt5XcbQpa.so.5.7.0" "libQt5XcbQpa.so.5"
# You may or may not need these, depending on which Qt features you
# use
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libQt5Svg.so.5.7.0" "libQt5Svg.so.5"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libQt5Qml.so.5.7.0" "libQt5Qml.so.5"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libQt5Quick.so.5.7.0" "libQt5Quick.so.5"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libQt5QuickControls2.so.5.7.0" "libQt5QuickControls2.so.5"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libQt5QuickTemplates2.so.5.7.0" "libQt5QuickTemplates2.so.5"
cp "${qt_install_dir}/Qt/5.7/gcc_64/lib/libQt5Network.so.5.7.0" "libQt5Network.so.5"
cp -R "${qt_install_dir}/Qt/5.7/gcc_64/qml/Qt/labs/settings/" "Qt/labs/"
cp -R "${qt_install_dir}/Qt/5.7/gcc_64/qml/QtGraphicalEffects/" "."
cp -R "${qt_install_dir}/Qt/5.7/gcc_64/qml/QtQuick/" "."
cp -R "${qt_install_dir}/Qt/5.7/gcc_64/qml/QtQuick.2/" "."
# Copy other project files
cp "${project_dir}/README.md" "README.md"
cp "${project_dir}/LICENSE" "LICENSE"
cp "${project_dir}/Qt License" "Qt License"
# Use chrpath to set up rpaths for Qt's libraries so they can find
# each other
chrpath -r \$ORIGIN/.. platforms/libqxcb.so
chrpath -r \$ORIGIN/.. platforms/libqminimal.so
chrpath -r \$ORIGIN/../.. QtQuick/Controls.2/libqtquickcontrols2plugin.so
chrpath -r \$ORIGIN/../.. QtQuick/Templates.2/libqtquicktemplates2plugin.so
# Copy files to a folder with configuration for creating an installer
# with Qt Installer Framework
echo "Copying files for installer..."
cp -R * "${project_dir}/installer/linux/packages/com.yourappproject.yourapp/data/"
# Copy files to a portable archive inside the containing folder
# created earlier
echo "Packaging portable archive..."
cd ..
7z a YourApp_${TAG_NAME}_linux_x86_64_portable.zip YourApp
# Use the Qt Installer Framework to create an installer executable
echo "Creating installer..."
cd "${project_dir}/installer/linux/"
binarycreator --offline-only -c config/config.xml -p packages YourApp_${TAG_NAME}_linux_x86_64_installer
echo "Done!"
exit 0
AppVeyor
The AppVeyor config file specifies building and deployment on Windows.
To get started on AppVeyor you’ll need to create a GitHub (or other Git hosting provider) repository and then enable it on AppVeyor.
To allow Travis to deploy to GitHub Releases, you’ll need to create a personal access token on GitHub. Next, you’ll use the Encrypt data tool, which can be found on the accounts dropdown menu after signing in on AppVeyor, to encrypt (hash) it as a secure environment variable. Then copy the output over the secure key in the deployment section with your encrypted (hashed) personal access key.
This process is detailed under the Environment variables section in the AppVeyor documentation.
Alternatively, you could add the unencrypted personal access token as an environment variable through the AppVeyor settings page for your repository.
AppVeyor is configured to build on all branches and will only deploy on tagged commits.
You can build with the Visual C++ compiler from multiple Visual Studio versions or with MinGW. If you’re building with the Visual C++ compiler, you’ll need to call vcvarsall.bat with %PLATFORM% as the argument, where %PLATFORM% is an AppVeyor environment variable.
appveyor.yml
image: Visual Studio 2015
environment:
matrix:
- QT: C:\Qt\5.7\msvc2015_64
PLATFORM: amd64
COMPILER: msvc
VSVER: 14
clone_depth: 1
init:
- set TAG_NAME=%APPVEYOR_REPO_TAG_NAME%
build_script:
- call "build_windows.cmd"
artifacts:
- path: build\windows\msvc\x86_64\release\YourApp_$(appveyor_repo_tag_name)_windows_x86_64_portable.zip
name: portable
- path: installer\windows\x86_64\YourApp_$(appveyor_repo_tag_name)_windows_x86_64_installer.exe
name: installer
deploy:
- provider: GitHub
tag: $(appveyor_repo_tag_name)
release: $(appveyor_repo_tag_name)
description: $(appveyor_repo_tag_name)
auth_token:
secure: bOwrg0z7hv/7CnAQD2q+sf74q2vH40mWJLZYc8EzYvqkrXk9KYnd23rVCJ3Fsrqs
artifact: portable, installer
draft: false
prerelease: false
force_update: true
on:
appveyor_repo_tag: true
Windows
This Windows script builds and run tests for CI, then packages the application as a portable archive and creates an installer executable for deployment. %APPVEYOR_REPO_TAG_NAME% is an AppVeyor-specific environment variable containing the tag name of the current build.
build_windows.cmd
echo on
SET project_dir="%cd%"
echo Set up environment...
set PATH=%QT%\bin\;C:\Qt\Tools\QtCreator\bin\;C:\Qt\QtIFW2.0.1\bin\;%PATH%
call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %PLATFORM%
echo Building YourApp...
qmake -spec win32-msvc2015 CONFIG+=x86_64 CONFIG-=debug CONFIG+=release
nmake
echo Running tests...
echo Packaging...
cd %project_dir%\build\windows\msvc\x86_64\release\
windeployqt --qmldir ..\..\..\..\..\src\ YourApp\YourApp.exe
rd /s /q YourApp\moc\
rd /s /q YourApp\obj\
rd /s /q YourApp\qrc\
echo Copying project files for archival...
copy "%project_dir%\README.md" "YourApp\README.md"
copy "%project_dir%\LICENSE" "YourApp\LICENSE.txt"
copy "%project_dir%\Qt License" "YourApp\Qt License.txt"
echo Copying files for installer...
mkdir "%project_dir%\installer\windows\x86_64\packages\com.yourappproject.yourapp\data\"
robocopy YourApp\ "%project_dir%\installer\windows\x86_64\packages\com.yourappproject.yourapp\data" /E
echo Packaging portable archive...
7z a YourApp_%TAG_NAME%_windows_x86_64_portable.zip YourApp
echo Creating installer...
cd %project_dir%\installer\windows\x86_64\
binarycreator.exe --offline-only -c config\config.xml -p packages YourApp_%TAG_NAME%_windows_x86_64_installer.exe
Questions and comments
Please contact me (andrewdolby@gmail.com) or leave a comment if you have any questions or suggestions, and I’ll be happy to address them in this article. I’ll also be happy to help with problems you’re having with continuous integration/deployment.