Having learned the practice of CI/CD, I decided to apply this technique to continuously deliver some of my projects starting with a recent NiceGUI app – File Copier.
Initially, I went to Github Marketplace and searched for pre-build PyInstaller
actions. Because I’m packaging for the Windows platform I ended up selecting PyInstaller Windows which is the most popular action in the category at the time of writing.
- name: PyInstaller Windows
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: src
The above code snippet shows how easy it is to integrate this action into the workflow. However, I encountered three problems with this approach:
- the first one is architectural – despite being Windows-oriented, this action runs inside a Linux (Ubuntu) container and uses Wine for cross-compilation. The reason for this is that PyInstaller currently does not cross-compile executables meaning that it cannot produce
.exe
files on Linux, therefore there is the need for a translation layer – Wine. Since 2021, Github gives you the ability to run workflows inside a Windows container which reduces the complexity and basically makes the aforementioned action obsolete. - the second problem is related to NiceGUI and the way it needs to be discovered by PyInstaller. It requires the use of an intermediary step – running a
build.py
file by the Python interpreter. SincePyInstaller Windows
abstracts away the run steps, there is no way to properly configure NiceGUI for PyInstaller. - the third one is also architectural and requires generating a
.spec
file on a local machine before committing it into a remote repository for later use in an actual workflow, which in my opinion, defeats the purpose of CI/CD.
With that said, there was the need for an alternative approach. Below is what I was able to come up with.
name: Python application
on:
push:
tags:
- v*
permissions:
contents: write
jobs:
build:
runs-on: windows-2019
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
# python-version: "3.8"
# python-version: "3.9"
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt - name: PyInstaller
run: |
python build.py
- name: Release
uses: technote-space/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: ./dist/file-copier.exe
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
First, we set up the trigger – a push event on the tags which help us mark a commit for release. We can add tags with the folloing command: git tag -a v1.0 -m "Release version 1.0"
. Later when we push changes to the main
branch we also push them on the tag: git push origin main v1.0
which activates the release action.
Second, we set up write access to the repository for uploading a compiled executable to releases. Then, we specify the container for the jobs to run inside of – in this case windows-2019
. Now onto the actual workflow:
- we set up Python 3.10 environment and install our project requirements
- we run the
build.py
file as documented here but with minor modifications. First is the “–name” that specifies the filename of our future executable – in my casefile-copier
.exe. Second is the--onefile
flag. Currently, it is not possible to correctly packagepywebview
with PyInstaller (which is whatnicegui
internally depends on) on Github Actions with the--onedir
flag due to a regression in another package –pythonnet
. One comment proposed to downgradepythonnet
from version3
to version2.5.2
, however because there is no binary wheel available for version2.5.2
for Python 3.10, building it from source inside Github Actions results in an error due to a missing compiler. One possible way to resolve this error is to try older versions of Python – 3.9 or 3.8. Another problem I faced is that executables built with the--onefile
flag may false-trigger an antivirus on the end-user machine, as--onefile
executables unpack themselves on each run which is similar to how many malicious programs behave. In this case, the antivirus can delete the executable. One can instruct the user to restore the file from quarantine and assure him or her of its safety. As I said, one can try different Python versions to try and downgrade thepythonnet
package and test the result.
Update: While it is possible to downgrade and build the pythonnet
package with Python 3.8 and 3.9 in a Windows container on Github Actions, running the final executable still results in an exception.
import os
import subprocess
from pathlib import Path
import nicegui
= [
cmd "python",
"-m",
"PyInstaller",
"--noconfirm",
"main.py", # your main file with ui.run()
#
"--name",
"file-copier", # name of your app
#
# "--onedir",
"--onefile",
#
"--windowed", # prevent console appearing, only use with ui.run(native=True, ...)
#
"--add-data",
f"{Path(nicegui.__file__).parent}{os.pathsep}nicegui",
] subprocess.call(cmd)
- finally, we set up the release action
technote-space/action-gh-release@v2
and point it to the newly built executable./dist/file-copier.exe
After all these steps are done with, we can see the v1.0 release on the repository page.