background-shape
feature-image

Qt Series S01E01: GitHub Actions and Qt

Thinking of a project without a CI/CD is unimaginable, even more for a cross-platform application where we have to juggle between multiple platforms. CI/CD Continuous integration (CI) and continuous delivery (CD) offer several advantages:

  • Smaller code changes.
  • Faster release rate.
  • Better user satisfaction.
  • Easier maintenance.

For Qt-based projects that are cross-platform by design, a CI/CD pipeline will have several additional benefits:

  • No need to spin several machines for each OS.
  • Robust and reproducible builds for all targetted OS.
  • Easier and faster changes integration.

In the following, we will see how to set up an essential GitHub Actions CD to obtain binaries of our software for Windows, Linux, and macOS. In the second part, we will see how to produce Windows online and offline installer with the QtInstallerFrameWork.

Tools

To build our pipeline, we will use several tools. The main one is the GitHub Actions framework, Aqtinstall, a command-line installer for Qt, and qmake to compile the Qt program.

Windows

First, we create a folder “.github/workflows” inside the git repository of our project. Then create an action named “windows.yml”.

name: Build Windows

on:
  push:
    branches: [master]
Photo by Vidar Nordli-Mathisen on Unsplash
jobs:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ilammy/msvc-dev-cmd@v1
      - uses: actions/setup-python@v2
        with:
          python-version: '3.8'
      - name: install qt6
        run: |
          pip install aqtinstall
          python3 -m aqt install-qt -m qtwebengine qtwebchannel qtpositioning -O ${{ github.workspace }}/Qt/ windows desktop 6.2.0 win64_msvc2019_64
          echo "${{ github.workspace }}/Qt/6.2.0/msvc2019_64/bin/" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append          
      - name: build
        shell: cmd
        run: |
          cd src/
          qmake6 Program.pro -spec win32-msvc
          nmake release
          nmake clean
          cd release
          windeployqt Program.exe --release --compiler-runtime
          copy ..\assets\icon.ico .
          copy C:\Windows\System32\concrt140.dll . 
          copy C:\Windows\System32\vccorlib140.dll .
          copy C:\Windows\System32\msvcp140.dll .
          copy C:\Windows\System32\vcruntime140.dll .
          7z a C:\Program.zip *          
      - name: Windows artefact
        uses: actions/upload-artifact@v1
        with:
          name: WindowsBuild
          path: C:\Program.zip

First, we name the Actions (Build Windows) and select when it is triggered, in this case, at each commit on the master branch. Then we create a job and select the GitHub runner, the latest Windows available.

actions/checkout@v2 will clone the repository, and ilammy/msvc-dev-cmd@v1 will set up an environment where we can use nmake. Finally, actions/setup-python@v2 will set up a Python environment.

We use aqtinstall to install Qt on the machine. We install several modules (qtwebengine, qtwebchannel, qtpositioning) alongside the Qt version 6.2.0 for MSVC 2019 64 bits. Finally, we set the path to the Qt installation using the $env:GITHUB_PATH variable.

As usual, we build the program using qmake and nmake.

At this step, we have produced a single binary file. To deploy the application, we must copy the necessary libraries for the execution. We use the Qt tool name windeployqt. Be aware that this tool will not copy external (non-Qt) libraries; this step must be performed manually. Additionally, we manually copy several dll that must be redistributed to ensure the standalone execution (see https://doc.qt.io/qt-5/windows-deployment.html section Creating the Application Package).

Finally, we use actions/upload-artifact@v1 to upload the resulting folder as an Action artifact that we will be able to download.

MacOs

macos.yml:

name: Build MacOs

on:
  push:
    branches: [master]

jobs:
    runs-on: macos-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 -m qtwebengine qtwebchannel qtpositioning -O ${{ github.workspace }}/Qt/ mac desktop 6.2.0
          echo ${{ github.workspace }}/Qt/6.2.0/macos/bin/ >> $GITHUB_PATH          
      - name: build
        run: |
          qmake6 src/Program.pro
          make
          make clean
          cd build/
          macdeployqt Program.app -always-overwrite
          wget https://raw.githubusercontent.com/arl/macdeployqtfix/master/macdeployqtfix.py
          python2.7 macdeployqtfix.py Program.app/Contents/MacOS/Program ../../Qt/6.2.0/
          hdiutil create -volname Program -srcfolder Program.app -ov -format UDZO Program.dmg          

      - name: Mac artefact
        uses: actions/upload-artifact@v1
        with:
          name: MacOsBuild
          path: ./build/Program.dmg

The procedure for MacOs is similar. At the end of the compilation, we use macdeployqt to copy the necessary libraries in the bundle and set the correct rpaths. For external dependencies, it is possible that macdeployqt doesn’t finish the job. To correct that, we use macdeployqtfix. Finally, we package the App as a dmg file.

Linux

There are several ways to release packages for Linux. In this example, we use the AppImage format that will be compatible with all Linux distributions.

linux.yml:

name: Build AppImage

on:
  push:
    branches: [master]

jobs:
  job_1:
    runs-on: ubuntu-18.04
    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 -m qtwebengine qtwebchannel qtpositioning -O ${{ github.workspace }}/Qt/ linux desktop 6.2.0
          echo ${{ github.workspace }}/Qt/6.2.0/gcc_64/bin/ >> $GITHUB_PATH          
      - name: build
        run: |
          qmake6 src/Program.pro
          make
          make clean          
      - name: appimage
        run: |
          cd build
          wget -O deploy.AppImage https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage
          chmod +x deploy.AppImage
          export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{ github.workspace }}/Qt/6.2.0/gcc_64/lib/
          ./deploy.AppImage Program -appimage -no-translations -bundle-non-qt-libs
          mv Program*.AppImage Program-x86_64.AppImage          
      - name: Linux artefact
        uses: actions/upload-artifact@v1
        with:
          name: LinuxBuild
          path: ./build/Program-x86_64.AppImage

As for the other, the procedure is the same. We use linuxdeployqt to create the AppImage file at the end before uploading the file as an action artifact.

Conclusion

Setting up a CI/CD that will build Qt program on Linux, Windows, and macOS is not very complicated and will result in considerable time savings. In the next part of the Qt series, we will see how to create executable (offline and online) for Windows with the Qt Installer Framework and GitHub Actions.