From fd8449ea31ec459972f21d6fc0dbe994d07a28b1 Mon Sep 17 00:00:00 2001 From: Felitendo <95575686+Felitendo@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:21:39 +0200 Subject: [PATCH] ci: add an arch tarball with static opencv --- .github/workflows/release.yml | 44 ++++++++++++++++++-- packaging/README.md | 31 +++++++++----- packaging/build-opencv.sh | 77 +++++++++++++++++++++++++++++++++++ packaging/build-tarball.sh | 70 +++++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+), 13 deletions(-) create mode 100755 packaging/build-opencv.sh create mode 100755 packaging/build-tarball.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bad4754..5fafcd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -127,9 +127,47 @@ jobs: dist/rpm-signer.asc if-no-files-found: error + tarball: + name: Arch Linux tarball + runs-on: ubuntu-latest + # Built on Arch itself, because the agent only fits the Qt it was built + # against. OpenCV is linked in, so no opencv here. + container: archlinux:latest + steps: + - name: Install the build tools + run: | + pacman -Syu --noconfirm --needed git base-devel cmake pkgconf curl zstd \ + qt6-base qt6-declarative qt6-wayland layer-shell-qt ki18n \ + pam systemd-libs gettext scdoc + + - uses: actions/checkout@v7 + + - name: Check the tag against the Makefile + run: packaging/check-version.sh "${{ github.ref_name }}" + + - run: packaging/build-opencv.sh + + - run: packaging/build-tarball.sh + + - name: Install it and run it + run: | + tar -xf dist/*.tar.zst --strip-components=1 -C / + for f in /usr/lib/face-unlock/* /usr/lib/security/pam_face_unlock.so; do + if ldd "$f" | grep 'not found'; then echo "$f is missing a library"; exit 1; fi + done + /usr/lib/face-unlock/face-unlockd --version + useradd -m tester + runuser -u tester -- face-unlock --version + + - uses: actions/upload-artifact@v7 + with: + name: tarball + path: dist/*.tar.zst + if-no-files-found: error + publish: name: Release and repositories - needs: [deb, rpm] + needs: [deb, rpm, tarball] if: startsWith(github.ref, 'refs/tags/v') || inputs.dry_run runs-on: ubuntu-latest steps: @@ -152,9 +190,9 @@ jobs: gh release create "${{ github.ref_name }}" \ --title "$(.github/release-notes.sh --title "${{ github.ref_name }}")" \ --notes-file notes.md \ - incoming/*.deb incoming/*.rpm \ + incoming/*.deb incoming/*.rpm incoming/*.tar.zst \ || gh release upload "${{ github.ref_name }}" \ - incoming/*.deb incoming/*.rpm --clobber + incoming/*.deb incoming/*.rpm incoming/*.tar.zst --clobber - name: Install the repository tools run: | diff --git a/packaging/README.md b/packaging/README.md index b728156..7286c32 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -8,13 +8,15 @@ description of the layout, and two descriptions drift. |---|---| | `deb/control`, `deb/copyright` | metadata for the Debian binary package | | `rpm/face-unlock.spec` | the RPM spec | -| `build-deb.sh`, `build-rpm.sh` | build one package into `dist/` | +| `build-deb.sh`, `build-rpm.sh`, `build-tarball.sh` | build one package into `dist/` | +| `build-opencv.sh` | builds the static OpenCV the Arch tarball links in | | `check-version.sh` | refuses a tag that disagrees with the Makefile | | `publish-repos.sh` | regenerates the APT and RPM repositories | | `pages/` | the landing page and the `.repo` file served from GitHub Pages | -The AUR package lives in [Felitendo/PKGBUILDS](https://github.com/Felitendo/PKGBUILDS/tree/main/face-unlock). -Its CI notices a new GitHub release, updates the checksum and pushes to the AUR. +The AUR packages live in [Felitendo/PKGBUILDS](https://github.com/Felitendo/PKGBUILDS): +`face-unlock` builds from source, `face-unlock-bin` takes the Arch tarball. +Its CI notices a new GitHub release, updates the checksums and pushes to the AUR. Unlike the shell-only LoonixTools, this one is compiled. The packages are per architecture (amd64 and x86_64), and they need the Qt 6 and KDE Frameworks 6 @@ -28,6 +30,13 @@ against. The `.deb` is therefore built twice, in Debian 13 and in Ubuntu 26.04 gets an APT repository of its own: `deb/trixie` and `deb/resolute`. The RPM is built on the current Fedora. +The Arch tarball (`face-unlock--arch-x86_64.tar.zst`) is built on Arch, +for the same reason. It holds a folder with the `make install` tree. Arch +changes the OpenCV soname with every new OpenCV, so OpenCV is linked in +statically (`build-opencv.sh`: only the modules the daemon uses, nothing it +would load at run time). Qt stays shared, so a new Qt minor version on Arch +needs a new release. + The two networks (YuNet and SFace, from the OpenCV model zoo) are not in the repository. `make models` downloads them and checks them against the checksums in the Makefile. The RPM spec and the PKGBUILD list them as sources @@ -44,9 +53,10 @@ disables the socket. ```bash packaging/build-deb.sh # in a Debian 13 container, with the -dev packages from release.yml packaging/build-rpm.sh # in a Fedora container, with the -devel packages from the spec +packaging/build-opencv.sh && packaging/build-tarball.sh # in an Arch container, with the packages from release.yml ``` -Both take the version from `make version` unless one is passed as the first +All three take the version from `make version` unless one is passed as the first argument. ## Making a release @@ -56,10 +66,11 @@ argument. 2. Add the release to `CHANGELOG.md`, in the format CLAUDE.md describes. 3. Commit, then `git tag vX.Y.Z && git push --tags`. -The `release` workflow builds both packages in a Debian and a Fedora container, -refuses the tag if it disagrees with the Makefile or has no changelog entry, -attaches the packages to a GitHub release with the entry as its notes, and adds -them to the APT and RPM repositories on the `gh-pages` branch. +The `release` workflow builds the packages in Debian, Ubuntu, Fedora and Arch +containers, refuses the tag if it disagrees with the Makefile or has no +changelog entry, attaches the packages to a GitHub release with the entry as +its notes, and adds the deb and rpm files to the APT and RPM repositories on +the `gh-pages` branch. ## Trying the release path first @@ -67,7 +78,7 @@ them to the APT and RPM repositories on the `gh-pages` branch. gh workflow run release.yml -f dry_run=true ``` -Builds both packages, builds both repositories with a key generated on the +Builds the packages, builds both repositories with a key generated on the spot, checks the signatures, and installs the packages back out of the repositories. Nothing is pushed and no release is made. @@ -81,7 +92,7 @@ gpg --armor --export-secret-keys 'face-unlock repository' \ | gh secret set GPG_PRIVATE_KEY ``` -Without the secret the workflow still builds both packages and attaches them to +Without the secret the workflow still builds the packages and attaches them to the release; it says so in the log and leaves the repositories alone. ## Pointing Pages at it, once, in this order diff --git a/packaging/build-opencv.sh b/packaging/build-opencv.sh new file mode 100755 index 0000000..43d5f8c --- /dev/null +++ b/packaging/build-opencv.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# +# Builds the OpenCV the Arch tarball links in, as static libraries. +# +# Arch moves to a new OpenCV now and then, each with a new soname, and a +# daemon linked against the old one then no longer starts. Linked in +# statically, the daemon brings its own. Only the modules face-unlock uses are +# built, with OpenCV's own copies of zlib, libjpeg and libpng, so nothing is +# left to load at run time. The camera is read through V4L2, which OpenCV +# talks to directly. +# +# packaging/build-opencv.sh [PREFIX] +# +# Installs into PREFIX (build/opencv-static by default) and does nothing when +# PREFIX already holds what this script builds. Needs: cmake, a C++ compiler, +# curl. + +set -euo pipefail + +here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +prefix="${1:-$here/build/opencv-static}" + +# 4.x, like the deb and the rpm. The static dnn of 5.0.0 does not link +# (https://github.com/opencv/opencv/issues/29342). +version=4.14.0 +sha256=ee8fb9b30eb60850431b4656447080e3737b56e45719c92b67f245950609f86e + +# A change to this script is a different build. +stamp="$(sha256sum "${BASH_SOURCE[0]}" | cut -d' ' -f1)" +if [[ -f $prefix/.stamp && $(<"$prefix/.stamp") == "$stamp" ]]; then + echo "$prefix already has this OpenCV" + exit 0 +fi +# It is emptied before the install, so it has to be one of ours. +if [[ -e $prefix && ! -f $prefix/.stamp ]]; then + echo "$0: $prefix exists and was not made by this script" >&2 + exit 1 +fi + +work="$(mktemp -d)" +trap 'rm -rf -- "$work"' EXIT + +curl -fL --retry 3 -o "$work/opencv.tar.gz" \ + "https://github.com/opencv/opencv/archive/refs/tags/$version.tar.gz" +echo "$sha256 $work/opencv.tar.gz" | sha256sum -c --quiet - +tar -xzf "$work/opencv.tar.gz" -C "$work" +src="$work/opencv-$version" + +# BUILD_LIST adds what the listed modules need (calib3d, features2d, flann). +# The rest is switched off because it would be picked up from the system or +# downloaded: codecs, video backends, IPP and the other accelerators. +cmake -S "$src" -B "$work/build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$prefix" \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_LIST=core,imgproc,imgcodecs,videoio,objdetect,dnn \ + -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_EXAMPLES=OFF \ + -DBUILD_DOCS=OFF -DBUILD_opencv_apps=OFF -DBUILD_JAVA=OFF \ + -DBUILD_ZLIB=ON -DBUILD_JPEG=ON -DBUILD_PNG=ON -DBUILD_PROTOBUF=ON \ + -DWITH_V4L=ON \ + -DWITH_FFMPEG=OFF -DWITH_GSTREAMER=OFF -DWITH_OBSENSOR=OFF \ + -DWITH_TIFF=OFF -DWITH_WEBP=OFF -DWITH_AVIF=OFF -DWITH_OPENEXR=OFF \ + -DWITH_OPENJPEG=OFF -DWITH_JASPER=OFF \ + -DWITH_IPP=OFF -DWITH_ITT=OFF -DWITH_OPENCL=OFF -DWITH_VA=OFF -DWITH_VA_INTEL=OFF \ + -DWITH_LAPACK=OFF -DWITH_EIGEN=OFF -DWITH_FLATBUFFERS=OFF -DWITH_QUIRC=OFF -DWITH_ADE=OFF +cmake --build "$work/build" --parallel "$(nproc)" + +rm -rf -- "$prefix" +cmake --install "$work/build" + +# OpenCV installs the licences of the libraries in it, but not its own. +mv "$prefix"/share/licenses/opencv* "$prefix/share/licenses/opencv" +install -Dm644 "$src/LICENSE" "$prefix/share/licenses/opencv/LICENSE" + +# A picture with a face, for the check that the models load and find one. +install -Dm644 "$src/samples/data/messi5.jpg" "$prefix/share/face-unlock-check/face.jpg" +echo "$stamp" > "$prefix/.stamp" diff --git a/packaging/build-tarball.sh b/packaging/build-tarball.sh new file mode 100755 index 0000000..d1dc4bc --- /dev/null +++ b/packaging/build-tarball.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# +# Builds the tarball for Arch Linux into dist/. The AUR package +# face-unlock-bin is made from it. +# +# Like the deb and the rpm, it holds what `make install` produced, under a +# folder named like the tarball. One thing is different: OpenCV is linked in +# statically (packaging/build-opencv.sh), so a new OpenCV on Arch does not +# break the daemon. Qt stays shared. The agent uses Qt's private API, so the +# tarball fits the Qt that Arch had when it was built. +# +# Needs: make, cmake, a C++ compiler, the packages check.yml installs (without +# opencv), msgfmt (gettext), scdoc, curl, zstd, and the static OpenCV: +# +# packaging/build-opencv.sh && packaging/build-tarball.sh + +set -euo pipefail + +here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +version="${1:-$(make -s -C "$here" version)}" +opencv="${OPENCV_PREFIX:-$here/build/opencv-static}" +name="face-unlock-$version-arch-$(uname -m)" + +for tool in msgfmt scdoc cmake readelf zstd; do + command -v "$tool" > /dev/null || { echo "$0: $tool is not installed" >&2; exit 1; } +done +opencv_dir="$(dirname "$(find "$opencv" -name OpenCVConfig.cmake -print -quit 2>/dev/null)")" +[[ $opencv_dir != . ]] || { echo "$0: no OpenCV in $opencv, run packaging/build-opencv.sh first" >&2; exit 1; } + +root="$(mktemp -d)" +work="$(mktemp -d)" +trap 'rm -rf -- "$root" "$work"' EXIT +dest="$root/$name" + +make -C "$here" models +make -C "$here" install \ + DESTDIR="$dest" \ + PREFIX=/usr \ + VERSION="$version" \ + BUILDDIR="$work/build" \ + PAMDIR=/usr/lib/security \ + SYSTEMUNITDIR=/usr/lib/systemd/system \ + USERUNITDIR=/usr/lib/systemd/user \ + CMAKE_FLAGS="-DOpenCV_DIR=$opencv_dir" + +# The tests, and the models on a real face: the part a smaller OpenCV could +# break without anything else noticing. +ctest --test-dir "$work/build" --output-on-failure +face="$opencv/share/face-unlock-check/face.jpg" +"$work/build/test_images" "$dest/usr/share/face-unlock/models" "$face" "$face" | tee "$work/faces" +grep -q 'similarity' "$work/faces" || { echo "$0: the models found no face" >&2; exit 1; } + +if readelf -d "$dest/usr/lib/face-unlock/face-unlockd" | grep -q 'libopencv'; then + echo "$0: the daemon still loads a shared OpenCV" >&2 + exit 1 +fi + +# The program is GPL, OpenCV and the libraries in it come with their own +# licences, and the copyright file names the models' authors and licences. +lic="$dest/usr/share/licenses/face-unlock" +install -Dm644 "$here/LICENSE" "$lic/LICENSE" +install -Dm644 "$here/packaging/deb/copyright" "$lic/copyright" +cp -r "$opencv/share/licenses/opencv" "$lic/opencv" + +mkdir -p "$here/dist" +out="$here/dist/$name.tar.zst" +tar -C "$root" --owner=0 --group=0 --numeric-owner --sort=name \ + -I 'zstd -19 -T0' -cf "$out" "$name" + +echo "$out"