Custom builds of QGIS for Windows: a base of qgis/QGIS, usually master, with PRs and branches merged onto it that qgis/QGIS does not have yet.
- A version is a file of
versions/. It names a base and what is merged onto it, in order. compose.pymerges a version into the branchqgis-custom/<version>.- The workflow
buildpushes that branch to opengisch/QGIS and builds it, for Windows and as a Docker image. - The workflow
releasepublishes a build as a release. rr-cache/keeps the resolutions of merge conflicts. All versions share them.
Each build takes the current heads of the base and of the PRs. Its manifest records their commits.
[base]
repo = "qgis/QGIS"
ref = "master" # a branch or a tag
follow = false # see below
[[merge]]
pr = 68084 # a PR of the base repo
note = "why we need it"
[[merge]]
repo = "opengisch/QGIS" # a branch of any repo
branch = "my-fix"To add a version, add its file and add its name to the version options in
release.yml.
A source, the base or an entry, is followed unless it sets follow = false. Each night at midnight in Paris,
the workflow follow runs the workflow build when a followed source has commits that
the last build lacks. A base on master is usually not followed: it moves each day, and each build takes its head
anyway. To check by hand which followed sources the last build lacks: ./compose.py --check <version>, which needs
gh.
./compose.py qgis-dmavThe script needs uv. The first run clones qgis/QGIS without its blobs into
work/QGIS. All versions share that clone. The result is the branch qgis-custom/qgis-dmav there, and the
manifest work/qgis-dmav.md.
A conflict cannot be resolved in CI. You resolve it once, and git rerere replays it for all later builds:
- Run
./compose.py <version>. It stops on the entry that conflicts and lists the files. - Resolve them in
work/QGIS, thengit addandgit commit. rerere records the resolution inrr-cache/. - Run
./compose.py <version>again, until it gets through. - Commit
rr-cache/and push. CI replays the resolution for every version that has the same conflict.
To redo a bad resolution, run git rerere forget <path> during the conflict. rerere cannot help with a
delete/modify conflict, or with a merge that is clean but does not compile. In that case, push a fix branch to
opengisch/QGIS and add it as an entry.
- Pull request: composes every version, without a build.
- Push to main: composes every version, pushes each one to opengisch/QGIS, and builds it: a zip for Windows, kept
as an artifact for 90 days, and a Docker image. A push that changes only this README,
release.ymlorfollow.ymlbuilds nothing. To build again without a commit, for example to take a newer master, run the workflow by hand: Actions → build → Run workflow. - A followed source moves: the workflow
followruns the build, as a run by hand.
A push while a build runs does not cancel it: the new build waits, and starts from the head of main.
The Windows build follows windows-qt6.yml of qgis/QGIS. The head of build.yml names
the upstream commit it follows. When upstream changes its steps or its flags, copy the change.
The dependencies come from the vcpkg binary cache of qgis/QGIS. ccache keeps the objects that are already compiled. The first build, or a build after a big jump of master, can take longer than the 6 h limit of a job. The build step stops at 5 h 30, and the cache is saved. Click "Re-run failed jobs": the run builds the same commit, and continues where it stopped.
The image ghcr.io/opengisch/qgis-custom has QGIS with its desktop and its Python, for Linux amd64. The
Dockerfile builds the composed commit as
qgis/qgis-slim is built, with the desktop on top: Ubuntu 26.04,
GDAL with Arrow and Parquet, without 3D, GRASS, PDAL or server. It also builds GEOS 3.15, which Ubuntu 26.04 has at
3.14. Its head names the upstream commit it copies. When
upstream changes slim/Dockerfile, copy the change. The job saves its ccache even when the build or its test fails.
The test starts the desktop, and the image is pushed only when it passes. Its tags:
<version>, for exampleqgis-dmav: the last build of the version on main.<version>-<tag>, for exampleqgis-dmav-1: a release.- The composed commit, for each build.
docker run --rm ghcr.io/opengisch/qgis-custom:qgis-dmav python3 -c "from qgis.core import Qgis; print(Qgis.version())"qgis starts the desktop on the display of DISPLAY. The head of the Dockerfile says how to build it locally.
The workflow release makes the releases. Do not create a release on the Releases page: it gets no zip.
- In Actions → build, check that the last run on main succeeded, and that no run is in progress.
- Actions → release → Run workflow. Choose the version, and type a tag, for example
1. - When the run is done, the release
<version>-<tag>, for exampleqgis-dmav-1, is on the Releases page. Its asset is the zipqgis-custom-<version>-<tag>-win64.zip, and its notes are the manifest. The Docker image of the build has the tag<version>-<tag>too.
From a terminal:
gh workflow run release.yml -R opengisch/qgis-custom -f version=qgis-dmav -f tag=1The release does not build: it publishes the zip and the image of the last build of that version on main, in a few
minutes. It also tags the composed commit qgis-custom/<version>-<tag> in opengisch/QGIS, so that the commit stays
when the branch moves on. The run fails, and changes nothing, while a build of main is running, when no build of the
version on main is left, when that build has no image, or when the release exists already.
To redo a release, delete it, with its tag here and its tag in opengisch/QGIS, and run the workflow again:
gh release delete qgis-dmav-1 -R opengisch/qgis-custom --cleanup-tag --yes
gh api -X DELETE repos/opengisch/QGIS/git/refs/tags/qgis-custom/qgis-dmav-1The secret QGIS_FORK_TOKEN pushes the branches and tags to opengisch/QGIS. It is a fine-grained token or a GitHub
App token, on opengisch/QGIS only, with Contents and Workflows write. Workflows write is necessary because
the commits of master change .github/workflows/. The Docker image needs no secret: the workflows push it with
their GITHUB_TOKEN.