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
164 changes: 164 additions & 0 deletions .github/workflows/build-aarch64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
name: aarch64 package repo

# Build an aarch64 Omarchy repository for manual or reusable workflows.

on:
workflow_dispatch:
inputs:
packages:
description: >-
Space-separated recipe directory names (empty = every eligible package).
Include required recipes from this repository; dependencies are not added automatically.
type: string
default: ""
mirror:
description: "edge, rc or stable"
type: string
default: edge
workflow_call:
inputs:
pkgs_repository:
description: "owner/name of the omarchy-pkgs checkout to build"
type: string
required: true
pkgs_ref:
description: "branch, tag or SHA of that repository"
type: string
required: true
packages:
description: >-
Space-separated recipe directory names (empty = every eligible package).
Include required recipes from this repository; dependencies are not added automatically.
type: string
default: ""
mirror:
type: string
default: edge
outputs:
artifact:
description: "Name of the uploaded repository artifact"
value: ${{ jobs.build.outputs.artifact }}

jobs:
build:
runs-on: ubuntu-24.04-arm
timeout-minutes: 360
permissions:
contents: read
outputs:
artifact: ${{ steps.meta.outputs.artifact }}
env:
ARCH: aarch64
INPUT_MIRROR: ${{ inputs.mirror }}
INPUT_PACKAGES: ${{ inputs.packages }}
steps:
- name: Checkout omarchy-pkgs
uses: actions/checkout@v4
with:
repository: ${{ inputs.pkgs_repository || github.repository }}
ref: ${{ inputs.pkgs_ref || github.ref }}
persist-credentials: false

- name: Resolve inputs
id: meta
run: |
MIRROR="${INPUT_MIRROR:-edge}"
PACKAGES="$INPUT_PACKAGES"
case "$MIRROR" in edge|rc|stable) ;; *) echo "invalid mirror: $MIRROR" >&2; exit 1 ;; esac
re='^[A-Za-z0-9._+ -]*$'
if ! [[ $PACKAGES =~ $re ]]; then echo "invalid package list" >&2; exit 1; fi
read -r -a packages <<< "$PACKAGES"
for package in "${packages[@]}"; do
if ! [[ $package =~ ^[A-Za-z0-9][A-Za-z0-9._+-]*$ && -f pkgbuilds/$package/PKGBUILD ]]; then
echo "unknown recipe: $package" >&2
exit 1
fi
done
PACKAGES="${packages[*]}"
{
echo "MIRROR=$MIRROR"
echo "PACKAGES=$PACKAGES"
echo "REPO_DIR=pkgs.omarchy.org/$MIRROR/$ARCH"
echo "OUTPUT_DIR=build-output/$MIRROR/$ARCH"
} >> "$GITHUB_ENV"
echo "artifact=omarchy-repo-$ARCH-$MIRROR" >> "$GITHUB_OUTPUT"
echo "packages_key=$(printf '%s\0' "${packages[@]}" | sha256sum | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"

# Reuse packages whose build inputs have not changed.
- name: Restore repository tree
id: cache
uses: actions/cache@v4
with:
path: ${{ env.REPO_DIR }}
key: omarchy-repo-v2-${{ env.ARCH }}-${{ env.MIRROR }}-${{ steps.meta.outputs.packages_key }}-${{ hashFiles('pkgbuilds/**', 'build/**', 'helpers/**', 'bin/**') }}

# The builder and runner use different user IDs.
- name: Prepare output directories
run: |
mkdir -p "$OUTPUT_DIR" "$REPO_DIR" src logs
chmod -R a+rwX build-output pkgs.omarchy.org src

- name: Build plan
run: |
read -r -a packages <<< "$PACKAGES"
bin/repo build --arch "$ARCH" --mirror "$MIRROR" --dry-run --package "${packages[@]}"

- name: Build packages
run: |
read -r -a packages <<< "$PACKAGES"
bin/repo build --arch "$ARCH" --mirror "$MIRROR" --package "${packages[@]}"

