From 18c9bbf06c43a6e73b2f561f21eaa6c826eed321 Mon Sep 17 00:00:00 2001 From: Akshar Patel <123344143+AksharP5@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:42:57 -0400 Subject: [PATCH 1/3] fix(build): preserve prerelease ordering in Arch packages --- .github/workflows/arch-package.yml | 30 ++++ scripts/build-linux-arch-package.sh | 6 +- scripts/build-linux-arch-package.test.mjs | 163 ++++++++++++++++++++++ 3 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/arch-package.yml create mode 100644 scripts/build-linux-arch-package.test.mjs diff --git a/.github/workflows/arch-package.yml b/.github/workflows/arch-package.yml new file mode 100644 index 00000000000..f3741b0bd6a --- /dev/null +++ b/.github/workflows/arch-package.yml @@ -0,0 +1,30 @@ +name: Arch package + +on: + pull_request: + paths: + - "scripts/build-linux-arch-package*" + - ".github/workflows/arch-package.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: Arch package versions + runs-on: blacksmith-2vcpu-ubuntu-2404 + container: archlinux:base-devel@sha256:68bfc3b0d277b08a99101dc9b94aaa03e5ae70cf1b4fb965c03b2b87b915760d + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install Node.js + run: pacman -Syu --noconfirm --needed nodejs + - name: Test package ordering and payloads + shell: bash + run: | + set -euo pipefail + useradd --create-home cap-package-test + runuser -u cap-package-test -- node --test --test-reporter=tap scripts/build-linux-arch-package.test.mjs | tee /tmp/arch-package-tests.tap + grep -q '^# skipped 0$' /tmp/arch-package-tests.tap diff --git a/scripts/build-linux-arch-package.sh b/scripts/build-linux-arch-package.sh index ce972e328fd..7fcd76e2490 100644 --- a/scripts/build-linux-arch-package.sh +++ b/scripts/build-linux-arch-package.sh @@ -43,15 +43,17 @@ cp "$deb" "$work/Cap.deb" checksum="$(sha256sum "$work/Cap.deb" | cut -d ' ' -f 1)" cat > "$work/PKGBUILD" < + spawnSync("sh", ["-c", `command -v ${command}`]).status === 0; +const canPackage = + process.platform === "linux" && + process.getuid() !== 0 && + ["makepkg", "vercmp", "bsdtar", "ar", "fakeroot", "zstd"].every(available); + +test( + "Arch prereleases upgrade to stable and supersede existing packages", + { skip: !canPackage }, + async (t) => { + const root = await mkdtemp(path.join(tmpdir(), "cap-arch-packages-")); + t.after(() => rm(root, { recursive: true, force: true })); + const temporary = path.join(root, "tmp"); + await mkdir(temporary); + const config = path.join(root, "makepkg.conf"); + await writeFile( + config, + [ + await readFile("/etc/makepkg.conf", "utf8"), + "BUILDENV=(!distcc !color !ccache !check !sign)", + 'PACKAGER="Synthetic fixture "', + "OPTIONS=(!strip !docs !libtool !staticlibs emptydirs !zipman !purge !debug !lto)", + "COMPRESSZST=(zstd -c -T1 -)", + "PKGEXT='.pkg.tar.zst'", + "", + ].join("\n"), + ); + const env = { + ...process.env, + TMPDIR: temporary, + MAKEPKG_CONF: config, + CARCH: "x86_64", + }; + function run(command, args, cwd = root) { + const result = spawnSync(command, args, { + cwd, + env, + encoding: "utf8", + timeout: 30_000, + }); + assert.ifError(result.error); + assert.equal( + result.status, + 0, + `${command}: ${result.stdout}\n${result.stderr}`, + ); + return result.stdout.trim(); + } + const versions = [ + "0.6.2-nightly.202610210300", + "0.6.2-nightly.202610220300", + "0.6.2-rc.1", + "0.6.2-1", + "0.6.2", + ]; + const packages = []; + for (const version of versions) { + const directory = path.join(root, version); + const control = path.join(directory, "control"); + const payload = path.join(directory, "payload"); + const output = path.join(directory, "output"); + await mkdir(control, { recursive: true }); + await mkdir(path.join(payload, "usr/bin"), { recursive: true }); + await mkdir(path.join(payload, "usr/lib/cap"), { recursive: true }); + await mkdir(output); + await writeFile(path.join(directory, "debian-binary"), "2.0\n"); + await writeFile( + path.join(control, "control"), + `Package: cap\nVersion: ${version}\nArchitecture: amd64\nMaintainer: Synthetic fixture \nDescription: Synthetic packaging fixture\n`, + ); + const binaryContents = + "#!/bin/sh\nprintf '%s\\n' 'synthetic Cap binary'\n"; + for (const binary of ["Cap", "cap-cli", "cap-muxer"]) { + await writeFile(path.join(payload, "usr/bin", binary), binaryContents, { + mode: 0o755, + }); + } + await writeFile( + path.join(payload, "usr/lib/cap/package-format"), + "deb\n", + ); + run("bsdtar", [ + "-czf", + path.join(directory, "control.tar.gz"), + "-C", + control, + ".", + ]); + run("bsdtar", [ + "-czf", + path.join(directory, "data.tar.gz"), + "-C", + payload, + ".", + ]); + const deb = path.join(directory, "Cap.deb"); + run( + "ar", + ["rcD", deb, "debian-binary", "control.tar.gz", "data.tar.gz"], + directory, + ); + run("bash", [script, deb, output]); + const files = (await readdir(output)).filter((file) => + file.endsWith(".pkg.tar.zst"), + ); + assert.equal(files.length, 1); + const artifact = path.join(output, files[0]); + const metadata = run("bsdtar", ["-xOf", artifact, ".PKGINFO"]); + const pkgver = /^pkgver = (.+)$/m.exec(metadata)?.[1]; + assert.ok(pkgver); + packages.push(pkgver); + assert.match(metadata, /^arch = x86_64$/m); + assert.ok( + metadata.includes(`provides = cap=${pkgver.replace(/-1$/, "")}\n`), + ); + assert.equal( + run("bsdtar", ["-xOf", artifact, "usr/lib/cap/package-format"]), + "arch", + ); + const extracted = path.join(directory, "extracted"); + await mkdir(extracted); + run("bsdtar", ["-xf", artifact, "-C", extracted]); + for (const binary of ["Cap", "cap-cli", "cap-muxer"]) { + assert.equal( + await readFile(path.join(extracted, "usr/bin", binary), "utf8"), + binaryContents, + ); + assert.equal( + run(path.join(extracted, "usr/bin", binary), []), + "synthetic Cap binary", + ); + } + } + for (const prerelease of packages.slice(0, -1)) { + assert.equal(run("vercmp", [prerelease, packages.at(-1)]), "-1"); + } + assert.equal(run("vercmp", [packages[1], packages[0]]), "1"); + assert.equal( + run("vercmp", [packages[1], "0.6.2_nightly.202610210300-1"]), + "1", + ); + assert.equal(run("vercmp", [packages.at(-1), "0.6.2-1"]), "1"); + }, +); From 38e8ad80d9956cd1604a91d6697115415c2933d6 Mon Sep 17 00:00:00 2001 From: Akshar Patel <123344143+AksharP5@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:19:50 -0400 Subject: [PATCH 2/3] fix(build): omit epoch from Arch download filenames --- packaging/linux/README.md | 6 ++++-- scripts/build-linux-arch-package.sh | 3 +++ scripts/build-linux-arch-package.test.mjs | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packaging/linux/README.md b/packaging/linux/README.md index 138bcc76b43..cb246aa71a8 100644 --- a/packaging/linux/README.md +++ b/packaging/linux/README.md @@ -33,10 +33,12 @@ This removes any old signature. The host must provide its Wayland client and Pip Run the package builder as an ordinary user in an Arch environment with `makepkg` and `bsdtar`: ```sh -bash scripts/build-linux-arch-package.sh Cap_0.6.0_amd64.deb ./arch-packages -sudo pacman -U ./arch-packages/cap-bin-0.6.0-1-x86_64.pkg.tar.zst +bash scripts/build-linux-arch-package.sh Cap_0.6.2_amd64.deb ./arch-packages +sudo pacman -U ./arch-packages/cap-bin-0.6.2-1-x86_64.pkg.tar.zst ``` +The output filename omits the epoch. Package metadata retains version `1:0.6.2-1` in this example, which `pacman` uses for upgrade ordering. + The builder reuses the exact DEB payload, changes the package-format marker, and declares Arch dependencies. It does not strip or rebuild the executables. Release CI uses a pinned Arch container without network access during packaging. On Omarchy, screen capture uses the installed Hyprland desktop portal and PipeWire. Package installation alone does not verify recording, playback, export, or device access; these require tests in the target desktop session. diff --git a/scripts/build-linux-arch-package.sh b/scripts/build-linux-arch-package.sh index 7fcd76e2490..4d1e1e10500 100644 --- a/scripts/build-linux-arch-package.sh +++ b/scripts/build-linux-arch-package.sh @@ -71,3 +71,6 @@ EOF cd "$work" PKGDEST="$output" makepkg --nodeps --noconfirm +package="$(PKGDEST="$output" makepkg --packagelist)" +filename="${package##*/}" +mv -- "$package" "$output/cap-bin-${filename#*:}" diff --git a/scripts/build-linux-arch-package.test.mjs b/scripts/build-linux-arch-package.test.mjs index 1d45953c7af..1e8e93f4cd0 100644 --- a/scripts/build-linux-arch-package.test.mjs +++ b/scripts/build-linux-arch-package.test.mjs @@ -127,6 +127,11 @@ test( const metadata = run("bsdtar", ["-xOf", artifact, ".PKGINFO"]); const pkgver = /^pkgver = (.+)$/m.exec(metadata)?.[1]; assert.ok(pkgver); + assert.match(pkgver, /^1:/); + assert.equal( + files[0], + `cap-bin-${pkgver.replace(/^1:/, "")}-x86_64.pkg.tar.zst`, + ); packages.push(pkgver); assert.match(metadata, /^arch = x86_64$/m); assert.ok( From 475380419fb921aeee9e14d824e17c2819e0ce32 Mon Sep 17 00:00:00 2001 From: Akshar Patel <123344143+AksharP5@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:25:23 -0400 Subject: [PATCH 3/3] chore(ci): pin checkout in Arch package checks --- .github/workflows/arch-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/arch-package.yml b/.github/workflows/arch-package.yml index f3741b0bd6a..e7e20ad5116 100644 --- a/.github/workflows/arch-package.yml +++ b/.github/workflows/arch-package.yml @@ -16,7 +16,7 @@ jobs: runs-on: blacksmith-2vcpu-ubuntu-2404 container: archlinux:base-devel@sha256:68bfc3b0d277b08a99101dc9b94aaa03e5ae70cf1b4fb965c03b2b87b915760d steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - name: Install Node.js