Automatically compiling PySide UI and resource files
The common workflow when working with Pyside is to build the UI with PySide Designer which outputs an xml file (.ui`). This file is then fed into pyside6-uic` to generate the python code that actually the window.
Resource files work in a similar manner. A resource file describes resources (e.g. fonts and images) that should be included into the application. These are described in an xml file (.qrc`) and compiled into Python by pyside6-rcc.
Keeping the xml and compiled files synced by hand is tedious. A common option is to use a makefile to automatically re-generate the python files when the xml files are updated.
A Simple makefile
Here is a simple Makefile to compile your .ui and .qrc` files
# Find all .ui files and generate corresponding .py target names
UI_FILES := $(shell find src/ -name "*.ui")
UI_PY_FILES := $(patsubst %.ui,$(dir %)ui_$(notdir $(basename %)).py,$(UI_FILES))
# QRC file and target
QRC_FILE := rcc/resources.qrc
QRC_PY_FILE := src/quinspect/resources.py
# Default target
all: compile-ui-files compile-qrc-files
# Convenience targets
compile-ui-files: $(UI_PY_FILES)
compile-qrc-files: $(QRC_PY_FILE)
# Rule to compile .ui files to .py files
src/%/ui_%.py: src/%/%.ui
@echo "Compiling $< ..."
.venv/bin/pyside6-uic --output $@ $<
# Rule to compile .qrc file to .py file
$(QRC_PY_FILE): $(QRC_FILE)
@echo "Compiling $< ..."
.venv/bin/pyside6-rcc --output $@ $<
A Simple Justfile
Makefiles may be powerful, but the syntax can be hard to grasp (and remember). For simple tasks, I like to use just <https://just.systems/>`_, a simple task runner.
just is a handy way to save and run project-specific commands. It bills itself as a command runner, not a build system. The main difference is that instead of tracking dependencies between files, it tracks dependencies between tasks.
It assumes that the tasks themselves are smart enough to avoid doing redundant work.
The big difference between the Justfile and the Makefile is that make` will only run a rule if the input is newer than the output. With just`, rules are always run, so we need to do this ourselves.
Here is our Justfile:
default:
just --list --justfile {{ justfile() }}
# Compile .ui files if the generated code is out-of-date or missing
[group("Generators")]
compile-ui-files:
#!/bin/bash
for ui_file in $(find src/ -name "*.ui"); do
ui_file_name=$(basename $ui_file)
py_file="$(dirname $ui_file)/ui_${ui_file_name/%.ui/.py}"
if [[ ! -f $py_file || $ui_file -nt $py_file ]]; then
echo "Compiling "$ui_file" ..."
.venv/bin/pyside6-uic --output $py_file $ui_file
fi
done
# Compile .rc files if the generated code is out-of-date or missing
[group("Generators")]
compile-qrc-files:
#!/bin/bash
rc_file=rcc/resources.qrc
py_file=src/quinspect/resources.py
if [[ ! -f $py_file || $rc_file -nt $py_file ]]; then
echo "Compiling "$rc_file" ..."
.venv/bin/pyside6-uic --output $py_file rc_file
fi
This Justfile defines three rules: default, compile-ui-files, and compile-qrc-files.
The default rule is called when just is invoked without arguments, and simply lists the supported commands:
$ just
just --list --justfile justfile
Available recipes:
default
[Generators]
compile-qrc-files # Compile .rc files if the generated code is out-of-date or missing
compile-ui-files # Compile .ui files if the generated code is out-of-date or missing
The compile-ui-files looks for .ui files, inside the src/ directory, and runs a command for each one.
for ui_file in $(find src/ -name "*.ui"); do
# contents of for loop elided
For each files we find, we want to generate the compiled Python version in the same directory, just prefixed with ui_ to make it identifiable.
# Get the file-name (stripping out the leading directories)
ui_file_name=$(basename $ui_file)
# Create the file python file in the same directory (dirname $ui_file), add the `ui_` prefix to the file name, and the replace the ``.ui`` extension with `.py`
py_file="$(dirname $ui_file)/ui_${ui_file_name/%.ui/.py}"
Finally, if the python file does not exists, or the .ui file has been modified more recently, re-generate the python file.
To check if the file exists, we use ! -f $py_file, where !` negates the ``-f` condition (argument is a file).
To compare the modification times, we use the -nt (or newer than) operator to [[. If $ui_file is newer than the $py_file, them we run the pyside6-uic compiler.
if [[ ! -f $py_file || $ui_file -nt $py_file ]]; then
echo "Compiling "$ui_file" ..."
.venv/bin/pyside6-uic --output $py_file $ui_file
fi
The compile-qrc-files` rule follows the same logic. However, since we only have one .qrc` file, and the location of the output file is less straightforward, we hard-code it into the rule.
Make or Just ?
So, should you use a Makefile or a Justfile?
If you already have a Makefile, by all means use a make to update the .ui files. You can see how much easier it is.
If you already have a Justfile, you might want to keep adding to it. I personally think that the syntax of the Justfile is easier to read, even if the rules end up being more verbose. However, in the age of coding assistants that can generate makefiles and explain them, this might not be as large an issue as it used to be.
Of course, you can also use your justfile to run your makefile. That might be the best of both worlds.
Bonus Content: Use fzf to select the file to edit
Since writing the first draft of this article, I have extended the justfile with the following rule.
# Select a .ui file to edit with PySide6 Designer
edit-ui:
fdfind --extension ui --type f \
| fzf --layout=reverse \
--preview 'batcat --style=numbers --color=always {}' \
--select-1 --exit-0 \
| xargs -r poetry run pyside6-designer
just --justfile {{ justfile() }} compile-ui-files
When run, it shows a list of ui files (with a preview of the file contents), and opens the selected file in PySide Designer. When the designer is closed, the compile-ui-files command is run to recompile the changed files.
A small change, but a definite time-saver.