name: CI on: push: branches: [linux-port] pull_request: branches: [linux-port] workflow_dispatch: permissions: contents: read jobs: build-and-test: strategy: fail-fast: false matrix: build: [core, asan] runs-on: ubuntu-24.04 env: BUILD_DIR: build-${{ matrix.build }} steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 # pack.sh derives the release from the newest v2.0.26-linux.* tag - name: Install dependencies run: | # GitHub-hosted runners intermittently hang contacting archive.ubuntu.com # (the "noble-security InRelease" stall seen in core/asan jobs). Force # IPv4, bound apt timeouts, and retry the whole step so a transient # apt-mirror flake cannot stall the pipeline. # (no ForceIPv4: could break runners whose IPv6 route works) echo 'Acquire::Retries "5";' | sudo tee -a /etc/apt/apt.conf.d/99ci-retry echo 'Acquire::http::Timeout "15";' | sudo tee -a /etc/apt/apt.conf.d/99ci-retry echo 'Acquire::https::Timeout "15";' | sudo tee -a /etc/apt/apt.conf.d/99ci-retry retry() { for i in 1 2 3 4 5 6; do timeout 300 "$@" && return 0; echo "apt retry $i"; sleep $((i*10)); done; return 1; } # The default archive.ubuntu.com mirror is intermittently unreachable # from GitHub runners; if the update fails after retries, fall back to # the Azure mirror that backs these runners, then retry once more. if ! retry sudo apt-get update; then echo "default mirror unreachable; switching to azure.archive.ubuntu.com" sudo sed -i -E 's@//(archive\|security)\.ubuntu\.com@//azure.archive.ubuntu.com@g' /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || sudo sed -i -E 's@//(archive\|security)\.ubuntu\.com@//azure.archive.ubuntu.com@g' /etc/apt/sources.list 2>/dev/null || true retry sudo apt-get update fi retry sudo apt-get install -y --no-install-recommends \ cmake g++ ninja-build \ libx11-dev libxext-dev libxrandr-dev libxinerama-dev libxtst-dev libxi-dev \ libxfixes-dev \ libwayland-dev wayland-protocols libxkbcommon-dev \ libffi-dev libdbus-1-dev dbus libgtk-3-dev zlib1g-dev libjpeg-dev \ xvfb xdotool sway xwayland weston \ xclip sudo apt-get install -y --no-install-recommends python3 dpkg-dev - name: Configure run: | cmake -S . -B "$BUILD_DIR" if [ "${{ matrix.build }}" = "asan" ]; then rm -rf "$BUILD_DIR" cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_CXX_FLAGS="-fsanitize=address -g" \ -DCMAKE_C_FLAGS="-fsanitize=address -g" \ -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" fi - name: Build run: cmake --build "$BUILD_DIR" -j$(nproc) - name: Headless regression run: | bash tests/run_tests.sh "$BUILD_DIR/source/linux/core/ahk_core" - name: Doc-check (Xvfb) run: | bash tests/doccheck/run_check.sh --xvfb "$BUILD_DIR/source/linux/core/ahk_core" # check0818: the doc-check must gate merges (was continue-on-error). - name: Upload doc-check outputs if: always() uses: actions/upload-artifact@v5 with: name: doccheck-${{ matrix.build }} path: tests/doccheck/out/ if-no-files-found: ignore - name: Wayland doc-check (pure + XWayland) run: | bash tests/doccheck/wayland_run.sh "$BUILD_DIR/source/linux/core/ahk_core" bash tests/doccheck/wayland_run.sh --xwayland "$BUILD_DIR/source/linux/core/ahk_core" env: XDG_RUNTIME_DIR: /tmp # check_detail0821 §14 / R4: the scenario acceptance runner gates the # seeded user scenarios under Xvfb (hotkeys, clipboard, long-press, # a&b, --pack, FileInstall resources, typed callbacks). - name: Scenario runner (Xvfb) run: xvfb-run -a bash tests/scenarios/run_scenarios.sh "$BUILD_DIR/source/linux/core/ahk_core" --env x11 # check_detail0821 §16-6 / R4: a short soak -- high-frequency-ish Send # under Xvfb with RSS stability (no unbounded growth) and no event loss. - name: Soak (RSS stability + event count) run: | printf 'cnt := 0\nF6::{\n global cnt\n cnt++\n}\nSleep 300\nLoop 100 {\n Send("{F6}")\n Sleep 200\n}\nSleep 1000\nFileAppend("count=" cnt "\\n", "/tmp/soak_out.txt")\nExitApp\n' > /tmp/soak.ahk xvfb-run -a bash -c '"$1" /tmp/soak.ahk' _ "$BUILD_DIR/source/linux/core/ahk_core" > /tmp/soak_run.log 2>&1 & SPID=$! RSS0=""; RSSN="" for i in 1 2 3 4 5 6 7 8 9 10 11 12; do kill -0 $SPID 2>/dev/null || break rss=$(grep VmRSS /proc/$SPID/status 2>/dev/null | awk '{print $2}') [ -n "$rss" ] && { [ -z "$RSS0" ] && RSS0=$rss; RSSN=$rss; } sleep 2 done wait $SPID grep -q 'count=100' /tmp/soak_out.txt || { echo "FAIL: event loss"; exit 1; } GROWTH=$(( ${RSSN:-0} - ${RSS0:-0} )) echo "soak RSS growth_kb=$GROWTH (start=${RSS0:-?} end=${RSSN:-?})" [ "$GROWTH" -lt 20000 ] || { echo "FAIL: RSS grew > 20MB"; exit 1; } echo soak-pass # check0819: the published numbers in CHECK_REPORT.md used to drift by # hand (Wayland "847", XWayland 235 vs 247). This recomputes every # count from the expect files and fails the pipeline on any mismatch, # so the docs can never go stale again. - name: Verify report numbers match the suites run: bash tests/doccheck/verify_report_numbers.sh # check_detail0821 §13: parity_data.h must stay in sync with parity.tsv # (regenerated by tools/gen_parity.py). - name: Verify parity classification is in sync run: python3 tools/gen_parity.py --check # §13: every function doc page with a parity entry must carry the badge. - name: Verify parity doc badges are in sync run: python3 tools/gen_parity_badges.py --check # check_detail0821 §16-1 / R4: container matrix over four distros -- builds # the port + runs the headless regression + a display smoke inside each # container, so glibc/GTK version differences cannot regress portability. container-matrix: strategy: fail-fast: false matrix: image: [fedora:41, archlinux:latest, debian:12, ubuntu:22.04] runs-on: ubuntu-24.04 container: image: ${{ matrix.image }} options: --user root steps: - name: Checkout uses: actions/checkout@v5 - name: Install dependencies run: bash tools/linux/ci-container-deps.sh - name: Configure run: cmake -S . -B build-container - name: Build # -j2: the container memory quota is small; -j$(nproc) over-commits # and the C++ compile stalls/hangs (observed in the container matrix). run: cmake --build build-container -j2 - name: Headless regression run: bash tests/run_tests.sh build-container/source/linux/core/ahk_core # A display smoke under Xvfb: the built binary must start + run a script # that touches the X11/GTK init path. Deliberately NOT MsgBox (it would # block forever on the unclicked dialog under headless); the script # writes a marker and exits. Skipped when the distro lacks xvfb. - name: Xvfb smoke run: | if ! command -v xvfb-run >/dev/null; then echo "xvfb not installed; smoke skipped" exit 0 fi rm -f /tmp/smoke_out.txt printf 'FileAppend("smoke-ok`n", "/tmp/smoke_out.txt")\nExitApp\n' > /tmp/smoke.ahk xvfb-run -a bash -c 'build-container/source/linux/core/ahk_core /tmp/smoke.ahk' test -f /tmp/smoke_out.txt && echo smoke-pass # check_detail0821 §5-M6 / R4: the packed binary must run in a container # WITHOUT the port installed -- it embeds both the interpreter and the # script (A_IsCompiled=1), and a FileInstall resource must land on disk. pack-container-acceptance: runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v5 - name: Install deps + build run: | bash tools/linux/ci-container-deps.sh cmake -S . -B build-pc cmake --build build-pc -j$(nproc) - name: Pack + verify in a bare container run: | AHK="$PWD/build-pc/source/linux/core/ahk_core" mkdir -p /tmp/packwork && cd /tmp/packwork printf 'PACKED-FILEINSTALL-OK' > res.txt cat > pkg.ahk <<'EOF' #Requires AutoHotkey v2.0 FileInstall("/tmp/packwork/res.txt", "/tmp/packwork/out.txt") c := FileExist("/tmp/packwork/out.txt") ? FileRead("/tmp/packwork/out.txt") : "" FileAppend("compiled=" A_IsCompiled " res=" (c = "PACKED-FILEINSTALL-OK" ? 1 : 0) "`n", "/tmp/packwork/pk.txt") ExitApp EOF "$AHK" --pack /tmp/packwork/packed /tmp/packwork/pkg.ahk # A clean ubuntu:24.04 container (no AHK installed; its runtime # shared libs match the build host) must run the packed binary and # have it extract the FileInstall resource -- proving the packed # binary needs no AHK installation, only the standard system libs. # (debian:12 would work too but its libjpeg ABI differs from the # build host's libjpeg8; the wcslcpy/strlcpy self-containment that # debian's older glibc exposed is covered by the symbol check.) docker run --rm -v /tmp/packwork:/tmp/packwork -w /tmp/packwork ubuntu:24.04 \ bash -c 'apt-get update -y -qq >/dev/null 2>&1 && apt-get install -y -qq --no-install-recommends libx11-6 libxext6 libxrandr2 libxinerama1 libxtst6 libxi6 libxfixes3 libxkbcommon0 libwayland-client0 libffi8 libgtk-3-0 zlib1g libjpeg8 >/dev/null 2>&1; rm -f out.txt pk.txt; ./packed; grep -q "compiled=1 res=1" pk.txt && echo pack-acceptance-pass' # check_detail0821 §16-3 / R4: a no-XWayland job -- sway with xwayland # disabled runs the Wayland suite, proving the pure-Wayland downgrade path # does not crash and reports clearly. no-xwayland: runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v5 - name: Install deps + build run: | bash tools/linux/ci-container-deps.sh cmake -S . -B build-nox cmake --build build-nox -j$(nproc) # The default wayland_run.sh mode already disables xwayland in the sway # config (line 49: `xwayland disable`), which is exactly the §16-3 # pure-Wayland downgrade path; --xwayland is the opt-in enabled mode. - name: Wayland suite with xwayland disabled run: bash tests/doccheck/wayland_run.sh build-nox/source/linux/core/ahk_core env: XDG_RUNTIME_DIR: /tmp package: runs-on: ubuntu-24.04 needs: build-and-test steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 # pack.sh derives the release from the newest v2.0.26-linux.* tag - name: Install dependencies run: | # GitHub-hosted runners intermittently hang contacting archive.ubuntu.com # (the "noble-security InRelease" stall seen in core/asan jobs). Force # IPv4, bound apt timeouts, and retry the whole step so a transient # apt-mirror flake cannot stall the pipeline. # (no ForceIPv4: could break runners whose IPv6 route works) echo 'Acquire::Retries "5";' | sudo tee -a /etc/apt/apt.conf.d/99ci-retry echo 'Acquire::http::Timeout "15";' | sudo tee -a /etc/apt/apt.conf.d/99ci-retry echo 'Acquire::https::Timeout "15";' | sudo tee -a /etc/apt/apt.conf.d/99ci-retry retry() { for i in 1 2 3 4 5 6; do timeout 300 "$@" && return 0; echo "apt retry $i"; sleep $((i*10)); done; return 1; } # The default archive.ubuntu.com mirror is intermittently unreachable # from GitHub runners; if the update fails after retries, fall back to # the Azure mirror that backs these runners, then retry once more. if ! retry sudo apt-get update; then echo "default mirror unreachable; switching to azure.archive.ubuntu.com" sudo sed -i -E 's@//(archive\|security)\.ubuntu\.com@//azure.archive.ubuntu.com@g' /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || sudo sed -i -E 's@//(archive\|security)\.ubuntu\.com@//azure.archive.ubuntu.com@g' /etc/apt/sources.list 2>/dev/null || true retry sudo apt-get update fi retry sudo apt-get install -y --no-install-recommends \ cmake g++ ninja-build \ libx11-dev libxext-dev libxrandr-dev libxinerama-dev libxtst-dev libxi-dev \ libxfixes-dev \ libwayland-dev wayland-protocols libxkbcommon-dev \ libffi-dev libdbus-1-dev dbus libgtk-3-dev libjpeg-dev \ python3 dpkg-dev rpm libfuse2 squashfs-tools # appimagetool (continuous, v295+) packages the AppDir through # FUSE; GitHub-hosted runners have no /dev/fuse by default, so # the device node must be recreated (the kernel fuse module and # libfuse2 are present, only the node is missing). if [ ! -e /dev/fuse ]; then sudo mknod /dev/fuse c 10 229 fi sudo chmod 666 /dev/fuse 2>/dev/null || true - name: Build run: | cmake -S . -B build-core cmake --build build-core -j$(nproc) - name: Build packages run: | bash tools/linux/pack.sh # AppImage + RPM are part of the release (check0820: the old # download/URL and source-layout bugs made them fail silently # under `|| true`; now they must build). bash tools/linux/pack-appimage.sh bash tools/linux/pack-rpm.sh # Regenerate CKSUMS.txt so it covers ALL dist artifacts (incl. # AppImage/RPM) and re-sign it (pack.sh runs before those exist). bash tools/linux/pack-finalize.sh ls -la dist/ - name: Verify packages run: | # head closes the pipe early -> tar gets SIGPIPE (exit 141); # ignore it (the listing is just a sanity check). set +e tar tzf dist/autohotkey-linux-*.tar.gz | head -5 tar_rc=$? set -e [ $tar_rc -eq 0 ] || [ $tar_rc -eq 141 ] dpkg-deb --info dist/autohotkey-linux-*.deb | head -8 ls dist/*.AppImage dist/*.rpm 2>/dev/null || true # check0820: every release ships a CKSUMS.txt (SHA-256) so # `ahk --update ` can be verified against the published hashes # (and older releases stay available for rollback). This step checks # that the hashes match the artifacts actually built here. - name: Verify release checksums run: | grep -E '^ [0-9a-f]{64} ' dist/CKSUMS.txt | sed -n 's/^ //p' > /tmp/ck.txt ( cd dist && sha256sum -c /tmp/ck.txt ) # check0820: CKSUMS.txt is OpenPGP-signed (dist/CKSUMS.txt.asc + # dist/ahk-release.pub) so the release can be verified end-to-end. - name: Verify release signature run: | test -s dist/CKSUMS.txt.asc || { echo "missing CKSUMS.txt.asc"; exit 1; } test -s dist/ahk-release.pub || { echo "missing ahk-release.pub"; exit 1; } gpg --batch --import dist/ahk-release.pub 2>/dev/null gpg --batch --verify dist/CKSUMS.txt.asc dist/CKSUMS.txt # check0819 P2-1: actually INSTALL each package, run the interpreter # and the launcher commands, then remove it again -- the previous # 'Verify packages' step only listed archive contents and read .deb # metadata. The tarball update step downloads the release asset from # GitHub, so this also proves the published asset round-trips. - name: Install/run/uninstall the packages run: | VER=$(ls dist/autohotkey-linux-*-amd64.deb | sed -n 's/.*linux-\([0-9.]*-linux\.[0-9]*\)-amd64.deb/\1/p' | head -1) bash tools/linux/verify-packages.sh "$VER"