Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/arch-package.yml
Original file line number Diff line number Diff line change
@@ -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@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
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
6 changes: 4 additions & 2 deletions packaging/linux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 7 additions & 2 deletions scripts/build-linux-arch-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,17 @@ cp "$deb" "$work/Cap.deb"
checksum="$(sha256sum "$work/Cap.deb" | cut -d ' ' -f 1)"
cat > "$work/PKGBUILD" <<EOF
pkgname=cap-bin
pkgver=${version//-/_}
pkgver=${version//-/pre}
pkgrel=1
# Retain this epoch so packages supersede the older underscore-format nightlies.
epoch=1
pkgdesc="Screen recording with Studio, Instant, and screenshot modes"
arch=("$architecture")
url="https://cap.so"
license=("AGPL-3.0-only")
depends=("libva" "libpulse" "libpipewire" "alsa-lib" "alsa-plugins" "libxkbcommon" "libxkbcommon-x11" "libx11" "openssl" "wayland" "libglvnd" "vulkan-icd-loader" "xdg-desktop-portal" "xdg-utils")
optdepends=("xdg-desktop-portal-hyprland: screen capture on Hyprland and Omarchy" "vulkan-driver: hardware accelerated recording and editing")
provides=("cap=\$pkgver")
provides=("cap=\$epoch:\$pkgver")
conflicts=("cap")
options=("!strip" "!debug")
source=("Cap.deb")
Expand All @@ -69,3 +71,6 @@ EOF

cd "$work"
PKGDEST="$output" makepkg --nodeps --noconfirm
package="$(PKGDEST="$output" makepkg --packagelist)"
filename="${package##*/}"
mv -- "$package" "$output/cap-bin-${filename#*:}"
168 changes: 168 additions & 0 deletions scripts/build-linux-arch-package.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
mkdir,
mkdtemp,
readdir,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";

const script = fileURLToPath(
new URL("./build-linux-arch-package.sh", import.meta.url),
);
const available = (command) =>
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 <fixture@example.invalid>"',
"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 <fixture@example.invalid>\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);
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(
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");
},
);
Loading