Skip to content

Commit 115e49e

Browse files
feat(ci): add independent manylinux_2_28 Qt build target (glibc 2.28 ABI floor) (#1409)
* feat(ci): add independent manylinux_2_28 Qt build target Adds a new `build-qt-manylinux-2-28` CI job that builds ActivityWatch inside `quay.io/pypa/manylinux_2_28_x86_64`, producing Linux binaries compatible with glibc 2.28 (RHEL/CentOS/AlmaLinux/Rocky 8 and older). The change is purely additive: the existing `build-qt` and `build-tauri` platform matrices are untouched. Key decisions: - `git config safe.directory` is set immediately after checkout, before the first version probe, so the version string is correct throughout the build. - Artifacts are renamed with a `-manylinux_2_28` suffix to avoid any name collision with the standard Ubuntu build. - An offline ABI gate (`scripts/package/abi-gate.sh`) scans every ELF in both the ZIP and AppImage payload and fails if any symbol requires a GLIBC version above 2.28. - The release job's `needs` list now includes `build-qt-manylinux-2-28` so the manylinux artifacts are included in every tagged release. Closes #1390 * fix(package): use cp -a to handle hidden files when flattening aw-qt dir The glob 'mv aw-qt-tmp/*' skips dotfiles, leaving aw-qt-tmp non-empty and causing 'rmdir: failed to remove aw-qt-tmp: Directory not empty' in the manylinux_2_28 container. Switch to cp -a dir/. which copies all files (including hidden) then rm -rf the source directory. * fix(ci): add server version guard and harden AppImage ABI gate Two P1 issues from Greptile review: 1. Manylinux job lacked the aw-server-rust version guard that build-qt and build-qt-tauri already have. Without it a tagged release could ship a manylinux bundle containing an aw-server binary from a mismatched release line. The new step is identical to the guard in those jobs. 2. abi-gate.sh failed open when AppImage extraction failed — it warned and exited 0, so a release could publish an unscanned AppImage containing GLIBC refs above 2.28. Extraction failure now exits 2 (script error). * fix(package): rename aw-qt dir before cp -a to avoid name conflict cp -a dist/activitywatch/aw-qt/. dist/activitywatch/ fails because the aw-qt binary inside the dir has the same name as the source directory — cp can't overwrite the directory with the non-directory. Move to a temp name first, then copy contents, then remove.
1 parent 991d906 commit 115e49e

3 files changed

Lines changed: 375 additions & 4 deletions

File tree

.github/workflows/release.yml

Lines changed: 234 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,239 @@ jobs:
526526
name: builds-${{ matrix.os }}-py${{ matrix.python_version }}
527527
path: dist/activitywatch-*.*
528528

529+
build-qt-manylinux-2-28:
530+
name: Build Qt artifacts (manylinux_2_28 — glibc 2.28 ABI floor)
531+
if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
532+
runs-on: ubuntu-22.04
533+
container:
534+
image: quay.io/pypa/manylinux_2_28_x86_64
535+
env:
536+
AW_EXTRAS: true
537+
AW_RESEARCH_EDITION: ${{ (github.event_name == 'workflow_dispatch' && inputs.edition == 'research') || endsWith(github.ref_name, '-research') }}
538+
# AppImage tools (linuxdeploy/appimagetool) are themselves AppImages;
539+
# FUSE is unavailable inside Docker containers, so use extract-and-run.
540+
APPIMAGE_EXTRACT_AND_RUN: 1
541+
# aw-sync enables openssl/vendored on Linux and tries to build OpenSSL
542+
# from source; use the container's system openssl-devel instead.
543+
OPENSSL_NO_VENDOR: 1
544+
defaults:
545+
run:
546+
shell: bash
547+
strategy:
548+
fail-fast: false
549+
matrix:
550+
python_version: [3.9]
551+
node_version: [22]
552+
553+
steps:
554+
- uses: actions/checkout@v7
555+
with:
556+
submodules: 'recursive'
557+
fetch-depth: 0
558+
559+
# Configure git safe.directory immediately after checkout so that every
560+
# subsequent step — including the version probe below — can read the full
561+
# history. Deferring this step past the first `git describe` call causes
562+
# the version probe to fall back to v0.0.0.dev-unknown.
563+
- name: Configure git safe directory
564+
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
565+
566+
- name: Set RELEASE
567+
run: |
568+
echo "RELEASE=${{ startsWith(github.ref_name, 'v') || github.ref_name == 'master' }}" >> "$GITHUB_ENV"
569+
570+
- name: Set tag metadata
571+
if: startsWith(github.ref, 'refs/tags/v')
572+
run: |
573+
echo "VERSION_TAG=${GITHUB_REF_NAME}" >> "$GITHUB_ENV"
574+
575+
- name: Determine and output version
576+
run: |
577+
VERSION_WITH_V=$(bash scripts/package/getversion.sh)
578+
# Strip the research tag suffix for filenames: the edition gets its
579+
# own token (activitywatch[-research]-<version>-...), so the version
580+
# part stays a plain version string.
581+
VERSION_WITH_V="${VERSION_WITH_V%-research}"
582+
VERSION_NO_V="${VERSION_WITH_V#v}"
583+
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> "$GITHUB_ENV"
584+
echo "VERSION_NO_V=${VERSION_NO_V}" >> "$GITHUB_ENV"
585+
echo "========================================"
586+
echo "Build Version Information"
587+
echo "========================================"
588+
echo "GitHub ref: ${{ github.ref }}"
589+
echo "GitHub ref_name: ${{ github.ref_name }}"
590+
echo "Version (with v): ${VERSION_WITH_V}"
591+
echo "Version (no v): ${VERSION_NO_V}"
592+
echo "========================================"
593+
594+
- name: Verify aw-server-rust submodule version matches release tag
595+
if: startsWith(github.ref, 'refs/tags/v')
596+
run: |
597+
# Fail fast if the aw-server-rust submodule is pinned to an older
598+
# release line than the one we're tagging. Same check as in
599+
# build-qt and build-qt-tauri — prevents shipping a manylinux
600+
# bundle with a mismatched aw-server-rust binary.
601+
#
602+
# The bundled binary's version lives in
603+
# aw-server-rust/aw-server/Cargo.toml (the workspace-level
604+
# aw-server-rust/Cargo.toml has no [package].version field).
605+
BUNDLED_VERSION=$(grep -m1 '^version = ' aw-server-rust/aw-server/Cargo.toml | sed -E 's/^version = "(.*)".*/\1/')
606+
if [ -z "$BUNDLED_VERSION" ]; then
607+
echo "ERROR: could not read aw-server version from aw-server-rust/aw-server/Cargo.toml" >&2
608+
exit 1
609+
fi
610+
611+
AW_VERSION="${VERSION_NO_V}" # e.g. "0.14.0b3"
612+
if [ -z "$AW_VERSION" ]; then
613+
echo "ERROR: VERSION_NO_V is empty — the 'Determine and output version' step must export it to GITHUB_ENV" >&2
614+
exit 1
615+
fi
616+
AW_MAJOR_MINOR=$(echo "$AW_VERSION" | cut -d'.' -f1-2) # "0.14"
617+
AWS_MAJOR_MINOR=$(echo "$BUNDLED_VERSION" | cut -d'.' -f1-2) # "0.14"
618+
619+
echo "AW release tag: ${AW_VERSION} (major.minor: ${AW_MAJOR_MINOR})"
620+
echo "Bundled aw-server: ${BUNDLED_VERSION} (major.minor: ${AWS_MAJOR_MINOR})"
621+
622+
if [ "$AW_MAJOR_MINOR" != "$AWS_MAJOR_MINOR" ]; then
623+
echo ""
624+
echo "ERROR: aw-server-rust major.minor (${AWS_MAJOR_MINOR}) does not match"
625+
echo " AW release major.minor (${AW_MAJOR_MINOR})."
626+
echo " The aw-server-rust submodule is stale for this tag."
627+
echo " Update the submodule (cd aw-server-rust && git pull) and re-tag."
628+
exit 1
629+
fi
630+
echo "OK: aw-server version ${BUNDLED_VERSION} is consistent with AW release ${AW_VERSION}"
631+
632+
- name: Set up Node
633+
uses: actions/setup-node@v6
634+
with:
635+
node-version: ${{ matrix.node_version }}
636+
637+
- name: Set up Rust
638+
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master
639+
id: toolchain
640+
with:
641+
toolchain: stable
642+
643+
- name: Cache node_modules
644+
uses: actions/cache@v6
645+
with:
646+
path: aw-server-rust/aw-webui/node_modules
647+
key: manylinux_2_28-node_modules-${{ hashFiles('**/package-lock.json') }}
648+
restore-keys: |
649+
manylinux_2_28-node_modules-
650+
651+
- name: Cache cargo build
652+
uses: actions/cache@v6
653+
env:
654+
cache-name: cargo-build-target
655+
with:
656+
path: aw-server-rust/target
657+
key: manylinux_2_28-${{ env.cache-name }}-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}
658+
restore-keys: |
659+
manylinux_2_28-${{ env.cache-name }}-${{ steps.toolchain.outputs.rustc_hash }}-
660+
661+
- name: Install Linux dependencies
662+
run: |
663+
dnf install -y epel-release
664+
dnf install -y \
665+
qt5-qtbase-devel \
666+
qt5-qtx11extras \
667+
qt5-qtwayland \
668+
fontconfig-devel \
669+
freetype-devel \
670+
libX11-devel \
671+
libXcursor-devel \
672+
libXext-devel \
673+
libXfixes-devel \
674+
libXft-devel \
675+
libXi-devel \
676+
libXrandr-devel \
677+
libXrender-devel \
678+
libxcb-devel \
679+
openssl-devel \
680+
kernel-headers \
681+
gcc \
682+
gcc-c++ \
683+
python39-devel \
684+
python39-libs \
685+
squashfs-tools \
686+
zip \
687+
wget
688+
689+
- name: Set up Python PATH
690+
run: |
691+
# Prepend the manylinux CPython 3.9 so `python3` resolves to the
692+
# container's build Python, which ships headers and a shared
693+
# libpython needed by PyInstaller.
694+
echo "/opt/python/cp39-cp39/bin" >> "$GITHUB_PATH"
695+
echo "LD_LIBRARY_PATH=/usr/lib64:${LD_LIBRARY_PATH:-}" >> "$GITHUB_ENV"
696+
697+
- name: Install Python tooling
698+
run: python3 -m pip install poetry==2.2.1
699+
700+
- name: Patch research edition defaults
701+
if: env.AW_RESEARCH_EDITION == 'true'
702+
run: python3 scripts/patch_research_edition_config.py aw-watcher-window/aw_watcher_window/config.py
703+
704+
- name: Emit research edition category preset for the web UI
705+
if: env.AW_RESEARCH_EDITION == 'true'
706+
run: |
707+
preset="$(python3 scripts/emit_research_category_preset.py)"
708+
echo "AW_PRESET_CATEGORY_SETS=${preset}" >> "$GITHUB_ENV"
709+
710+
- name: Build
711+
run: |
712+
python3 -m venv venv
713+
source venv/bin/activate
714+
poetry install
715+
make build
716+
pip freeze
717+
718+
- name: Run tests
719+
run: |
720+
source venv/bin/activate
721+
make test
722+
723+
- name: Run integration tests
724+
run: |
725+
source venv/bin/activate
726+
make test-integration
727+
728+
- name: Package
729+
run: |
730+
source venv/bin/activate
731+
poetry install
732+
make package
733+
734+
- name: Package AppImage
735+
run: ./scripts/package/package-appimage.sh
736+
737+
- name: Rename artifacts with manylinux_2_28 suffix
738+
run: |
739+
EDITION=""
740+
if [[ "$AW_RESEARCH_EDITION" == "true" ]]; then EDITION="-research"; fi
741+
# ZIP produced by `make package`
742+
ZIP_SRC="dist/activitywatch${EDITION}-${VERSION_WITH_V}-linux-x86_64.zip"
743+
ZIP_DST="dist/activitywatch${EDITION}-${VERSION_WITH_V}-linux-x86_64-manylinux_2_28.zip"
744+
[ -f "$ZIP_SRC" ] && mv -v "$ZIP_SRC" "$ZIP_DST"
745+
# AppImage produced by package-appimage.sh (intentionally unversioned)
746+
AI_SRC="dist/activitywatch${EDITION}-linux-x86_64.AppImage"
747+
AI_DST="dist/activitywatch${EDITION}-linux-x86_64-manylinux_2_28.AppImage"
748+
[ -f "$AI_SRC" ] && mv -v "$AI_SRC" "$AI_DST"
749+
750+
- name: ABI gate — verify glibc 2.28 floor
751+
run: |
752+
bash scripts/package/abi-gate.sh \
753+
dist/activitywatch*-manylinux_2_28.zip \
754+
dist/activitywatch*-manylinux_2_28.AppImage
755+
756+
- name: Upload packages
757+
uses: actions/upload-artifact@v7
758+
with:
759+
name: builds-manylinux_2_28-qt-py${{ matrix.python_version }}
760+
path: dist/activitywatch-*.*
761+
529762
build-tauri:
530763
name: Build Tauri artifacts
531764
if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
@@ -994,7 +1227,7 @@ jobs:
9941227
release:
9951228
name: Publish draft release
9961229
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
997-
needs: [build-qt, build-tauri, release-notes]
1230+
needs: [build-qt, build-qt-manylinux-2-28, build-tauri, release-notes]
9981231
runs-on: ubuntu-latest
9991232
steps:
10001233
# Pin checkout in this contents-write job so a moved v7 tag cannot

Makefile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,12 @@ ifeq ($(TAURI_BUILD),true)
209209
cp aw-server-rust/target/$(targetdir)/aw-sync dist/activitywatch/aw-server-rust/aw-sync
210210
else
211211
# Move aw-qt to the root of the dist folder
212-
mv dist/activitywatch/aw-qt aw-qt-tmp
213-
mv aw-qt-tmp/* dist/activitywatch
214-
rmdir aw-qt-tmp
212+
# Rename first to avoid cp conflict: the aw-qt binary inside the dir has the
213+
# same name as the source directory, so cp -a src/. dest/ would fail trying
214+
# to overwrite the directory with the binary of the same name.
215+
mv dist/activitywatch/aw-qt dist/aw-qt-tmp
216+
cp -a dist/aw-qt-tmp/. dist/activitywatch/
217+
rm -rf dist/aw-qt-tmp
215218
endif
216219
# Remove problem-causing binaries
217220
rm -f dist/activitywatch/libdrm.so.2 # see: https://github.com/ActivityWatch/activitywatch/issues/161

0 commit comments

Comments
 (0)