# Replaces bin/repo sign + promote, which need the repository host's key.
- name: Publish into the repository tree and write omarchy.db
run: |
shopt -s nullglob
files=("$OUTPUT_DIR"/*.pkg.tar.zst "$OUTPUT_DIR"/*.pkg.tar.xz)
echo "new package files: ${#files[@]}"
[[ ${#files[@]} -gt 0 ]] && cp -v "${files[@]}" "$REPO_DIR/"
chmod -R a+rwX pkgs.omarchy.org
bin/repo update --arch "$ARCH" --mirror "$MIRROR"

- name: Verify repository
id: verify
run: |
source helpers/package-metadata.sh
pkgbuilds="$PWD/pkgbuilds"
cd "$REPO_DIR"
dbdir=$(mktemp -d)
trap 'rm -rf "$dbdir"' EXIT
tar --use-compress-program=unzstd -xf omarchy.db.tar.zst -C "$dbdir"
for desc in "$dbdir"/*/desc; do
awk '
/^%BASE%$/ { getline; base=$0 }
/^%NAME%$/ { getline; name=$0 }
END { print (base != "" ? base : name) }
' "$desc"
done > "$dbdir/pkgbases"
echo "== pkgbases in omarchy.db =="
sort -u "$dbdir/pkgbases"
missing=0
read -r -a packages <<< "$PACKAGES"
for package in "${packages[@]}"; do
pkgbase=$(package_pkgbuild_var "$pkgbuilds/$package" pkgbase)
[[ -n $pkgbase ]] || pkgbase=$(package_pkgbuild_var "$pkgbuilds/$package" pkgname)
grep -Fxq "$pkgbase" "$dbdir/pkgbases" || { echo "missing pkgbase: $pkgbase ($package)" >&2; missing=1; }
done
du -sh .
exit $missing

- name: Upload repository artifact
uses: actions/upload-artifact@v4
with:
name: ${{ steps.meta.outputs.artifact }}
path: ${{ env.REPO_DIR }}
if-no-files-found: error
retention-days: 7

- name: Upload build logs
if: always()
uses: actions/upload-artifact@v4
with:
name: build-logs-${{ env.ARCH }}-${{ env.MIRROR }}
path: logs/
if-no-files-found: ignore
retention-days: 7
9 changes: 8 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,16 @@ jobs:
-w /workspace \
archlinux:base-devel bash -lc '
set -euo pipefail
pacman -Syu --noconfirm git jq
pacman -Syu --noconfirm git jq python-yaml xz zstd
./bin/sync-upstream self-test
./bin/sync-rebuilds --self-test
./bin/omarchy-pkgs self-test
./bin/omarchy-release self-test
shopt -s nullglob
for test in pkgbuilds/*/test.sh; do
bash "$test"
done
if [[ -d test ]]; then
runuser -u nobody -- python -m unittest discover -s test -v
fi
'
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,7 @@ bin/repo release --package my-package
- Same workflow, just add `--arch aarch64`; the scheduled pipeline runs it
automatically once `aarch64` is in `PUBLISHED_ARCHES`
- Packages whose `arch=()` lacks `aarch64` are skipped, not failed
- For native CI builds, run the **aarch64 package repo** workflow.

### Building for Both Architectures

Expand Down
121 changes: 121 additions & 0 deletions test/test_build_aarch64.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Exercise workflow shell steps. Requires Bash, GNU tar, zstd and PyYAML."""

import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest

import yaml


WORKFLOW = yaml.safe_load(
(Path(__file__).resolve().parents[1] / ".github/workflows/build-aarch64.yml").read_text()
)
STEPS = {step["id"]: step for step in WORKFLOW["jobs"]["build"]["steps"] if "id" in step}


class BuildAarch64Test(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
package = self.root / "pkgbuilds/example/PKGBUILD"
package.parent.mkdir(parents=True)
package.write_text("pkgname=example\n")
helpers = self.root / "helpers"
helpers.mkdir()
shutil.copy(Path(__file__).resolve().parents[1] / "helpers/package-metadata.sh", helpers)

def run_step(self, name, **env):
return subprocess.run(
["bash", "-euo", "pipefail", "-c", STEPS[name]["run"]],
cwd=self.root,
env={
**os.environ,
"ARCH": "aarch64",
"INPUT_MIRROR": "edge",
"INPUT_PACKAGES": "",
"GITHUB_ENV": str(self.root / "env"),
"GITHUB_OUTPUT": str(self.root / "output"),
**env,
},
capture_output=True,
text=True,
)

def test_empty_package_set_is_valid(self):
result = self.run_step("meta")
self.assertEqual(result.returncode, 0, result.stderr)

def test_checkout_does_not_persist_credentials(self):
checkout = next(step for step in WORKFLOW["jobs"]["build"]["steps"]
if step.get("uses", "").startswith("actions/checkout@"))
self.assertIs(checkout["with"].get("persist-credentials"), False)

def test_known_package_and_rc_mirror(self):
result = self.run_step("meta", INPUT_PACKAGES=" example ", INPUT_MIRROR="rc")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("PACKAGES=example\n", (self.root / "env").read_text())

def test_options_paths_unknown_packages_and_newlines_are_rejected(self):
for value in ("--dry-run", "..", "../example", "missing", "example\ninjected=value"):
with self.subTest(value=value):
result = self.run_step("meta", INPUT_PACKAGES=value)
self.assertNotEqual(result.returncode, 0)

def test_split_package_is_verified_by_pkgbase(self):
(self.root / "pkgbuilds/example/PKGBUILD").write_text("pkgbase=example-source\npkgname=(example-libs)\n")
repo = self.root / "repo"
desc = repo / "example-libs-1-1/desc"
desc.parent.mkdir(parents=True)
desc.write_text("%NAME%\nexample-libs\n\n%BASE%\nexample-source\n")
subprocess.run(
["tar", "--zstd", "-cf", "omarchy.db.tar.zst", desc.parent.name],
cwd=repo, check=True,
)
result = self.run_step("verify", REPO_DIR=str(repo), PACKAGES="example")
self.assertEqual(result.returncode, 0, result.stderr)
# A recipe without pkgbase uses pkgname, read for the target architecture.
(self.root / "pkgbuilds/example/PKGBUILD").write_text(
'[[ $CARCH == aarch64 ]] || return 1\npkgname=example-source\n'
)
result = self.run_step("verify", REPO_DIR=str(repo), PACKAGES="example")
self.assertEqual(result.returncode, 0, result.stderr)
(self.root / "pkgbuilds/example/PKGBUILD").write_text("pkgbase=exampl.-source\npkgname=(example-libs)\n")
result = self.run_step("verify", REPO_DIR=str(repo), PACKAGES="example")
self.assertNotEqual(result.returncode, 0)

def test_cache_includes_sources_and_package_selection(self):
cache = STEPS["cache"]["with"]
self.assertNotIn("restore-keys", cache)
self.assertIn("packages_key", cache["key"])
for path in ("pkgbuilds/**", "build/**", "helpers/**", "bin/**"):
self.assertIn(path, cache["key"])

@unittest.skipUnless(shutil.which("makepkg") and shutil.which("repo-add"), "requires Arch packaging tools")
def test_real_repository_records_pkgbase(self):
recipe = self.root / "pkgbuilds/example"
(recipe / "PKGBUILD").write_text('''pkgbase=example-source
pkgname=(example-libs)
pkgver=1
pkgrel=1
arch=(any)
license=(MIT)
package() {
install -Dm644 "$startdir/PKGBUILD" "$pkgdir/usr/share/example/PKGBUILD"
}
''')
subprocess.run(["makepkg", "--nodeps", "--noconfirm"], cwd=recipe, check=True,
stdout=subprocess.DEVNULL)
packages = list(recipe.glob("*.pkg.tar.zst"))
self.assertEqual(len(packages), 1)
subprocess.run(["repo-add", "omarchy.db.tar.zst", packages[0].name], cwd=recipe,
check=True, stdout=subprocess.DEVNULL)
result = self.run_step("verify", REPO_DIR=str(recipe), PACKAGES="example")
self.assertEqual(result.returncode, 0, result.stderr)


if __name__ == "__main__":
unittest.main()