186 changed files with 3840 additions and 6170 deletions

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
const approvePrWorkflows = require('./approve-pr-workflows.cjs');
const BOT = 'github-actions[bot]';
// A sync workflow pushes its branch with GITHUB_TOKEN. GitHub holds the
// resulting pull_request runs for approval and, unlike a person's push,
// creates no pull_request_target run, so approve-pr.yml never sees it. The
// sync workflow therefore releases the runs for the commit it just pushed,
// under the same rule approve-pr.yml applies: only while build-approved is
// on the PR. The sync applies that label itself, so its pushes build without
// a maintainer. It acts only on its own bot-authored, same-repository PR for
// the branch and commit it pushed.
module.exports = async function approveSyncPush({ github, context, core,
number, branch, headSha, since, approve = approvePrWorkflows, ...options }) {
if (!Number.isInteger(number) || !branch || !headSha || !since) {
throw new Error('Missing sync PR number, branch, head SHA or push time.');
}
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: number });
const repository = `${context.repo.owner}/${context.repo.repo}`;
if (pr.user?.login !== BOT || pr.head.repo?.full_name !== repository ||
pr.base.repo?.full_name !== repository || pr.head.ref !== branch) {
throw new Error(`PR #${number} is not ${BOT}'s ${branch} PR in ${repository}; refusing to approve.`);
}
if (pr.state !== 'open' || pr.head.sha !== headSha) {
core.info(`PR #${number} is closed or has moved past ${headSha}; nothing to approve.`);
return;
}
if (!pr.labels.some(label => label.name === 'build-approved')) {
core.info(`PR #${number} has no build-approved label; its runs stay held.`);
return;
}
await approve({ github, context, core, vouchStatus: 'bot', pullRequest: pr,
action: 'synchronize', since, ...options });
};
+8
View File
@@ -14,6 +14,9 @@
// test.yml runs on PRs only; an existing test may also run on master
// (builder-images.yml runs tests/build-isolation.sh with packages: write),
// so editing one still needs a maintainer.
// - Not the upstream sync. It labels its own PR build-approved to release
// GitHub's hold on its pushes, which is no one's approval; it stays on the
// reviewed lane.
const PACKAGES = 'pkgbuilds/';
const TESTS = 'tests/';
const TEST_WORKFLOW = '.github/workflows/test.yml';
@@ -35,6 +38,7 @@ function packageChange(file) {
if (file.filename === TEST_WORKFLOW) return file.status === 'modified' && onlyRunsTests(file.patch);
return false;
}
const REVIEWED_BRANCHES = /^auto\/sync-upstream(\/|$)/;
function decide({ pr, files, vouchStatus, repository }) {
if (pr.state !== 'open') return { enable: false, reason: 'PR is not open' };
@@ -51,6 +55,10 @@ function decide({ pr, files, vouchStatus, repository }) {
return { enable: false, reason: `author not trusted to build (${vouchStatus || 'missing'}${labelled ? ', labelled' : ''})` };
}
if (pr.head.repo?.full_name === repository && REVIEWED_BRANCHES.test(pr.head.ref)) {
return { enable: false, reason: `${pr.head.ref} stays on the reviewed lane` };
}
if (!files.length) return { enable: false, reason: 'PR changes no files' };
const outside = files.filter(file => !packageChange(file));
if (outside.length) {
-36
View File
@@ -1,36 +0,0 @@
#!/bin/bash
# Usage: package-pr-groups.sh BRANCH_PREFIX PATCH_OUT
#
# Run from the repository root after a sync has rewritten recipes in the
# working tree. Writes those changes to PATCH_OUT and prints, as one JSON
# line, the PRs they split into: [{"branch": ..., "packages": "a b"}, ...].
#
# One PR per package, so a package whose build fails holds back only itself.
# Packages pinned from the same upstream branch (omarchy-dev and
# omarchy-settings-dev) share a PR: sync-upstream moves them in lockstep, and
# merging one without the other would ship the pair from different commits.
# Branch names come from sync-pr-branch.sh, so a scheduled run and a dispatch
# naming the same packages update the same PR.
set -euo pipefail
prefix=${1:?branch prefix required}
patch=${2:?patch path required}
scripts=$(dirname "$0")
git diff --binary -- pkgbuilds > "$patch"
declare -A members=()
while IFS= read -r package; do
key=$(jq -r '(.upstream.watch? | objects | select(has("git_branch")) | "\(.git_branch)#\(.branch)") // empty' \
"pkgbuilds/$package/.omarchy/package.json" 2>/dev/null || true)
members["${key:-$package}"]+="$package "
done < <(git diff --name-only -- pkgbuilds | cut -d/ -f2 | sort -u)
groups='[]'
for key in "${!members[@]}"; do
read -r -a packages <<<"${members[$key]}"
branch=$("$scripts/sync-pr-branch.sh" "$prefix" "${packages[@]}" | sed -n 's/^branch=//p')
groups=$(jq -c --arg branch "$branch" --arg packages "${packages[*]}" \
'. + [{branch: $branch, packages: $packages}]' <<<"$groups")
done
jq -c 'sort_by(.branch)' <<<"$groups"
+8 -10
View File
@@ -20,18 +20,16 @@ on:
description: "Space-separated package directories to build"
required: true
# A new push normally cancels the PR's in-flight build. The bots' branches
# (auto/sync-* and auto/track-branches-*) are the exception: they are
# force-pushed with fresh upstream releases several times a day, which kept
# cancelling multi-hour aarch64 builds before they could finish. There the
# newest run waits instead (GitHub keeps at most one pending run per group,
# replacing older pending ones), and when it starts it reuses every artifact
# the finished build uploaded, so only packages whose tree changed are built
# again. The tracker labels the PR it opens, so a new tracker PR starts two
# runs at once; cancelling the first left a red `result` on the PR (#901).
# A new push normally cancels the PR's in-flight build. The sync bots'
# branches (auto/sync-*) are the exception: they are force-pushed with fresh
# upstream releases several times a day, which kept cancelling multi-hour
# aarch64 builds before they could finish. There the newest run waits
# instead (GitHub keeps at most one pending run per group, replacing older
# pending ones), and when it starts it reuses every artifact the finished
# build uploaded, so only packages whose tree changed are built again.
concurrency:
group: build-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (startsWith(github.head_ref, 'auto/sync-') || startsWith(github.head_ref, 'auto/track-branches'))) }}
cancel-in-progress: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'auto/sync-')) }}
jobs:
# Builds cost real machines, so they run only for trusted authors:
-134
View File
@@ -1,134 +0,0 @@
name: Open package PRs
# Shared by the three bot lanes (sync-upstream.yml, sync-rebuilds.yml and
# track-branches.yml). The caller's sync job rewrites recipes, groups the
# changes with .github/scripts/package-pr-groups.sh and uploads them as the
# `package-pins` artifact. This opens or updates one PR per group and arms
# auto-merge, so every green package lands on its own and only the failures
# stay open. A single batch PR let one red package hold back every other one
# (#788, #837).
#
# Everything goes through the PAT in PKGS_BOT_TOKEN: its pushes build without
# an approval hold, and a merge made with it starts publish.yml. A merge made
# with the built-in GITHUB_TOKEN would not publish.
on:
workflow_call:
inputs:
groups:
description: 'JSON from package-pr-groups.sh: [{"branch", "packages"}]'
required: true
type: string
prefix:
description: 'Branch prefix the caller owns, e.g. auto/sync-upstream'
required: true
type: string
title:
description: 'PR title; ": <package> <version>, ..." is appended'
required: true
type: string
body:
description: 'PR body'
required: true
type: string
secrets:
PKGS_BOT_TOKEN:
required: true
permissions:
contents: read
jobs:
pr:
if: ${{ inputs.groups != '[]' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 4
matrix:
group: ${{ fromJSON(inputs.groups) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Fetch the pins
uses: actions/download-artifact@v4
with:
name: package-pins
path: ${{ runner.temp }}
# The PR title names what moved, so the merged history reads like a
# changelog: "Sync upstream releases: zed 1.23.2-1".
- name: Apply this PR's packages
id: apply
env:
PACKAGES: ${{ matrix.group.packages }}
TITLE: ${{ inputs.title }}
run: |
read -r -a packages <<<"$PACKAGES"
git apply $(printf -- '--include=pkgbuilds/%s/* ' "${packages[@]}") "$RUNNER_TEMP/pins.patch"
git status --porcelain
field() { sed -n "s/^$2=//p" "pkgbuilds/$1/PKGBUILD" | head -1 | tr -d "\"'"; }
versions=$(for package in "${packages[@]}"; do
printf '%s %s-%s, ' "$package" "$(field "$package" pkgver)" "$(field "$package" pkgrel)"
done)
title="$TITLE: ${versions%, }"
echo "title=$title" >> "$GITHUB_OUTPUT"
echo "- $title" >> "$GITHUB_STEP_SUMMARY"
- name: Open or update the PR
id: pr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.PKGS_BOT_TOKEN }}
commit-message: ${{ steps.apply.outputs.title }}
title: ${{ steps.apply.outputs.title }}
body: ${{ inputs.body }}
branch: ${{ matrix.group.branch }}
delete-branch: true
labels: automated
# Auto-merge, not a direct merge: branch protection still has to see
# `result`, `self-tests` and `build-isolation` green. auto-merge-pr.yml
# may arm the same PR at the same moment; either one arming it is the
# outcome wanted.
- name: Enable auto-merge
if: ${{ steps.pr.outputs.pull-request-number != '' }}
env:
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
PR: ${{ steps.pr.outputs.pull-request-number }}
run: |
armed() { [[ "$(gh pr view "$PR" -R "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; }
if armed; then
echo "auto-merge already enabled on #$PR"
exit 0
fi
gh pr merge --auto --merge "$PR" -R "$GITHUB_REPOSITORY" || armed
# A PR on this lane's prefix that the run did not refresh, and that
# conflicts with master, is stale: master moved its packages on without it.
# Close it rather than leave a red PR behind. Not being refreshed is not
# enough on its own, since a feed that failed this run (one of dozens timing
# out is routine) leaves a still-valid PR unrefreshed too. The lane's old
# batch PR, on the bare prefix, is retired outright.
close-superseded:
runs-on: ubuntu-latest
steps:
- name: Close superseded PRs
env:
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
GROUPS_JSON: ${{ inputs.groups }}
PREFIX: ${{ inputs.prefix }}
run: |
gh pr list -R "$GITHUB_REPOSITORY" --state open --limit 500 --json number,headRefName,mergeable \
--jq '.[] | select(.headRefName == env.PREFIX or (.headRefName | startswith(env.PREFIX + "-"))) | "\(.number) \(.headRefName) \(.mergeable)"' |
while read -r number branch mergeable; do
jq -e --arg branch "$branch" 'any(.[]; .branch == $branch)' <<<"$GROUPS_JSON" >/dev/null && continue
[[ $branch == "$PREFIX" || $mergeable == CONFLICTING ]] || continue
echo "closing #$number ($branch): superseded"
gh pr close "$number" -R "$GITHUB_REPOSITORY" --delete-branch \
--comment "Superseded: master has moved these packages on, or per-package PRs carry them now."
done
-4
View File
@@ -168,13 +168,9 @@ jobs:
runs-on: ubuntu-latest
environment: publish
timeout-minutes: 30
# queue: max, because by default a group holds one waiting job: a third
# merge arriving cancels the second one's publish, and nothing reruns it.
# With it up to 100 wait, in the order they reach this job.
concurrency:
group: publish
cancel-in-progress: false
queue: max
steps:
- uses: actions/checkout@v4
with:
+60 -56
View File
@@ -1,11 +1,9 @@
name: Sync Rebuild Triggers
# Rebuilds ride the unattended lane, like track-branches.yml: each package's
# pkgrel bump gets its own PR on auto/sync-rebuilds-<package>, opened by
# package-prs.yml. It builds on the droplets, auto-merge lands it once
# `result`, `self-tests` and `build-isolation` are green, and the merge
# publishes. A rebuild that fails stays an unmerged red PR for a maintainer
# without holding back any other package.
# Rebuilds ride the unattended lane, like track-branches.yml: the pkgrel bump
# PR builds on the droplets, auto-merge lands it once `result`, `self-tests`
# and `build-isolation` are green, and the merge publishes. A rebuild that
# fails stays an unmerged red PR for a maintainer.
#
# The PR and auto-merge use the PAT in PKGS_BOT_TOKEN, because a merge made
# with the built-in GITHUB_TOKEN does not start publish.yml, and its pushes
@@ -22,19 +20,12 @@ on:
required: false
default: ''
# One sync at a time: two runs racing on one auto/sync-rebuilds-* branch
# would each force-push their own bump over the other's.
concurrency:
group: sync-rebuilds
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
groups: ${{ steps.groups.outputs.groups }}
contents: write
pull-requests: write
steps:
- name: Require the bot token
@@ -51,13 +42,23 @@ jobs:
with:
persist-credentials: false
# A scoped dispatch regenerates only the named packages. Pushed to the
# shared branch, that would replace every other pending update in its
# PR, so it gets a branch and PR of its own.
- name: Choose the PR branch
id: branch
env:
PACKAGES: ${{ github.event.inputs.packages }}
run: |
read -r -a package_args <<< "${PACKAGES:-}"
.github/scripts/sync-pr-branch.sh auto/sync-rebuilds "${package_args[@]}" | tee -a "$GITHUB_OUTPUT"
# Runs in an Arch container against the mirror the x86_64 builder itself
# uses, because the question being asked is what that builder will link
# against and a different mirror can be hours ahead of it. Recording a
# version the build never saw is the one failure this command must not
# have: nothing re-fires once the record matches.
- name: Bump pkgrel for packages whose dependencies moved
id: sync
run: |
docker run --rm \
-e PACKAGES="$PACKAGES" \
@@ -67,7 +68,7 @@ jobs:
-v "$PWD/helpers:/workspace/helpers:ro" \
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
-w /workspace \
ghcr.io/archlinux/archlinux:base-devel bash -lc '
archlinux:base-devel bash -lc '
set -euo pipefail
printf "Server = https://mirror.omarchy.org/\$repo/os/\$arch\n" > /etc/pacman.d/mirrorlist
@@ -87,53 +88,56 @@ jobs:
env:
PACKAGES: ${{ github.event.inputs.packages }}
- name: Group the bumps into PRs
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
id: groups
- name: Check for changes
id: changes
run: |
groups=$(.github/scripts/package-pr-groups.sh auto/sync-rebuilds "$RUNNER_TEMP/pins.patch")
echo "groups=$groups" >> "$GITHUB_OUTPUT"
if [ -z "$(git status --porcelain)" ]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Hand the bumps to the PR jobs
if: ${{ !cancelled() && steps.groups.outputs.groups != '' && steps.groups.outputs.groups != '[]' }}
uses: actions/upload-artifact@v4
- name: Create Pull Request
if: steps.changes.outputs.has_changes == 'true'
id: cpr
uses: peter-evans/create-pull-request@v7
with:
name: package-pins
path: ${{ runner.temp }}/pins.patch
retention-days: 1
token: ${{ secrets.PKGS_BOT_TOKEN }}
commit-message: 'chore: rebuild against updated dependencies'
title: "chore: rebuild against updated dependencies${{ steps.branch.outputs.scope && format(' ({0})', steps.branch.outputs.scope) || '' }}"
body: |
Automated pkgrel bump for packages that link against a dependency
which has moved in the official repositories.
prs:
needs: sync
if: ${{ !cancelled() && needs.sync.outputs.groups != '' }}
permissions:
contents: read
uses: ./.github/workflows/package-prs.yml
with:
groups: ${{ needs.sync.outputs.groups }}
prefix: auto/sync-rebuilds
title: Rebuild against updated dependencies
body: |
Automated pkgrel bump for a package that links against a dependency
which has moved in the official repositories.
Each package names those dependencies in `rebuild_on` and carries the
versions its current pkgrel was bumped for in `rebuilt_against`. The
bump is what makes the rebuilt package an upgrade pacman will offer;
without it the build produces the version already published and no
one receives it.
Each package names those dependencies in `rebuild_on` and carries the
versions its current pkgrel was bumped for in `rebuilt_against`. The
bump is what makes the rebuilt package an upgrade pacman will offer;
without it the build produces the version already published and no
one receives it.
This PR auto-merges once the build checks pass. A failing rebuild
leaves it open for a maintainer.
branch: ${{ steps.branch.outputs.branch }}
delete-branch: true
labels: automated
This PR auto-merges once the build checks pass. A failing rebuild
leaves it open for a maintainer without holding back any other package.
secrets:
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
# Auto-merge, not a direct merge: branch protection still has to see
# the build checks green before the rebuild lands.
- name: Enable auto-merge
if: steps.cpr.outputs.pull-request-number != ''
env:
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
PR: ${{ steps.cpr.outputs.pull-request-number }}
run: |
# Idempotent across re-runs of an updated PR: enabling twice errors.
if [[ "$(gh pr view "$PR" -R "${{ github.repository }}" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; then
echo "auto-merge already enabled on #$PR"
exit 0
fi
gh pr merge --auto --merge "$PR" -R "${{ github.repository }}"
notify:
needs: [sync, prs]
if: ${{ failure() }}
runs-on: ubuntu-latest
steps:
- name: Notify Basecamp on failure
if: ${{ env.BASECAMP_CHATBOT_URL != '' }}
if: failure() && env.BASECAMP_CHATBOT_URL != ''
env:
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
run: |
+91 -69
View File
@@ -1,17 +1,5 @@
name: Sync Upstream Releases
# Bumps every package whose upstream release feed has moved, except the
# "auto_merge": true packages that track-branches.yml checks hourly. Each
# package gets its own PR on auto/sync-upstream-<package>, opened by
# package-prs.yml: the PR builds on the droplets, auto-merge lands it when
# `result` is green, and the merge publishes. A package that fails to build
# stays an open red PR for a maintainer without holding back any other; the
# next run updates it if upstream moves again. `min_release_age` still
# quarantines fresh releases before they are proposed.
#
# The PRs and auto-merge use the PAT in PKGS_BOT_TOKEN, so their pushes
# build without an approval hold and their merges start publish.yml.
on:
schedule:
# Every 6 hours, off the hour to dodge the scheduling backlog at :00
@@ -23,35 +11,36 @@ on:
required: false
default: ''
# One sync at a time: two runs racing on one auto/sync-upstream-* branch
# would each force-push their own update over the other's.
concurrency:
group: sync-upstream
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: read
contents: write
pull-requests: write
outputs:
groups: ${{ steps.groups.outputs.groups }}
branch: ${{ steps.branch.outputs.branch }}
pushed_at: ${{ steps.pushed.outputs.at }}
number: ${{ steps.cpr.outputs.pull-request-number }}
operation: ${{ steps.cpr.outputs.pull-request-operation }}
head_sha: ${{ steps.cpr.outputs.pull-request-head-sha }}
steps:
- name: Require the bot token
env:
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
run: |
if [[ -z "$PKGS_BOT_TOKEN" ]]; then
echo "::error::Set PKGS_BOT_TOKEN to a PAT with Contents: write and Pull requests: write on this repository, owned by an account trusted to trigger builds."
exit 1
fi
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
# A scoped dispatch regenerates only the named packages. Pushed to the
# shared branch, that would replace every other pending update in its
# PR, so it gets a branch and PR of its own.
- name: Choose the PR branch
id: branch
env:
PACKAGES: ${{ github.event.inputs.packages }}
run: |
read -r -a package_args <<< "${PACKAGES:-}"
.github/scripts/sync-pr-branch.sh auto/sync-upstream "${package_args[@]}" | tee -a "$GITHUB_OUTPUT"
# Runs in an Arch container for vercmp: whether a release is an upgrade has
# to be decided by the same comparator pacman will use on users' machines.
- name: Update packages from upstream release feeds
@@ -66,7 +55,7 @@ jobs:
-v "$PWD/helpers:/workspace/helpers:ro" \
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
-w /workspace \
ghcr.io/archlinux/archlinux:base-devel bash -lc '
archlinux:base-devel bash -lc '
set -euo pipefail
pacman -Syu --noconfirm git jq python libarchive
@@ -75,8 +64,8 @@ jobs:
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
chown -R runner:runner /workspace/pkgbuilds
# Packages marked auto_merge ride track-branches.yml instead,
# which checks them hourly.
# The reviewed lane only: packages marked auto_merge ride
# track-branches.yml, which merges without a human.
if [[ -n "${PACKAGES:-}" ]]; then
read -r -a package_args <<< "$PACKAGES"
runuser -u runner -- ./bin/sync-upstream --lane reviewed "${package_args[@]}"
@@ -89,50 +78,50 @@ jobs:
UPSTREAM_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Failed feeds leave their recipes untouched; completed updates still
# get their PRs. The failed sync step keeps the workflow red and notifies.
- name: Group the updates into PRs
# reach review. The failed sync step keeps the workflow red and notifies.
- name: Check for changes
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
id: groups
id: changes
run: |
groups=$(.github/scripts/package-pr-groups.sh auto/sync-upstream "$RUNNER_TEMP/pins.patch")
echo "groups=$groups" >> "$GITHUB_OUTPUT"
if [ -z "$(git status --porcelain)" ]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Hand the updates to the PR jobs
if: ${{ !cancelled() && steps.groups.outputs.groups != '' && steps.groups.outputs.groups != '[]' }}
uses: actions/upload-artifact@v4
# Runs created by this push are newer than this; the approve job
# waits for them. A minute's slack absorbs runner clock skew.
- name: Record push time
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
id: pushed
run: echo "at=$(date -u -d '-1 minute' +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Create Pull Request
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
id: cpr
uses: peter-evans/create-pull-request@v7
with:
name: package-pins
path: ${{ runner.temp }}/pins.patch
retention-days: 1
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync upstream releases'
title: "chore: sync upstream releases${{ steps.branch.outputs.scope && format(' ({0})', steps.branch.outputs.scope) || '' }}"
body: |
Automated update of packages that track an upstream vendor release
feed rather than the AUR.
prs:
needs: sync
if: ${{ !cancelled() && needs.sync.outputs.groups != '' }}
permissions:
contents: read
uses: ./.github/workflows/package-prs.yml
with:
groups: ${{ needs.sync.outputs.groups }}
prefix: auto/sync-upstream
title: Sync upstream releases
body: |
Automated update of a package that tracks an upstream vendor release
feed rather than the AUR.
Release watches and providers are declared in `.omarchy/package.json`;
exceptional feeds use `.omarchy/upstream.sh`. Failed package updates
are left untouched; check the workflow result for outstanding failures.
branch: ${{ steps.branch.outputs.branch }}
delete-branch: true
# The bot is trusted; build-approved lets the approve job below
# release GitHub's hold on its pushes without a maintainer.
labels: |
automated
build-approved
reviewers: ryanrhughes
Release watches and providers are declared in `.omarchy/package.json`;
exceptional feeds use `.omarchy/upstream.sh`. This PR auto-merges once
the build checks pass. A failing build leaves it open for a maintainer
without holding back any other package.
secrets:
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
notify:
needs: [sync, prs]
if: ${{ failure() }}
runs-on: ubuntu-latest
steps:
- name: Notify Basecamp on failure
if: ${{ env.BASECAMP_CHATBOT_URL != '' }}
if: failure() && env.BASECAMP_CHATBOT_URL != ''
env:
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
run: |
@@ -142,3 +131,36 @@ jobs:
"🔴 <strong>Upstream sync failed</strong><br><a href=\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\">View run</a>" \
'{content: $content}')" \
"$BASECAMP_CHATBOT_URL"
# GitHub holds pull_request runs from a GITHUB_TOKEN push for approval and
# creates no pull_request_target run for it, so approve-pr.yml never sees
# the sync's own pushes. The sync labels its PR build-approved, so release
# the held runs for the commit just pushed, whether it opened the PR or
# updated it. A separate job, so the sync container's token never holds
# actions: write.
approve:
needs: sync
if: ${{ !cancelled() && (needs.sync.outputs.operation == 'created' || needs.sync.outputs.operation == 'updated') }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
actions: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Release held build and test runs
uses: actions/github-script@v7
env:
NUMBER: ${{ needs.sync.outputs.number }}
BRANCH: ${{ needs.sync.outputs.branch }}
HEAD_SHA: ${{ needs.sync.outputs.head_sha }}
SINCE: ${{ needs.sync.outputs.pushed_at }}
with:
script: |
const approve = require('./.github/scripts/approve-sync-push.cjs');
const { NUMBER, BRANCH, HEAD_SHA, SINCE } = process.env;
await approve({ github, context, core, number: Number(NUMBER),
branch: BRANCH, headSha: HEAD_SHA, since: SINCE });
+1 -8
View File
@@ -45,12 +45,10 @@ jobs:
docker run --rm \
-v "$PWD:/workspace:ro" \
-w /workspace \
ghcr.io/archlinux/archlinux:base-devel bash -lc '
archlinux:base-devel bash -lc '
set -euo pipefail
pacman -Syu --noconfirm git jq python libarchive neovim tmux
python tests/owe-package-restart.py
python tests/oma-service-removal.py
python tests/passwordless-package-lifecycle.py
python tests/upstream-watch.py
bash tests/ipu7-install.sh
bash tests/ipu7-headers.sh
@@ -64,7 +62,6 @@ jobs:
./tests/published-build-plan.sh
./tests/settings-boot-config.sh
./tests/settings-runtime-profile.sh
./tests/passwordless-package-files.sh
./tests/pinned-sources.sh
./tests/controller.sh
./tests/artifact-helpers.sh
@@ -73,8 +70,4 @@ jobs:
./tests/superwhisper-bin-install.sh
pacman -S --noconfirm --quiet rclone >/dev/null
./tests/publish-artifact.sh
shopt -s nullglob
for test in pkgbuilds/*/test.sh; do
bash "$test"
done
'
+70 -62
View File
@@ -1,21 +1,13 @@
name: Track upstream branches
# The hourly lane. Packages marked "auto_merge": true update here instead of
# in sync-upstream.yml's 6-hourly run: some follow a moving upstream branch
# (omarchy-dev and omarchy-settings-dev on quattro, omasnap-git on main)
# rather than tagged releases, so nothing in this repository changes when
# their source does; the rest are release feeds worth shipping within the
# hour (browsers, large vendors, Omacom's own projects). This workflow makes
# each new branch tip a commit pin in the recipe, which publish.yml then
# treats like any other version bump: the PR builds on the droplets,
# auto-merge lands it when `result` is green, and the merge publishes the
# artifacts. A tip that fails to build stays an unmerged red PR that the next
# tick supersedes.
#
# One PR per package, on auto/track-branches-<package>, opened by
# package-prs.yml. Packages pinned from the same upstream branch
# (omarchy-dev and omarchy-settings-dev) share one, because sync-upstream
# moves them in lockstep.
# The unattended lane. Packages marked "auto_merge": true follow a moving
# upstream branch (omarchy-dev and omarchy-settings-dev on quattro, omasnap-git
# on main) rather than tagged releases, so nothing in this repository changes
# when their source does. This workflow makes each new branch tip a commit pin
# in the recipe, which publish.yml then treats like any other version bump:
# the PR builds on the droplets, auto-merge lands it when `result` is green,
# and the merge publishes the artifacts. A tip that fails to build stays an
# unmerged red PR that the next tick supersedes.
#
# The PR and auto-merge use the PAT in PKGS_BOT_TOKEN so they trigger the
# build and publish workflows. The built-in GITHUB_TOKEN cannot drive this
@@ -24,10 +16,8 @@ name: Track upstream branches
on:
schedule:
# Hourly, off the hour to dodge the scheduling backlog at :00. GitHub
# drops scheduled runs under load: the old every-2-hours entry fired
# every 6 to 9 hours in practice.
- cron: '35 * * * *'
# Every 2 hours, off the hour to dodge the scheduling backlog at :00
- cron: '35 */2 * * *'
workflow_dispatch:
inputs:
packages:
@@ -35,8 +25,8 @@ on:
required: false
default: ''
# One tracker at a time: two runs racing on one auto/track-branches-* branch
# would each force-push their own pin over the other's.
# One tracker at a time: two runs racing on auto/track-branches would each
# force-push their own pin over the other's.
concurrency:
group: track-branches
cancel-in-progress: false
@@ -46,8 +36,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
groups: ${{ steps.groups.outputs.groups }}
steps:
- name: Require the tracking token
@@ -64,7 +52,7 @@ jobs:
with:
persist-credentials: false
# Same container as sync-upstream.yml: vercmp decides whether a pin is
# Same container as the reviewed sync: vercmp decides whether a pin is
# an upgrade with the comparator pacman uses on users' machines.
- name: Pin tracked branches to their current tips
id: sync
@@ -78,7 +66,7 @@ jobs:
-v "$PWD/helpers:/workspace/helpers:ro" \
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
-w /workspace \
ghcr.io/archlinux/archlinux:base-devel bash -lc '
archlinux:base-devel bash -lc '
set -euo pipefail
pacman -Syu --noconfirm git jq python libarchive
@@ -98,50 +86,70 @@ jobs:
PACKAGES: ${{ github.event.inputs.packages }}
UPSTREAM_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Group the pins into PRs
- name: Check for changes
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
id: groups
id: changes
run: |
groups=$(.github/scripts/package-pr-groups.sh auto/track-branches "$RUNNER_TEMP/pins.patch")
echo "groups=$groups" >> "$GITHUB_OUTPUT"
if [ -z "$(git status --porcelain)" ]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
git status --porcelain
{
echo "### Pinned"
git diff --unified=0 -- 'pkgbuilds/*/PKGBUILD' | grep -E '^\+(pkgver|_commit)=' | sed 's/^+/- /'
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Hand the pins to the PR jobs
if: ${{ !cancelled() && steps.groups.outputs.groups != '' && steps.groups.outputs.groups != '[]' }}
uses: actions/upload-artifact@v4
# The PR title names what moved, so the merged history reads like a
# changelog: "Track upstream branches: omarchy-dev 4.0.0.r6520.g1a2b3c4, ...".
- name: Describe the pins
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
id: describe
run: |
title=$(git diff --unified=0 -- 'pkgbuilds/*/PKGBUILD' \
| awk '/^\+\+\+ b\/pkgbuilds\//{split($2,p,"/"); pkg=p[3]} /^\+pkgver=/{sub(/^\+pkgver=/,""); printf "%s %s, ", pkg, $0}' \
| sed 's/, $//')
echo "title=Track upstream branches: ${title}" >> "$GITHUB_OUTPUT"
- name: Open or update the tracking PR
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
id: pr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
name: package-pins
path: ${{ runner.temp }}/pins.patch
retention-days: 1
token: ${{ secrets.PKGS_BOT_TOKEN }}
commit-message: ${{ steps.describe.outputs.title }}
title: ${{ steps.describe.outputs.title }}
body: |
Automated pin of packages that follow a moving upstream branch
(`"auto_merge": true` in `.omarchy/package.json`). Each package's
`_commit` now points at the branch tip. Fresh tips wait until
their commit timestamp is at least `min_release_age` old.
prs:
needs: track
if: ${{ !cancelled() && needs.track.outputs.groups != '' }}
permissions:
contents: read
uses: ./.github/workflows/package-prs.yml
with:
groups: ${{ needs.track.outputs.groups }}
prefix: auto/track-branches
title: Track upstream branches
body: |
Automated pin of a package that follows a moving upstream branch or a
trusted release feed (`"auto_merge": true` in `.omarchy/package.json`).
Its recipe now points at the newest upstream release, or the newest
branch tip whose commit timestamp is at least `min_release_age` old.
This PR auto-merges once the build checks pass. A failing build
leaves it open; the next tracker run replaces it with the newer tip.
branch: auto/track-branches
delete-branch: true
labels: automated
This PR auto-merges once the build checks pass. A failing build leaves
it open without holding back any other package; the next tracker run
replaces it with the newer tip.
secrets:
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
# Auto-merge, not a direct merge: branch protection still has to see
# `result`, `self-tests` and `build-isolation` green, and this lane
# inherits every rule the reviewed lane has except the human.
- name: Enable auto-merge
if: ${{ !cancelled() && steps.pr.outputs.pull-request-number != '' }}
env:
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
PR: ${{ steps.pr.outputs.pull-request-number }}
run: |
# Idempotent across re-runs of an updated PR: enabling twice errors.
if [[ "$(gh pr view "$PR" -R "${{ github.repository }}" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; then
echo "auto-merge already enabled on #$PR"
exit 0
fi
gh pr merge --auto --merge "$PR" -R "${{ github.repository }}"
notify:
needs: [track, prs]
if: ${{ failure() }}
runs-on: ubuntu-latest
steps:
- name: Notify Basecamp on failure
if: ${{ env.BASECAMP_CHATBOT_URL != '' }}
if: failure() && env.BASECAMP_CHATBOT_URL != ''
env:
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
run: |
-2
View File
@@ -25,11 +25,9 @@ jobs:
environment: publish
timeout-minutes: 15
# The publish job's group: one writer per channel database at a time.
# queue: max, as there: a waiting job is queued, not replaced.
concurrency:
group: publish
cancel-in-progress: false
queue: max
steps:
- uses: actions/checkout@v4
with:
+1025 -428
View File
File diff suppressed because it is too large. Load diff
-54
View File
@@ -329,43 +329,6 @@ write_pkgrel() {
mv "$scratch" "$pkgbuild"
}
# A package built against a trigger's ABI pins it exactly, once per architecture
# because Arch Linux ARM can trail x86_64: depends_x86_64=('hyprland=0.56.2').
# Rebuilding against a newer trigger has to move that pin with it, or the
# rebuilt package demands the version it no longer loads into. Unpinned
# dependencies, and pins outside the per-architecture arrays, are left alone.
write_pins() {
local package_dir="$1"
local current="$2"
local pkgbuild="$package_dir/PKGBUILD"
local scratch="$pkgbuild.sync-rebuilds"
local arch trigger version pattern
cp "$pkgbuild" "$scratch" || return 1
while IFS=$'\t' read -r arch trigger version; do
pattern=$(printf '%s' "$trigger" | sed 's/[][\.*^$+?(){}|/]/\\&/g')
if ! awk -v array="depends_$arch" -v pattern="$pattern" -v pin="$trigger=$(version_pkgver "$version")" '
$0 ~ "^" array "=\\(" { inside = 1 }
inside { gsub("[\"\047]" pattern "=[^\"\047]*[\"\047]", "\047" pin "\047") }
inside && /\)/ { inside = 0 }
{ print }
' "$scratch" > "$scratch.new"; then
rm -f "$scratch" "$scratch.new"
return 1
fi
mv "$scratch.new" "$scratch"
done < <(jq -r 'to_entries[] | .key as $arch | .value | to_entries[] | [$arch, .key, .value] | @tsv' <<<"$current")
if ! bash -n "$scratch" 2>/dev/null; then
print_error "PKGBUILD with moved pins is not valid shell"
rm -f "$scratch"
return 1
fi
chmod --reference="$pkgbuild" "$scratch"
mv "$scratch" "$pkgbuild"
}
write_metadata() {
local package_dir="$1"
local filter="$2"
@@ -588,12 +551,6 @@ sync_package() {
return 0
fi
if ! write_pins "$package_dir" "$current"; then
print_error " Failed to move pinned trigger versions for $package"
((++FAILED))
return 0
fi
if [[ -n "$suffix" ]] && ! write_metadata "$package_dir" '.pkgrel.suffix = ($suffix | tonumber)' --arg suffix "$suffix"; then
print_error " Failed to record pkgrel suffix for $package"
((++FAILED))
@@ -835,17 +792,6 @@ cmd_self_test() {
check "run succeeds" 0 "$(run_case "$root")"
check "pkgrel untouched" 1 "$(pkgrel_of "$root/pkgbuilds/t-current")"
echo "A rebuild moves the trigger's exact pin for the architecture it ran for:"
root=$(selftest_root pins)
selftest_package "$root" t-pins 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"1.0-1"}}'
printf "depends_x86_64=('dep-a=1.0' 'dep-b')\ndepends_aarch64=('dep-a=1.0')\n" >> "$root/pkgbuilds/t-pins/PKGBUILD"
selftest_pacman "$root" dep-a=2.0-1
selftest_published "$root"
check "run succeeds" 0 "$(run_case "$root")"
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-pins")"
check "x86_64 pin moved" "depends_x86_64=('dep-a=2.0' 'dep-b')" "$(grep '^depends_x86_64=' "$root/pkgbuilds/t-pins/PKGBUILD")"
check "aarch64 pin left for its own run" "depends_aarch64=('dep-a=1.0')" "$(grep '^depends_aarch64=' "$root/pkgbuilds/t-pins/PKGBUILD")"
echo "Metadata that does not parse fails the run instead of vanishing from it:"
root=$(selftest_root unreadable)
selftest_package "$root" t-broken 1 '{"source":"local","rebuild_on":["dep-a"'
+1 -1
View File
@@ -23,7 +23,7 @@ Update packages that track an upstream vendor release feed instead of the AUR.
Packages opt in declaratively through "upstream" in .omarchy/package.json.
Providers cover GitHub Releases with checksum manifests or API asset digests,
semver-shaped git tags, npm dist-tags, and plain Debian Packages indexes. See
docs/upstream-sources.md for the schemas. Anything outside those conventions may provide
README.md for the schemas. Anything outside those conventions may provide
pkgbuilds/<package>/.omarchy/upstream.sh, a hook that reports JSON on stdout:
{
-4
View File
@@ -88,10 +88,6 @@ if [[ "$DRY_RUN" != true ]]; then
mkdir -p "$BUILD_OUTPUT_DIR" "$FINAL_OUTPUT_DIR"
# The image drops pacman's private signing key. Keyring upgrades need a
# container-local key to sign newly trusted keys.
sudo pacman-key --init || exit 1
# Bring the container up to date before any makedepends are installed. The
# image is layer-cached, so its glibc drifts behind the mirror while makepkg
# -s pulls makedepends from the freshly synced database -- a partial upgrade
+80 -102
View File
@@ -1,118 +1,96 @@
# The x86_64 builder pool
# CI spike: build PRs on ephemeral DigitalOcean droplets
x86_64 package builds run on DigitalOcean droplets that exist for one job
each. aarch64 builds run on GitHub's `ubuntu-24.04-arm` runners and need
nothing here.
Status: spike. Nothing here publishes. The repository host keeps building and
signing on merge exactly as before.
## How it works
## Pieces
- **Jobs ask for a runner labelled `omarchy-builder`.** `build-pr.yml` builds
x86_64 packages there, and so does the `rebuild` job in `publish.yml`.
- **A controller creates one droplet per queued job.** `controller.sh` runs
from a systemd timer every minute on a small always-on droplet (tag
`omarchy-controller`). It polls GitHub for queued jobs with the label and
creates droplets up to `MAX_DROPLETS`. It has no inbound endpoint.
- **A droplet runs one job and powers off.** `runner-cloud-init.yaml` installs
Docker and the GitHub runner, registers it `--ephemeral` with a
registration token that expires in an hour, runs the job, and powers off, also when the runner download or
registration fails. The controller removes any other failed droplet at
`MAX_AGE_MINUTES`.
- **The controller deletes droplets** that are powered off, older than
`MAX_AGE_MINUTES`, or still provisioning after `MAX_BOOT_MINUTES`, and
replaces a stuck one in the same tick.
- **Sizes and regions fall back.** Each tick tries every size in `SIZES`, in
every region DigitalOcean lists it in stock. `REGIONS` only sets which
regions go first. A refused create is logged with DigitalOcean's message.
- `.github/workflows/build-pr.yml` — on a PR touching `pkgbuilds/**`, one job
per changed package on runners labelled `omarchy-builder`. aarch64 jobs
run on GitHub's native `ubuntu-24.04-arm` runners instead. Uploads the unsigned
`.pkg.tar.zst` as a workflow artifact (7 days).
- `runner-cloud-init.yaml` — Ubuntu 24.04 user-data: docker + buildx, the
GitHub runner registered `--ephemeral`, runs one job, powers off.
- `controller.sh` — systemd timer every minute on a small always-on droplet.
Polls for queued jobs with our label, creates one g5-32vcpu-64gb-50gb droplet per job up
to `MAX_DROPLETS`, deletes droplets that are powered off or older than
`MAX_AGE_MINUTES`, or still provisioning after `MAX_BOOT_MINUTES`. Builders go in any region DigitalOcean lists the size in
stock in (`REGIONS` only sets which to try first); a refused create, logged
with DigitalOcean's message, falls back to the next region, then the next
of `SIZES`. No inbound endpoint. Plain curl against both APIs, no
doctl and no gh: a token in the environment cannot pick the wrong account
the way a saved doctl context can. Needs curl and jq.
`tests/controller.sh` exercises every decision against canned responses.
- `controller-box/` — the always-on droplet: unit, timer, env template,
cloud-init, and `create.sh` to stand it up with one API call.
No secret reaches a builder. Signing and upload happen in the `publish` job on
a GitHub-hosted runner.
## Standing up the controller box
## Operate it
DIGITALOCEAN_TOKEN=<omarchy account> GITHUB_TOKEN=<fine-grained PAT> \
REPO=omacom/omarchy-pkgs ci/controller-box/create.sh <branch>
SSH to the controller droplet as root. It is the droplet tagged
`omarchy-controller` in the DigitalOcean account that pays for the builders;
the operators' keys were installed when it was created.
The GitHub PAT is fine-grained, scoped to the one repo: Actions read,
Administration read+write (registration tokens). The DO token is baked into
the box's env file, so it is the account that pays for builder droplets.
Watch it with `journalctl -u omarchy-controller -f` on the box.
```bash
journalctl -u omarchy-controller -n 50 --no-pager # What it is doing
journalctl -u omarchy-controller -f # Follow it
systemctl list-timers omarchy-controller.timer # Is it ticking
```
Each tick pulls the box's checkout first, so a merged `controller.sh` is live
within a minute. The unit and timer are copies made at creation; after
changing them, on the box:
| Log line | Meaning |
|---|---|
| `creating <name> (<size> in <region>)` | A queued job is getting a droplet |
| `<size> in <region> refused: <message>` | DigitalOcean refused; the next region or size is tried |
| `no size in '<sizes>' can be created in any region` | Every create was refused. Read the `refused:` lines above it for why: out of stock (add a size to `SIZES`), or the account's droplet limit |
| `at cap (<live>/<max>, <busy> busy) with <queued> queued` | `MAX_DROPLETS` is reached. Raise it if the queue is long |
| `deleting droplet <id> (status=<status> age=<n>m)` | Reaping a finished, old or stuck droplet |
cp /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.{service,timer} /etc/systemd/system/
systemctl daemon-reload
Check the pool from anywhere:
## What the spike proved (2026-09-17, fork ryanrhughes/omarchy-pkgs)
```bash
gh api repos/omacom/omarchy-pkgs/actions/runners --jq \
'"online \([.runners[]|select(.status=="online")]|length), busy \([.runners[]|select(.busy)]|length)"'
```
- `bin/build` works from a bare clone: with no local published tree it
plans against and resolves from `https://pkgs.omarchy.org/<mirror>/<arch>`.
- Droplet create → runner registered: ~70 s. omarchy-fish PR job: 2 min
including the builder image build. Droplet powers off after the job.
- linux-omarchy on a c-32 droplet: 30 min wall clock for the build job
(23:39 → 00:09), 254 MB artifact. Cold start ~90 s before the job began.
- A PR whose PKGBUILD fails to build turns the required check red and GitHub
refuses the merge (`mergeStateStatus=BLOCKED`, `gh pr merge` refuses
without `--admin`).
- Controller: one queued job + one busy droplet ⇒ creates exactly one more;
reaps powered-off droplets on the next tick.
Jobs queued with no runner online for several minutes means the controller is
not creating droplets: read its journal. See what is waiting:
## Not done (required before this touches the real repo)
```bash
gh run list -R omacom/omarchy-pkgs --workflow build-pr.yml --status queued
```
- Tooling from base: check out master's `bin/ helpers/ build/` and overlay
only the PR's `pkgbuilds/<name>`; today a PR can edit the build script
and it runs on the droplet. The vouch gate limits who can do that, not
what they can do.
- DigitalOcean cloud firewall on the `omarchy-builder` tag: no inbound, no
egress to private ranges or the metadata address.
- A fine-grained GitHub token for the real repository (the one on the
controller box is scoped to the fork), and the publish environment's
secrets set there.
- Disable the host's auto-release timers for any channel CI publishes to,
so two writers never touch one database.
Only x86_64 jobs use this pool. A queued aarch64 job is waiting on GitHub's
own runners.
## Done since the spike README was first written
### Change settings
- Controller as a systemd timer on its own droplet, plain curl, self-test.
- Build once against edge; one artifact per package per architecture,
published into every channel it belongs to (fast ring: all three at
once). arch=any builds once for every architecture database.
- Publish is incremental and immutable: pull the channel db, refuse
different bytes under an existing name, accept identical bytes, upload
packages then signatures then the db.
- aarch64 under QEMU with credential-preserving binfmt. PR builds now run
aarch64 natively on `ubuntu-24.04-arm` (QEMU was up to ~15x slower). When a
merged tree has no artifact, publish.yml rebuilds it in its own job, aarch64
there and x86_64 on a droplet, outside the publish lock.
- Publish itself builds nothing: it signs and uploads on `ubuntu-latest` in the
tested builder image pulled from GHCR, and only that job holds the `publish`
concurrency group, so a merge waits for seconds of signing, not for builds.
- Vouch gate: collaborators, `.github/VOUCHED.td`, or the `build-approved`
label; denounced authors cannot be overridden by the label.
- Tests run on PRs only; `result`, `self-tests`, `build-isolation` are the
required checks with strict up-to-date branches.
Edit `/etc/omarchy-controller.env` on the box. The next tick reads it.
## Cleanup
| Setting | Default in `controller-box/controller.env.example` | |
|---|---|---|
| `MAX_DROPLETS` | 6 | Builders alive at once. The DigitalOcean account's droplet limit also applies |
| `SIZES` | `g5-32vcpu-64gb-50gb g5-32vcpu-128gb-50gb` | Tried in order |
| `REGIONS` | empty | Regions to try first; empty means any |
| `MAX_AGE_MINUTES` | 200 | Delete a droplet older than this |
| `MAX_BOOT_MINUTES` | 10 | Delete a droplet still provisioning after this |
| `DO_SSH_KEYS` | Ryan, DHH and Emir's account key IDs | Attached to every builder. Without one, DigitalOcean emails a root password per builder |
### Change the controller
Merge the change to `master`. The unit pulls `/opt/omarchy-pkgs` before every
tick, so it is live within a minute. The unit and timer files are copies made
when the box was created; after changing them, on the box:
```bash
cp /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.{service,timer} /etc/systemd/system/
systemctl daemon-reload
```
`tests/controller.sh` exercises every controller decision against canned API
responses. Run it before merging a change.
### Reach a builder
Operators can SSH to a builder as root while it lives. It powers off after its
one job.
### Clean up by hand
The controller reaps on its own. To do it manually, delete droplets tagged
`omarchy-builder` in the DigitalOcean account.
## Stand up a controller
```bash
DIGITALOCEAN_TOKEN=<account that pays for builders> GITHUB_TOKEN=<fine-grained PAT> \
REPO=omacom/omarchy-pkgs ci/controller-box/create.sh
```
- The GitHub PAT is fine-grained and scoped to this repository: Actions read,
Administration read and write (for runner registration tokens).
- The DigitalOcean token is written into the box's env file, so it is the
account that pays. It needs ssh_key:read to attach `DO_SSH_KEYS`; without
it every create is refused with 403.
- `ADMIN_GITHUB_USERS` names whose GitHub SSH keys get root on the box and
the builders. Set it; the default is a fixed list in `create.sh`.
- The script refuses to create a second controller.
doctl compute droplet list --tag-name omarchy-builder
doctl compute droplet delete -f <id>
-5
View File
@@ -17,8 +17,3 @@ LOCK=/run/omarchy-controller/lock
# Operator public keys for root on every builder droplet (JSON array).
# create.sh fills this from the operators' GitHub keys.
SSH_KEYS_JSON=[]
# DigitalOcean account SSH key IDs attached to every builder (JSON array), so
# DigitalOcean does not email a root password for each one. Needs ssh_key:read
# on DIGITALOCEAN_TOKEN. List them with: doctl compute ssh-key list
# Ryan R. Hughes, DHH, Emir.
DO_SSH_KEYS=[59143572,59143588,59779105]
+1 -1
View File
@@ -18,7 +18,7 @@ SSH_KEYS=${SSH_KEYS:-[]}
# Public keys authorized for root: the operators' GitHub keys, fetched at
# creation so the box never depends on an ssh_key API scope. Override with
# ADMIN_GITHUB_USERS.
ADMIN_GITHUB_USERS=${ADMIN_GITHUB_USERS:-ryanrhughes dhh emirb}
ADMIN_GITHUB_USERS=${ADMIN_GITHUB_USERS:-ryanrhughes dhh}
ssh_keys_json=$(for u in $ADMIN_GITHUB_USERS; do curl -fsS "https://github.com/$u.keys"; done | jq -R . | jq -sc .)
[[ $(jq length <<<"$ssh_keys_json") -gt 0 ]] || { echo "no ssh keys fetched for $ADMIN_GITHUB_USERS" >&2; exit 1; }
+2 -8
View File
@@ -40,12 +40,6 @@ CLOUD_INIT=${CLOUD_INIT:-$(dirname "$0")/runner-cloud-init.yaml}
# Operator public keys authorized on every builder (JSON array of strings).
# The box's env file carries them; empty means no root login.
SSH_KEYS_JSON=${SSH_KEYS_JSON:-[]}
# DigitalOcean account SSH key IDs or fingerprints attached at creation (JSON
# array). A droplet created with none gets a root password that DigitalOcean
# emails to the token's owner, once per builder. Attaching one needs the
# ssh_key:read scope on DIGITALOCEAN_TOKEN; without it every create is
# refused with 403.
DO_SSH_KEYS=${DO_SSH_KEYS:-[]}
LOCK=${LOCK:-/tmp/omarchy-controller.lock}
log() { echo "$(date '+%F %T') $*"; }
@@ -158,8 +152,8 @@ create_droplet() {
while read -r size region; do
[[ -n $size ]] || continue
body=$(jq -n --arg name "$name" --arg region "$region" --arg size "$size" --arg image "$IMAGE" \
--arg tag "$TAG" --arg ud "$userdata" --argjson keys "$DO_SSH_KEYS" \
'{name:$name, region:$region, size:$size, image:$image, tags:[$tag], user_data:$ud, ssh_keys:$keys, monitoring:false}')
--arg tag "$TAG" --arg ud "$userdata" \
'{name:$name, region:$region, size:$size, image:$image, tags:[$tag], user_data:$ud, monitoring:false}')
log "creating $name ($size in $region)"
if response=$(do_api droplets -X POST -d "$body"); then
jq -r '"created droplet \(.droplet.id)"' <<< "$response"
-3
View File
@@ -14,10 +14,7 @@
# Operators can reach a builder by key while it lives; it powers off after
# one job anyway. Keys are substituted by the controller (__SSH_KEYS_JSON__).
# Never by password: DigitalOcean sets and emails one when no account key is
# attached, and its vendor data turns password logins on to match.
disable_root: false
ssh_pwauth: false
chpasswd:
expire: false
ssh_authorized_keys: __SSH_KEYS_JSON__
-80
View File
@@ -1,80 +0,0 @@
# How a package builds
`bin/build` is the one builder: PR builds, rebuilds at merge and local builds
all call it.
## Dependency resolution
Within one `bin/build` run, the build system handles dependencies between the
packages it was asked to build. CI builds each package in its own job, so a
PR's packages do not see each other's fresh builds; they resolve from `edge`.
1. Plans dependency order once, including `depends`, `makedepends`,
`checkdepends`, and their architecture-specific arrays.
2. Builds each package in a fresh container. Installed packages and changes
to the container's system files cannot carry over to the next build.
3. Shares successful artifacts through the temporary `[omarchy-build]` repo,
installing newly built prerequisites in each consumer's container.
4. Blocks consumers of a failed prerequisite while continuing independent
builds.
Example: If `aether` depends on `hyprshade`, `hyprshade` is built first.
Isolation also lets the release and dev Omarchy pairs build in the same run:
Flea can install `omarchy` without preventing `omarchy-dev` from installing
its conflicting settings package in a different container.
Pacman downloads are cached under `cache/pacman/<channel>/<arch>/` across
containers and runs. The installed package database is never shared. Each
container updates its base system before resolving build dependencies, so a
cached builder image cannot cause a partial system upgrade. The existing
`OMARCHY_KEEP_BUILD_WORKSPACE`, `OMARCHY_SKIP_BUILDER_IMAGE`, and
`OMARCHY_DEFER_RUNTIME_DEPS` flags retain their behavior.
`tests/build-isolation.sh` exercises conflicting package pairs, failed
prerequisites, resumed builds, cache replacement, and deferred dependencies
using real containers and pacman transactions. It uses the prepared builder
image, or an image named by `TEST_BUILDER_IMAGE`; CI builds the small fixture
image in `tests/build-isolation.Dockerfile`.
## Daily builder images
`Refresh builder images` builds fresh `edge` environments daily at 04:23 UTC,
when their inputs change on `master`, and on manual dispatch. x86_64 and
aarch64 build on native GitHub-hosted runners, without occupying the DO
package-builder pool. Each candidate must pass `tests/build-isolation.sh`,
including real package builds, before publication to
`ghcr.io/omacom/omarchy-pkg-builder`. Only `master` in this repository can
publish; PR workflows cannot replace the shared images.
PRs that change image inputs also build and test both candidates on native
runners, with a read-only token and no registry publication.
The compatibility tag contains the architecture, mirror, and a hash of the
entire `build/` context, including executable bits and symlink targets but
excluding checkout timestamps and ownership. This deliberately invalidates
images when mounted build scripts change too. `v1` identifies the image build
contract; change it if the invocation or compatibility rules change. Each
successful refresh also gets a run-specific tag for diagnosis and rollback.
A failed build, isolation test, or push leaves the previous compatible image
selected. Scheduled builds use `--pull --no-cache` so unchanged Dockerfiles
still pick up fresh Arch packages.
To build and test a candidate locally:
```bash
bin/builder-image key --arch x86_64 --mirror edge
bin/builder-image build --arch x86_64 --mirror edge --tag builder-candidate:test --fresh
CONTAINER_ENGINE=docker TEST_BUILDER_IMAGE=builder-candidate:test tests/build-isolation.sh
```
The workflow uses its repository `GITHUB_TOKEN` with `packages: write`; no
registry PAT is needed. **One-time setup when the image package is first
created:** GHCR
creates the package private. In the `omacom/omarchy-pkg-builder` package
settings, change visibility to **Public**, then rerun the failed refresh job.
The workflow checks anonymous registry access before advancing the compatible
tag, so fork PRs will not be directed to an image they cannot pull. Subsequent
refreshes preserve that package visibility.
`publish.yml` and `unpublish.yml` pull the compatible image to sign and update
databases. Package build jobs still build their own image on the runner.
-44
View File
@@ -1,44 +0,0 @@
# Rebuild triggers
```bash
bin/sync-rebuilds # Bump every package whose dependencies moved
bin/sync-rebuilds quickshell-git # Update specific packages
bin/sync-rebuilds --self-test # Run the regression tests
```
Some packages have to be rebuilt when something they link against changes, even though nothing in their own source moved. A Qt private-API consumer is the usual case: `Qt_6_PRIVATE_API` symbols are not covered by the soname, so a qt6-base point release can leave an installed binary unable to resolve a symbol at startup, and pacman upgrades Qt out from under it because the dependency is unversioned. The package still builds from the same git commit, so its version alone would not trigger a rebuild.
A package names those dependencies in `.omarchy/package.json`:
```json
{ "source": "local", "rebuild_on": ["qt6-base", "qt6-declarative", "qt6-wayland"] }
```
`bin/sync-rebuilds` reads each named package's version from the official
repositories for every architecture a merge builds (`CI_ARCHES` in
`helpers/paths.sh`, both by default) that the package supports and compares
it to `rebuilt_against`. Records are kept per architecture because Arch and
Arch Linux ARM can carry different dependency versions. pkgrel is bumped once
when any recorded version moves; that one source revision is then built for
each architecture by the PR that carries the bump.
The bump is the point of the command, and it has to land in git rather than in the builder. A rebuild that reuses the published version string produces a package pacman will never offer anyone, so rebuilding without a version change would ship nothing. Bumping pkgrel needs no other change: the PR that carries the bump builds the package, and the merge publishes it.
The bumped version is checked against the published one as well as the checked-in one, and refused when pacman would not order it higher. The checked-in version is not the floor; what a user already has is, and a checkout that has fallen behind the repository can otherwise be bumped to something that loses to the package it means to replace. That check is skipped with a warning when the published database cannot be read.
x86_64 versions are read from the local pacman database, so the workflow runs
in an Arch container pointed at `mirror.omarchy.org`, the same mirror as the
x86_64 builder. aarch64 versions are read directly from the live Arch Linux ARM
repository database, which is also what the ARM builder uses. Testing and
staging repositories do not count. A legacy flat `rebuilt_against` record is
read as x86_64 and is migrated naturally the next time a rebuild is needed.
A dependency this repository carries for an architecture (a recipe here that builds for edge on it, such as aquamarine on aarch64) shadows the distribution's, because the builder lists `[omarchy]` first. Its version is the recipe's, and it counts only once edge publishes that version: until then the builder still links against the previous one, so dependents are left alone for that run.
## The workflow
`sync-rebuilds.yml` runs `bin/sync-rebuilds` every 6 hours and opens one PR per
package on `auto/sync-rebuilds-<package>` with auto-merge enabled. Each PR lands
once `result`, `self-tests` and `build-isolation` pass, and the merge publishes.
A rebuild that fails stays an open red PR for a maintainer without holding back
the others.
-119
View File
@@ -1,119 +0,0 @@
# Cutting an Omarchy release
The release train runs from `bin/omarchy-release`. Nothing in the GitHub
workflows promotes a channel: its channel moves and RC builds were written to
execute on the old repository host, which is abandoned (see
[repository-host.md](repository-host.md)), against a local copy of the
published tree.
> `rc` and `ship` build the release pair with `bin/repo release` and move
> channels with `bin/repo advance`, on the machine that holds the published
> tree. No workflow replaces them yet. That machine's tree has to match the
> bucket first: `advance` reads the source channel from local disk.
`bin/omarchy-release` is the front door. A release train has three human
moments, each one command — and bare `omarchy-release` observes reality
(branches, pins, published channels, tags) and walks you to the next one:
```bash
bin/omarchy-release # shepherd: status + guided next step
bin/omarchy-release start 4.0.2 # open the train: branch v4-0-2 + staging PR
bin/omarchy-release pick # choose merged PRs to cherry-pick (multi-select)
bin/omarchy-release rc # publish the next 4.0.2rcN to the rc channel
bin/omarchy-release ship # tag, final pins, promote rc → stable,
# draft GitHub release, ISO, website — one swoop
bin/omarchy-release doctor # verify credentials/connections up front
```
Versions are inferred from branch names (`v4-0-2` ⇒ `4.0.2rcN` ⇒ tag
`v4.0.2`); `start` is the only place a version is typed. Every command is
idempotent — re-runs skip whatever is already done. `ship` refuses to promote
a commit no RC was cut from. One exception: `ship` carries on past a failed
ISO or website step, and a later `ship` exits once stable holds the version
and the GitHub release exists. Finish a failed ISO or website step by hand.
## Promoting a channel
Ship the tested rc channel to stable (copies packages + signatures, then
cleans, rebuilds the database, and syncs):
```
bin/repo advance --from rc --to stable --arch all
```
Open a release train that ships new edge packages by carrying edge into rc
first:
```
bin/repo advance --from edge --to rc --arch all
```
`advance` moves x86_64 alone unless given `--arch`. `--arch all` covers the
architectures in `PUBLISHED_ARCHES` (`helpers/paths.sh`), x86_64 only by
default; set `OMARCHY_ARCHES="x86_64 aarch64"` on the machine that runs it to
promote both. The variable is not carried over when `bin/repo` forwards a
command over ssh, and `omarchy-release ship` promotes through the same path. It refuses to
carry a package into a channel its `channels` metadata excludes.
## The pin engine
The release pair is marked `"pinned": true`. It is built for `rc` only from
the `rc` branch worktree, with `OMARCHY_RC_PINS=1` set (`omarchy-release rc`
sets it), and never built for `stable`: stable gets it by promotion. A merge to
`master` therefore cannot overwrite an RC in flight.
The `omarchy` and `omarchy-settings` packages are released as a pair, always
built from the same upstream commit of basecamp/omarchy.
**Use `bin/omarchy-release`** — it drives the whole train across all four repositories and
calls the pin engine below for you.
`bin/omarchy-pkgs` is that engine, available directly for one-off pins and
debugging. It rewrites both PKGBUILDs in lockstep (same `_tag`/`_commit`/
`pkgver`/`sha256sums`), validates ordering with `vercmp`, commits, and pushes
the current branch. Driven by `omarchy-release` it pins to the release branch
on the `rc` branch and orders against the rc channel; invoked directly it
targets the current branch and edge.
```bash
bin/omarchy-pkgs release v4.0.0 # Final release from the upstream v4.0.0 tag
bin/omarchy-pkgs release rc v4.0.0 # Newest upstream v4.0.0-rcN tag -> 4.0.0rcN
bin/omarchy-pkgs release beta v4.0.0 # Same for beta (alpha also supported)
bin/omarchy-pkgs release latest # Newest upstream tag, rc/beta included (prompts)
bin/omarchy-pkgs release rc # Untagged RC from the quattro tip, auto-numbered
bin/omarchy-pkgs release --commit abc123 --base 4.1.0 # Untagged RC from a commit
bin/omarchy-pkgs release ... --dry-run # Show the plan; write nothing
bin/omarchy-pkgs release ... --no-push # Full flow, local commit only (testing)
bin/omarchy-pkgs self-test # Version normalization + ordering tests
```
## Versioning rules
- Finals are `X.Y.Z`; pre-releases are `X.Y.ZalphaN` / `X.Y.ZbetaN` /
`X.Y.ZrcN` in the **attached** form only. pacman's vercmp orders
`4.0.0alpha1 < 4.0.0beta1 < 4.0.0rc1 < 4.0.0`, but separator forms
(`4.0.0.rc1`, `4.0.0_rc1`) sort **after** `4.0.0` and would strand users on
the pre-release — the tooling normalizes upstream tags (`v4.0.0-rc1`,
`v4.0.0-rc.1`, ...) to the attached form and refuses anything it cannot
normalize. Upstream tags are cut on the quattro branch.
- `pkgrel` resets to 1 on every version change. Bump `pkgrel` by hand only to
repackage the same source.
- `epoch` is never set by tooling. It is sticky forever; adding one is a
human decision of last resort.
## Where releases land
- **RCs build for the rc channel.** RC testers and RC ISO installs run the real
`omarchy` package against the package set stable users will actually get.
Stable never sees an rc version; rc testers upgrade rc1 → rc2 → final
naturally.
- **Finals build into rc, then the whole channel promotes.** After testing, the
ship step promotes the exact tested artifacts (packages and signatures)
forward:
```bash
bin/repo advance --from rc --to stable --arch all
```
Neither package is on the `fast` ring, and `bin/omarchy-pkgs` never touches
stable — promotion is always this explicit step.
-586
View File
@@ -1,586 +0,0 @@
# The repository host (abandoned)
**This pipeline is abandoned.** Packages are built and published by GitHub
workflows: see the [README](../README.md). Don't follow this page to publish
anything.
The package pipeline used to run on one machine that built, signed and
published every channel on a timer. This page is kept as the reference for
the tooling that was written for it and is still in the tree: `bin/repo`, the
units in `systemd/`, and the host side of the release train
([releases.md](releases.md)).
**That tooling writes to the same bucket as CI.** If you run any of it
(`release`, `sync`, `promote`, `push`, `deploy`, `advance`, `remove`), these
rules apply:
- Do not run `bin/repo release`, `sync`, `push` or `deploy` for a package CI
publishes. Merge a PR, or dispatch `publish.yml`, instead.
- Keep the `omarchy-auto-release-*` and `omarchy-check-versions` timers
disabled while CI publishes the same channels. Two writers on one channel
database overwrite each other.
- Never pass `--prune` to `bin/repo sync`. The host's tree does not hold what
CI published, and `--prune` deletes remote files the local tree lacks.
- Never rebuild a version that is already published. Bump `pkgrel`.
## Setting up a host
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
### aarch64 under emulation
The repository host builds every architecture it publishes on the same
machine. A foreign architecture runs under QEMU user emulation, which
`bin/build` checks by actually running a container for the target platform.
Rootful Docker registers QEMU on first use. Rootless Podman uses the host's
registration and prints the one-time Arch setup commands when it is missing or
lacks the credential flag required by `sudo` inside the builder:
```bash
# Verify
podman run --rm --platform linux/arm64 docker.io/library/alpine:latest uname -m
# Should output: aarch64
```
**Note**: emulated builds are much slower than native ones.
## Published architectures
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
`helpers/paths.sh` names the architectures the host's scheduled pipeline and
release train work on. CI does not read it:
```bash
PUBLISHED_ARCHES="${OMARCHY_ARCHES:-x86_64}"
```
That list drives the whole scheduled pipeline. `check-versions` compares
PKGBUILDs against each architecture's channel databases and writes one queue
file per channel and architecture (`.sync-needed-<channel>-<arch>`);
`auto-release <channel>` works through the queues one architecture at a
time, each with its own backoff (`.build-failed-<channel>-<arch>`), so a
failing build on one architecture never holds up the other; and the release
train advances channels with `--arch all`: it takes one host-wide lock and
verifies every architecture's source database before moving any of them. The
first entry is the reference architecture the release train observes channels
through. Rerunning the same advance after a failed remote sync completes it
safely.
Adding an architecture to the scheduled pipeline is therefore one checked-in
change to that list: the next `check-versions` tick queues everything the new
architecture lacks, and the next `auto-release` tick starts building it. CI
does not read this list: it builds the architectures in `CI_ARCHES`, both by
default. For a one-off run, override it directly:
```bash
OMARCHY_ARCHES=x86_64 bin/check-versions
OMARCHY_ARCHES=aarch64 bin/check-versions
OMARCHY_ARCHES="x86_64 aarch64" bin/check-versions
```
The builder image bootstraps
`omarchy-keyring` from the x86_64 tree for every architecture, so the first
build of a new architecture does not depend on a repository that only it can
create.
## Release in one command
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
The release command is smart and **incremental** - it only builds packages that have changed or are missing. You generally don't need to specify a package manually unless you are debugging a specific failure.
When a package fails, a completed build run still signs and publishes the packages
that succeeded. Failed packages and their blocked dependents remain queued with
failure backoff; retries compare against the updated repository and skip the
published versions. Only artifacts recorded by fully completed package builds
are eligible for a partial release. An interrupted build, a failed publication
step, or an incomplete pair using deferred runtime dependencies still stops the
release. Reports distinguish partial publication from complete success.
```bash
# Build changed/new packages, sign, promote, clean, update, and sync
bin/repo release
# Stable Mirror
bin/repo release --mirror stable
# ARM64
bin/repo release --arch aarch64
# Build one package (still skipped when its version is already published)
bin/repo release --package omarchy-nvim
# Show what would build without signing/promoting/syncing
bin/repo release --dry-run
```
### Step-by-Step
```bash
bin/repo build # Build (unsigned)
bin/repo sign # Sign packages
bin/repo promote # Copy to production
bin/repo clean # Remove old versions
bin/repo update # Update database
bin/repo sync # Sync to remote
```
## Building Heavy Packages Locally
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
Large packages build faster on a local machine than on the server. Build them
here, then hand the artifacts to the repository host, which signs and publishes
them:
```bash
bin/repo deploy --package nvidia-580xx-utils # Build here, publish from the host
```
`deploy` is `build` followed by `push`. The two steps are also available
separately when a build needs inspecting before it ships:
```bash
bin/build --package nvidia-580xx-utils # Build on the fast machine
bin/repo push --package nvidia-580xx-utils # Upload + publish on the host
```
`push` uploads to the host's `build-output/`, verifies checksums, and runs
`bin/upload-prebuilt` there. Do not publish from a local checkout instead: a
local machine holds neither the complete repository nor a signing key.
**Name the package.** `build` plans against the local repository database, or
against the public channel's when there is none. `deploy` refuses to run
unscoped when the local database is missing. Unscoped builds belong on
the repository host, where `bin/repo release` does the same job against a real
database.
Every other `bin/repo` command — `sign`, `promote`, `update`, `clean`,
`advance`, `remove`, `sync`, `release` — works on the published tree. Run them
from anywhere: with a repository host configured `bin/repo` forwards them over
ssh to run there (see [Build trigger and the build host](#build-trigger-and-the-build-host)),
and `--local` forces execution on the current machine. `bin/repo build`
forwards too; call `bin/build` directly, or pass `--local`, to build on this
machine. `push` and `deploy` always run locally.
## Commands
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
### Global Flags
These flags can be used with all commands:
- `--mirror <edge|rc|stable>`: Selects the channel (default: `edge`).
- `--local`: Run on this machine instead of forwarding to the repository host.
- `--arch <x86_64|aarch64>`: Selects the target architecture (default: `x86_64`).
### Build
```bash
bin/repo build # All packages (x86_64, edge)
bin/repo build --arch aarch64 # ARM64
bin/repo build --mirror rc # Release-candidate channel
bin/repo build --mirror stable # Stable channel
bin/repo build --package yay cursor-bin # Specific packages
bin/repo build --dry-run # Show what would build
```
**Output**: Unsigned `.pkg.tar.zst` in `build-output/`. Only builds packages whose version differs from what the repository holds.
Use `--dry-run` to show the build plan without running `makepkg`.
### Sign
```bash
bin/repo sign
```
Reads the GPG key and passphrase from the environment and signs all packages in `build-output/`. Supports `--arch` and `--mirror`.
### Promote
```bash
bin/repo promote # Copy to production
bin/repo promote --arch aarch64 # ARM64
bin/repo promote --dry-run # Preview
```
Copies signed packages from `build-output/` → `pkgs.omarchy.org/`.
### Clean
```bash
bin/repo clean # Keep 2 versions
bin/repo clean --keep 3 # Keep 3 versions
bin/repo clean --dry-run # Preview
```
Removes old package versions from the file system. **Does not update the database.**
### Update
```bash
bin/repo update # Update database
```
Updates the repository database (adding the newest version of each package). Run this after `promote` or `clean`.
### Sync Repository
```bash
bin/repo sync # Sync current arch/mirror
bin/repo sync --mirror stable # Sync stable
bin/repo sync --arch aarch64 # Sync ARM64
bin/repo sync --skip-prod-check # No confirmation
```
Syncs package repositories to the remote server using rclone based on the configured mirror and architecture.
**Uploads are additive.** A local tree is not authoritative about what belongs on
the remote — `pkgs.omarchy.org/` is gitignored, and packages built on another
machine exist only there — so sync never deletes by default. To take a package
out of the channels, use the `unpublish.yml` workflow.
For the same reason sync refuses to publish a repository database built from a
tree holding fewer packages than the remote database already lists. The database
is what pacman resolves against, so a partial one hides every package it does not
know about even though the files are still on the mirror. Use `bin/repo push` to
publish packages built on another machine.
### Deploy
```bash
bin/repo deploy --package nvidia-580xx-utils # Build locally, publish from the host
bin/repo deploy --host root@example.com # Point at a specific repo host
bin/repo deploy --dry-run # Show the plan, change nothing
```
Runs `build` then `push` in one command. The repository host is resolved before
the build starts, so a missing `--host` fails immediately rather than after a long
compile.
### Push to the Repository Host
```bash
bin/repo push # Push everything in build-output
bin/repo push --package nvidia-580xx-utils # Push one package
bin/repo push --mirror stable --arch aarch64 # Pick mirror and architecture
bin/repo push --host root@example.com # Override the repo host
bin/repo push --dry-run # Show the plan, transfer nothing
```
Uploads packages from `build-output/` to the repository host and publishes them there
with `bin/upload-prebuilt` (sign → promote → update → sync). Use it when a package
is quicker to build on a local machine than on the server.
Publishing happens on the host rather than locally for two reasons: the host
has a GPG signing key (CI has its own copy in the `publish` environment), and
it holds the repository tree that its database and sync are built from. Local
machines therefore need no secrets.
The host comes from `--host`, `$OMARCHY_REPO_HOST`, then `.repo-host`. The
setting is named for the repository rather than for building, which happens
wherever you like. The same setting tells `bin/omarchy-pkgs release` which host
to poke after a release push.
`--package` means the same thing as it does to `build`: a pkgbase, whose every
output ships together. Pushing `nvidia-580xx-utils` carries `nvidia-580xx-dkms`
and `opencl-nvidia-580xx` with it, because that is what the build produced. An
output's own name still selects just that one, for publishing a single package
on purpose. Omit `--package` to push everything built.
Publishing signs and promotes everything staged on the host, not just what this
push uploaded, so `push` stops when it finds packages already staged there —
usually leftovers from a failed run. Remove them on the host, or pass
`--include-staged` to publish them too.
### Other host commands
```bash
bin/repo advance --from rc --to stable # Ship the tested rc channel to stable
bin/repo advance --from edge --to rc # Open a train: carry edge forward into rc
bin/repo advance --from edge --to rc --package x # Deliberate single-package advance
bin/repo advance --from rc --to stable --dry-run # Preview without changing files
bin/repo bootstrap-rc # One-time initial rc seed from stable
bin/repo timers # Timer schedule, last runs, queues, backoff, lock
bin/repo list # List package metadata
bin/repo deploy # Build locally, then publish from the host
bin/repo push # Upload local builds to the host and publish
bin/repo remove <package> # Remove package (host workflow; CI uses unpublish.yml)
bin/clean-docker # Clear Docker images/cache (forces fresh rebuild)
```
## Build trigger and the build host
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
Release commands run from anywhere. `bin/repo` is a **remote control**: with a
repository host configured, every command that operates on the published tree
(`release`, `build`, `sign`, `promote`, `update`, `clean`, `advance`,
`bootstrap-rc`, `remove`, `sync`, `migrate`) executes ON the host over ssh —
the exact same code, run where the tree lives, so ssh'ing in and running the
same commands by hand behaves identically. Pass `--local` to force execution
on the current machine. `list`, `push`, `deploy`, and `setup` never forward
(`push`/`deploy` exist precisely to move local builds *to* the host).
Without a configured host, commands run locally — which on the build host
itself (no `.repo-host` there) is exactly right, and elsewhere the exact
commands to run are printed by the orchestrator, with the 5-minute
auto-release timer as the backstop.
The host setting is any destination `ssh` accepts, resolved in this order:
1. `--host <dest>`, for `push`, `deploy` and `bin/omarchy-release`
2. `OMARCHY_REPO_HOST` environment variable
3. the git-ignored `.repo-host` file (one line; `#` comments allowed)
The server layout lives under `/root`, so the value is `root@<ip>`,
`root@<hostname>`, or — nicest — a `Host` alias from `~/.ssh/config` that
carries the user, key, and port:
```
# .repo-host
root@pkgs.example.com
```
All connections are plain ssh; nothing else is used to reach the host.
**How "this machine is the build host" is detected — and its one caveat.**
The marker is the published database living in this checkout
(`pkgs.omarchy.org/<channel>/<arch>/omarchy.db*`), the same convention the
rest of `bin/` uses. A workstation that once ran a full local
`bin/repo release` carries that marker too and would infer it is the host.
An explicitly configured destination always outranks the inference — `--host`,
`OMARCHY_REPO_HOST`, and `.repo-host` are checked first, and only when none is
set does local detection apply. So if a machine ever misidentifies itself,
writing the real host into `.repo-host` is the fix.
## Directory Structure
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
```
omarchy-pkgs/
├── pkgbuilds/ # Source PKGBUILDs
│ └── package-name/
│ ├── PKGBUILD
│ └── .omarchy/
│ ├── package.json # Source/sync/release metadata
│ └── upstream.sh # Optional custom vendor release feed hook
├── build/
├── build-output/ # Unsigned packages (temporary)
│ ├── edge/ # (rc/ and stable/ alongside, each x86_64 + aarch64)
│ ├── rc/
│ └── stable/
├── pkgs.omarchy.org/ # Signed packages (production)
│ ├── .release.lock # Host-wide lock: one channel mutation at a time
│ ├── edge/ # Each channel: x86_64/ and aarch64/
│ ├── rc/
│ └── stable/
└── bin/ # CLI tools (on host)
```
## Architecture-Specific Notes
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
### x86_64
- Native builds (fast)
- Mirrors: mirror.omarchy.org, rackspace, pkgbuild.com
### aarch64
- Built on the repository host like x86_64; under QEMU when the host is x86_64
- On an ARM host, package builds and the signing/database utility containers
run natively; only an explicitly requested x86_64 package build is emulated
- Uses Arch Linux ARM repositories through the same HTTPS mirror for every
channel (Arch Linux ARM publishes no dated snapshots to pin a channel's base)
- Additional repos: `[alarm]`, `[aur]`
- 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
### Building for Both Architectures
```bash
# Build x86_64
bin/repo release --package myapp
# Build aarch64
bin/repo release --arch aarch64 --package myapp
# Sync both
bin/repo sync
bin/repo sync --arch aarch64
```
## Version Management
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
Packages are only rebuilt if:
- PKGBUILD version differs from the repository version
- Package doesn't exist in production
A package that has to be rebuilt because something underneath it changed is handled by turning that into a version change: `bin/sync-rebuilds` bumps pkgrel when a dependency named in `rebuild_on` moves.
## Scheduled releases on the host
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
Four systemd units in `systemd/` drive unattended releases from the host.
All four units run **every 5 minutes**, staggered by a minute each, so a push
reaches the mirror in minutes rather than hours:
1. **check-versions** (`*:0/5`): Pulls latest from git, compares PKGBUILD versions to published versions for every published architecture, creates one state file per channel and architecture if builds are needed
2. **auto-release-edge** (`*:1/5`): For each published architecture with a state file, builds all edge packages that need updates
3. **auto-release-rc** (`*:2/5`): Builds fast-ring packages for rc, from the main checkout like the other two — natively in the rc image, not copied from another channel. The pinned release pair is built separately by `omarchy-release rc` in the `rc` branch worktree
4. **auto-release-stable** (`*:3/5`): If a state file exists, builds `release_ring=fast` packages for stable
That cadence is only safe because of three guards:
- **No overlap.** Every channel-mutating run takes a host-wide lock
(`pkgs.omarchy.org/.release.lock`). Scheduled runs take it
**non-blocking**: if a build is already going, the tick exits immediately
instead of queuing. Waiting would stack one stalled process per tick behind
a long build and stampede when it finished. Manual commands still wait, as
an operator expects. `check-versions` takes it too — its `git pull` would
otherwise swap PKGBUILDs out from under a running build.
- **Backoff on failure.** A failed release records the attempt in
`.build-failed-<channel>-<arch>` and backs off exponentially — 10m, 20m, 40m, up to
a 6h ceiling — instead of rebuilding the same broken tree every 5 minutes.
**Any new commit clears the backoff immediately**, since a push is the most
likely fix. Clear it by hand with
`rm /root/.state/.build-failed-<channel>-<arch>`.
- **Quiet when idle.** With nothing queued a tick exits without output, so the
journal shows the runs that mattered rather than 288 no-ops a day.
`bin/repo timers` reports all of this: schedules, last results, what is
queued, what is failing and when it will retry, and whether the lock is held.
## Build reports
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
Every release run reports to Basecamp Campfire — not just failures, so a push
can be followed all the way to the mirror without watching the host:
| Event | Message |
|---|---|
| 🔨 Started | channel, arch, host, the commit being built, and which packages are queued |
| ✅ Published | the packages and versions that went out, duration, and the channel URL |
| 📦 Promoted | what `advance` moved between channels (including the rc bootstrap) |
| 📦 Nothing to publish | a queued run built nothing — see below |
| 🔴 Failed | which step failed, the commit, and the last 25 log lines |
Reports are tied to release *runs*, not timer ticks: a tick with nothing
queued exits silently, so a quiet day is a quiet chat. That is what makes the
"nothing to publish" report meaningful rather than routine — a release only
runs when the version check queued work, so building nothing means the check
and the builder disagree about what is out of date. It names the packages that
were queued but not built, which is where to start looking.
By default everything posts to `BASECAMP_CHATBOT_URL`, the same chat the
sync workflows use — one variable, nothing extra to configure.
If release traffic starts drowning that chat, give it its own: create a second
Basecamp chat, add a chatbot integration to it, and export its lines URL
alongside the existing one in `/root/.omarchy/build-credentials`:
```bash
export OMARCHY_RELEASE_CHATBOT_URL="https://3.basecamp.com/<account>/integrations/<key>/buckets/<project>/chats/<chat>/lines"
```
Release reports then go there while the sync workflows keep posting to
`BASECAMP_CHATBOT_URL`. With neither set, reports are silently skipped.
`bin/setup` reports which of the three cases applies.
Check on all of it with `bin/repo timers` — schedule, each unit's last run and
whether it succeeded, what is queued, whether a release is running right now,
and any failed units. Like the other host commands it forwards over ssh, so
the build box's state is one command away from any machine:
```bash
bin/repo timers # runs on the repository host when one is configured
bin/repo timers --local # inspect this machine instead
```
State files are stored in `/root/.state/`:
- `.sync-needed-<channel>-<arch>` — the packages queued for that channel and
architecture, one per line; the release run reads them to name what it is
building
- `.build-failed-<channel>-<arch>` — consecutive failure count, timestamp, and
the commit it failed on (drives the backoff; removing it forces a retry)
Legacy files without the architecture suffix are consumed once as x86_64
state, so upgrading the host does not lose an in-flight build.
## Schedule (America/New_York)
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
| Minute of every hour | Action |
|------|--------|
| :00, :05, :10, … | check-versions (git pull + creates state files) |
| :01, :06, :11, … | auto-release-edge |
| :02, :07, :12, … | auto-release-rc |
| :03, :08, :13, … | auto-release-stable |
Each release unit is a no-op when its channel has nothing queued, another run
holds the lock, or the channel is in failure backoff.
## Installation
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
```bash
ssh root@<host> 'cd /root/omarchy-pkgs && bin/setup --skip-timers'
```
`bin/setup` installs the dependencies, ensures Docker is running and creates
the state directory. Without `--skip-timers` it also installs and enables the
release timers, which must stay off while CI publishes. It works on
Debian/Ubuntu and on Arch, and is idempotent, so run it again whenever a
dependency is added.
The host does not need to be Arch: makepkg, repo-add and package signing all
run inside containers, so it needs only Docker, rclone, bsdtar, jq, git and
rsync. Docker is left alone when it already works, rather than replacing a
working installation from Docker's own repository with the distribution's.
```bash
bin/repo setup --check # Report what is missing, change nothing
bin/repo setup --skip-timers # Prepare the host without the release timers
```
Signing credentials (`/root/.omarchy/build-credentials`) and the rclone remote
hold secrets, so setup reports on them rather than creating them.
## Management
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
```bash
# Everything at a glance: schedules, last runs, queues, backoff, lock
bin/repo timers # forwards to the host when one is configured
# Manual trigger (edge, rc, stable)
systemctl start omarchy-check-versions.service
systemctl start omarchy-auto-release-edge.service
# View logs
journalctl -u omarchy-auto-release-edge.service -n 50
# Clear a channel stuck in failure backoff (a new commit also clears it)
rm /root/.state/.build-failed-edge-x86_64
# Release lock left behind by a killed run (bin/repo timers shows if it is live)
rm /root/omarchy-pkgs/pkgs.omarchy.org/.release.lock
```
With the timers enabled, `check-versions` pulls the main checkout on its
5-minute tick. With them disabled, nothing does: `bin/repo` pulls before a
command it forwards, and a command run on the host itself needs a `git pull`
first. The checkout must be on `master`.
+5 -193
View File
@@ -73,21 +73,13 @@ because its published history counted every commit and the number must never
go down.
`min_release_age` holds a branch tip until its commit timestamp is old enough.
A fresh tip falls back to the newest commit on the branch's first-parent
history that has cleared the window, so a busy branch still advances; it never
selects a commit from a merged side branch. This uses Git's committer date, not
the time a commit was pushed, measured from when the run cloned the branch so
packages pinned from it agree. `BYPASS_MIN_RELEASE_AGE=1` bypasses the hold.
A fresh tip leaves the existing pin alone; the watch never walks backward to
an older commit. This uses Git's committer date, not the time a commit was
pushed. `BYPASS_MIN_RELEASE_AGE=1` bypasses the hold.
Packages marked `"auto_merge": true` ride the unattended lane
(`track-branches.yml`), checked hourly instead of every 6 hours. Every lane
opens its PRs through `package-prs.yml` and auto-merges them as soon as the
build checks pass. Each package gets its own
PR on `auto/track-branches-<package>`, and packages that pin the same branch
share one, so a package whose build fails holds back only itself. The lane works with every
provider, not only branch watches, so it also carries release feeds trusted to
ship without review: the browsers, large vendors' apps, Omacom's own projects,
and vendor binaries that hold fresh releases with `min_release_age`. `bin/sync-upstream --lane
(`track-branches.yml`) instead of the reviewed sync PR: their bump PR is opened
and auto-merged as soon as the build checks pass. `bin/sync-upstream --lane
reviewed|auto-merge|all` selects a lane; the scheduled workflows each pass their
own. Packages that pin the same branch move in lockstep: if one of them fails
to update, the run restores the others and reports the group as failed. A
@@ -228,183 +220,3 @@ These packages were already excluded from automatic AUR updates. The migration p
- RustDesk reads hbb_common from the release gitlink; its existing build-time dependency/toolchain checks remain in force.
- Spotify uses HTTPS and retains its signed Release/Packages verification.
- Source and build compatibility still need review when upstream code changes. Direct watches remove AUR recipe churn, not the need to maintain packaging.
## How the sync PRs build
`sync-upstream.yml`, `sync-rebuilds.yml` and `track-branches.yml` each run their
sync once, split the changed recipes into one PR per package with
`.github/scripts/package-pr-groups.sh`, and hand them to `package-prs.yml`. That
pushes each PR with the PAT in `PKGS_BOT_TOKEN`, so its builds start without an
approval hold, and arms auto-merge. A push to an `auto/sync-*` or
`auto/track-branches-*` branch does not cancel the PR's build in flight: the new
build waits, then reuses every artifact the finished one uploaded.
A red PR stays open for a maintainer and holds back only its own package. A
later run updates it if upstream moves again, and closes it once it conflicts
with `master`. A run started by hand with `packages` updates the same
per-package PRs a scheduled run would.
## Declarative providers and hooks
The older provider form, still in use beside `upstream.watch`. Give a new
package an `upstream.watch`; `helpers/upstream-watch.py check` validates that
form only.
```bash
bin/sync-upstream # Update every package with an upstream hook
bin/sync-upstream openai-codex-desktop # Update specific packages
```
A package declares where its releases come from either as data or, for an
unusual feed, a small hook.
A vendor shipping tagged GitHub releases is pure data, declared as `upstream`
in `.omarchy/package.json` with no code at all:
```json
"upstream": {
"github": "jdx/mise",
"checksums": "SHASUMS256.txt",
"assets": {
"x86_64": "mise-{tag}-linux-x64.tar.xz",
"aarch64": "mise-{tag}-linux-arm64.tar.xz"
}
}
```
`checksums` names the manifest asset the vendor publishes. A vendor publishing
none sets `"digests": true` instead, and the checksums come from the SHA-256
digest GitHub's release API reports for every asset — see
`pkgbuilds/schist-bin/.omarchy/package.json`. Either way the artifacts
themselves are never downloaded.
An architecture may map to an ordered array when its PKGBUILD downloads more
than one release asset. Small versioned files outside the release assets can be
listed under `sources` and are downloaded and hashed when a new version appears:
```json
"upstream": {
"github": "owner/project",
"digests": true,
"assets": {
"x86_64": ["tool-{pkgver}-x86_64", "tool-{pkgver}-x86_64.asc"],
"aarch64": ["tool-{pkgver}-aarch64", "tool-{pkgver}-aarch64.asc"]
},
"sources": {
"any": ["https://raw.githubusercontent.com/owner/project/{tag}/LICENSE"]
}
}
```
Asset and source keys must be disjoint because each key maps to one PKGBUILD
checksum array (`any` means the unsuffixed `sha256sums`).
Repositories whose historical releases use incompatible tag schemes may set
`"latest_only": true`. The provider then considers only the newest stable
GitHub release, while retaining all validation for that release. A quarantine
will wait for that release to age instead of falling back to an older one.
`{tag}` and `{pkgver}` interpolate into asset names; a leading `v` on the tag is
stripped for `pkgver`; drafts and prereleases are ignored. Only the 100 most
recent releases are considered. The provider fails closed on anything it cannot
read — an unusable tag, timestamp, or checksum stops the sync rather than being
skipped.
Projects that publish version tags but no checksum manifest can declare the
tag repository, the exact tag shape, and every source that should be hashed:
```json
"upstream": {
"git_tags": "https://github.com/owner/project.git",
"tag_pattern": "v{pkgver}",
"sources": {
"any": ["https://github.com/owner/project/archive/refs/tags/{tag}.tar.gz"]
}
}
```
The newest matching tag is selected with pacman's `vercmp`; unrelated tags are
ignored. `tag_pattern` must contain exactly one `{pkgver}`. Source templates may
use `{tag}` and `{pkgver}`. Each expanded URL must be HTTPS and is downloaded
only when the discovered version is newer. A checked-in patch or other local
source can be included as `file:patch-name.patch`; it is hashed from the package
directory. Keys such as `any`, `x86_64`, and `aarch64` select the corresponding
`sha256sums` array.
npm packages use the same source mapping, with `{npm_tarball}` available for
the tarball named by the selected dist-tag:
```json
"upstream": {
"npm": "@scope/package",
"dist_tag": "latest",
"sources": {
"any": ["{npm_tarball}", "https://example.com/v{pkgver}/CHANGELOG.md"]
}
}
```
`dist_tag` defaults to `latest`. The registry's publication timestamp is
carried into the provider result, so `min_release_age` works for npm packages.
A vendor with a plain-text Debian `Packages` index can use it to discover the
newest exact package version, then hash immutable source URLs:
```json
"upstream": {
"debian": "https://example.com/debian/dists/stable/main/binary-amd64/Packages",
"package": "example-app",
"sources": {
"x86_64": ["https://example.com/tool-{pkgver}-x64.tar.gz"],
"aarch64": ["https://example.com/tool-{pkgver}-arm64.tar.gz"]
}
}
```
This deliberately accepts only Debian versions that are already valid Arch
`pkgver` values. Feeds needing epoch, revision, or filename translation retain
a hook. Exactly one of `github`, `git_tags`, `npm`, or `debian` may appear in a
declaration.
A timestamped provider may also declare `"min_release_age": "24h"`
(`s`/`m`/`h`/`d` suffix or bare seconds) to quarantine fresh releases until
maintainers have had time to pull a bad or compromised one. GitHub Releases and
npm provide publication times; raw git tags and Debian Packages indexes do not,
so combining either with this policy fails closed. The newest release that has
cleared the window ships, so a fast release cadence cannot starve updates. The
window is enforced
centrally: whatever reports the release must prove its age via `published_at`,
or the sync fails. A maintainer deliberately shipping inside the window runs
`BYPASS_MIN_RELEASE_AGE=1 bin/sync-upstream <package>` locally and merges the
result through a normal PR; scheduled automation never sets the bypass.
A vendor whose feed fits no convention (a Debian package index, a bare
version.txt) instead provides `.omarchy/upstream.sh`, a hook that reports the
newest upstream release as JSON on stdout — declaring both an `upstream` block
and a hook is an error:
```json
{
"pkgver": "1.2.3",
"sha256sums": { "x86_64": ["<sha256>"], "aarch64": ["<sha256>"] }
}
```
Architecture keys become `sha256sums_<arch>` in the PKGBUILD; the key `any` means
the unsuffixed `sha256sums` array, and only the arrays a hook names are touched.
An empty object (`{}`) reports no update, which is how a hook waits out a release
that has landed for one architecture but not yet the other.
When the reported version is newer than the checked-in one, `bin/sync-upstream`
rewrites `pkgver` and those checksum arrays and resets `pkgrel` to 1. A version
that is equal or older leaves the package alone, so a vendor rolling a release
back cannot walk the repository backwards.
Hooks should read checksums from whatever manifest the vendor publishes rather
than downloading the artifacts — see `pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh`,
which reads OpenAI's Debian package index and never fetches the 750 MB of debs
it describes. Hooks honoring `min_release_age` receive the window as
`MIN_RELEASE_AGE_SECONDS` and report `published_at` alongside `pkgver`.
`bin/sync-upstream self-test` runs offline fixture tests over the release
selection, quarantine backstop, duration parsing, and manifest validation.
+1 -2
View File
@@ -333,8 +333,7 @@ packages_for_upstream_sync() {
# "auto_merge": true rides the unattended lane instead: its bump PR is opened
# and auto-merged by the branch tracker as soon as CI is green, which is how a
# package that follows a moving branch (omarchy-dev, omasnap-git) gets rebuilt
# without anyone clicking, and how trusted release feeds (browsers, large
# vendors, Omacom's own projects) ship without waiting for review. The lanes are disjoint so a branch tip can never
# without anyone clicking. The lanes are disjoint so a branch tip can never
# hold up a reviewed vendor release, or the other way round.
package_auto_merge() {
local pkgdir="$1" metadata
+8 -21
View File
@@ -186,9 +186,9 @@ def matches(watch, text, extra=None, full=False):
yield candidate(watch, {**(extra or {}), **match.groupdict()})
def git_branch_tip(url, branch, tag_pattern, cache, min_age=0):
"""Describe the newest commit on an upstream branch that has cleared
min_age: commit, total count, date, and with a tag_pattern
def git_branch_tip(url, branch, tag_pattern, cache):
"""Describe the current tip of an upstream branch:
commit, total count, date, and with a tag_pattern
the newest release tag reachable from it plus the distance from that tag,
so a branch build can be versioned <tag>.r<n>.g<sha>, above the release it
follows and below the next one, the way a pkgver() function would.
@@ -196,33 +196,20 @@ def git_branch_tip(url, branch, tag_pattern, cache, min_age=0):
One blobless single-branch clone per (url, branch) per run, shared by
every package that tracks it, so two recipes pinned from one clone always
see the same commit. The clone is read with git only; nothing in it runs.
A tip younger than min_age falls back along first-parent history, the
commits that were themselves the branch tip, never into a merged side
branch. Holding the whole branch instead stalled a busy branch for as
long as it kept getting commits. Age is measured from when the clone was
made, so siblings synced seconds apart cannot straddle the window.
select_release applies the age hold to this tip, without walking back
into history (which could select a commit from a merged side branch).
"""
https(url)
key = hashlib.sha256(f"{url}#{branch}".encode()).hexdigest()
work = Path(cache) / f"{key}.branch.git"
cloned_at = work.with_name(f"{key}.branch.time")
if not work.exists():
scratch = work.with_name(f"{work.name}.{os.getpid()}.tmp")
started = dt.datetime.now(dt.timezone.utc).isoformat()
subprocess.run(["git", "clone", "--quiet", "--bare", "--filter=blob:none", "--single-branch", "--branch", branch, url, str(scratch)], check=True)
cloned_at.write_text(started)
scratch.replace(work)
git = ["git", "-C", str(work)]
commit = run([*git, "rev-parse", "HEAD"], text=True).strip()
if not re.fullmatch(r"[0-9a-f]{40}", commit):
raise ValueError("branch tip is not a commit")
if min_age:
now = dt.datetime.fromisoformat(cloned_at.read_text().strip())
for line in run([*git, "log", "--first-parent", "--format=%H %cI", commit], text=True).splitlines():
sha, committed = line.split()
if (now - dt.datetime.fromisoformat(committed)).total_seconds() >= min_age:
commit = sha
break
count = run([*git, "rev-list", "--count", commit], text=True).strip()
date = run([*git, "show", "-s", "--format=%cs", commit], text=True).strip().replace("-", "")
timestamp = run([*git, "show", "-s", "--format=%cI", commit], text=True).strip()
@@ -246,7 +233,7 @@ def git_branch_tip(url, branch, tag_pattern, cache, min_age=0):
return values
def discover(watch, fetch, min_age=0):
def discover(watch, fetch):
provider = validate(watch)
feed = watch[provider]
results = []
@@ -273,7 +260,7 @@ def discover(watch, fetch, min_age=0):
for tag, commit in tags.items():
results.extend(matches(watch, tag, {"tag": tag, "commit": commit}, full=True))
elif provider == "git_branch":
tip = git_branch_tip(feed, watch["branch"], watch.get("tag_pattern"), fetch.cache, min_age)
tip = git_branch_tip(feed, watch["branch"], watch.get("tag_pattern"), fetch.cache)
results.append(candidate(watch, tip))
elif provider == "npm":
data = fetch.json("https://registry.npmjs.org/" + quote(feed, safe=""))
@@ -557,7 +544,7 @@ def sync(package, fetch, min_age=0, check=False):
original = path.read_text()
before = read_recipe(path)
bypass = os.environ.get("BYPASS_MIN_RELEASE_AGE") == "1"
release = select_release(discover(watch, fetch, 0 if bypass else min_age), min_age, bypass=bypass)
release = select_release(discover(watch, fetch), min_age, bypass=bypass)
if release is None:
return {"status": "skipped", "reason": "minimum release age"}
current = scalar(before, "pkgver")
+5 -5
View File
@@ -1,6 +1,6 @@
pkgname=1password-beta
_tarver=8.12.42-32.BETA
_tarver=8.12.40-27.BETA
case "${CARCH}" in
x86_64)
_archdir="x64"
@@ -9,8 +9,8 @@ case "${CARCH}" in
_archdir="arm64"
;;
esac
pkgver=8.12.42_32.BETA
pkgrel=1
pkgver=8.12.40_27.BETA
pkgrel=2
conflicts=('1password' '1password-beta-bin')
pkgdesc="Password manager and secure wallet"
arch=('x86_64' 'aarch64')
@@ -22,8 +22,8 @@ source=()
sha256sums=()
source_x86_64=(https://downloads.1password.com/linux/tar/beta/x86_64/1password-${_tarver}.x64.tar.gz{,.sig})
source_aarch64=(https://downloads.1password.com/linux/tar/beta/aarch64/1password-${_tarver}.arm64.tar.gz{,.sig})
sha256sums_x86_64=('55fe8dbc80e65337e657c926948dff356b09c3d0860f3b381f1df5af2b99741a' '62494cae0f90d553cade1eeda82cec4d5b26e4719f2233085ce47a70cc80da63')
sha256sums_aarch64=('53a63a857e9219da410839915216c4f9cc017cb17bbff9095caf01365e28c5d8' 'd910aedf72c4ff31745b0bbfc12d03c03b177df85f73a381a35227439320ad5c')
sha256sums_x86_64=('1327d102255b5c347e6c5e19b1a6581986446e06848015b93b5d7b10b9bc3f52' '2a19fba2b81ade500d9707a20829930d4fca972254fdc653ffa27cee57638098')
sha256sums_aarch64=('54b14cae2a676480f3f2df7b85e42bf389d1f84207b0a4ad1e32382071625146' '3a55dc9cdee5729e4bdf6830e04d9813c11546b328003800a837df83f79e33f9')
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
package() {
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"debian": "https://downloads.1password.com/linux/debian/amd64/dists/stable/main/binary-amd64/Packages",
-1
View File
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
+3 -3
View File
@@ -1,6 +1,6 @@
# Maintainer: Basecamp <support@basecamp.com>
pkgname=basecamp-cli
pkgver=0.13.0
pkgver=0.12.0
pkgrel=1
pkgdesc="CLI for Basecamp project management"
arch=('x86_64' 'aarch64')
@@ -15,8 +15,8 @@ optdepends=(
)
source_x86_64=("https://github.com/basecamp/basecamp-cli/releases/download/v${pkgver}/basecamp_${pkgver}_linux_amd64.tar.gz")
source_aarch64=("https://github.com/basecamp/basecamp-cli/releases/download/v${pkgver}/basecamp_${pkgver}_linux_arm64.tar.gz")
sha256sums_x86_64=('8b33a93018c165f82ac7050d83cad8cde509f9cb7cfbccbc48fb9d98c0649931')
sha256sums_aarch64=('bef69d622d66685899b4f955b255951bb588cf1aac54f990b6bd818ae551104c')
sha256sums_x86_64=('25d61c38de5660e97855661a2bf7329264912fcd46e2b3fb893fbc70bb467b5c')
sha256sums_aarch64=('1f692d2a8cc733ef95232eeb107414e32f1dd3228a0dde6c9039538661e0f2ca')
package() {
install -Dm755 "basecamp" "${pkgdir}/usr/bin/basecamp"
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
+4 -4
View File
@@ -10,8 +10,8 @@
# Contributor: Giusy Digital <kurmikon at libero dot it>
pkgname=brave-bin
pkgver=1.97.56
pkgrel=1
pkgver=1.96.61
pkgrel=2
epoch=1
pkgdesc='Web browser that blocks ads and trackers by default (binary release)'
arch=(x86_64 aarch64)
@@ -35,8 +35,8 @@ source_aarch64=(${pkgname}-${pkgver}-aarch64.zip::https://github.com/brave/brave
noextract=(${pkgname}-${pkgver}-x86_64.zip ${pkgname}-${pkgver}-aarch64.zip)
sha256sums=('75a87dd17b42fcc6f27adfd16c82bed1c08e9251b07d2012f8d49f7412fa1d00'
'c07276b69c7304981525ecb022f92daf7ae125a4fb05ac3442157b50826e257a')
sha256sums_x86_64=('8acac76eae35942e30cc63e05adc772f632ad87be8a5ffb3d8a6e280380584ec')
sha256sums_aarch64=('f18823bfe8bf626a7412a2cc142ba851f991665ca31af8d750f80dbb8371f984')
sha256sums_x86_64=('c68cf179603470e8001a949294fe98b593f0749ca95f42687d855081b1c394a4')
sha256sums_aarch64=('33a1513379505642e4df0dda36a1dd78f735ee351631cffd4a4ac90b1cec0cd5')
prepare() {
mkdir -p brave
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
+4 -4
View File
@@ -1,8 +1,8 @@
# Maintainer: brave <aur-release@brave.com>
pkgname=brave-origin-bin
pkgver=1.97.56
pkgrel=1
pkgver=1.96.61
pkgrel=2
epoch=1
pkgdesc='The minimalist browser from the makers of Brave (binary release).'
arch=(x86_64 aarch64)
@@ -22,8 +22,8 @@ source_aarch64=("${pkgname}-${pkgver}-aarch64.zip::https://github.com/brave/brav
noextract=("${pkgname}-${pkgver}-x86_64.zip" "${pkgname}-${pkgver}-aarch64.zip")
sha256sums=('5ff70ee473f35c2fc7642c422c8abe20aaac0d7cc30a3292744eb9fbeafba1bd'
'c70bc71c696b6764247070375ae111bd76c8bad9c7bda4d46e03975b95571a8a')
sha256sums_x86_64=('4710187525d4153d47c1d0366a69829f351b445f568ed091bf62215de25ddf34')
sha256sums_aarch64=('66789d7cd7d423f761d713edc9959ebff86b1493b1718c15346f627b02f6ef74')
sha256sums_x86_64=('06e4c48b71c65a33bdb94b6909aab505237b5939adf8985b332cb14557792816')
sha256sums_aarch64=('df07224b25284b67b5c7d0e8859d16f3d2533abcf588c81437df59e9fd4f4c62')
prepare() {
mkdir -p brave
-1
View File
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
+3 -3
View File
@@ -3,7 +3,7 @@
# Contributor: 37h4n (aarch64 support added by Ethan Reece <aur at ethanreece dot com>)
# Contributor: sh!zeeg (shizeeque@gmail.com) support for non-avx2 CPUs, shell completions.
pkgname=bun-bin
pkgver=1.4.3
pkgver=1.4.2
pkgrel=1
pkgdesc="All-in-one JavaScript runtime built for speed, with bundler, transpiler, test runner, and package manager. Includes bunx, shell completions and support for baseline CPUs"
arch=('x86_64' 'aarch64')
@@ -12,8 +12,8 @@ license=('MIT')
provides=('bun')
conflicts=('bun')
options=('!debug')
sha256sums_x86_64=('3e730528d89775f03f87f05c06787c4df4099d0a37f3f252e7b829cb23816e18' '1fc2edac843102909e3a1be1d8d9802cc6071cf074e67e88231f4ffe0f8b397b' '9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
sha256sums_aarch64=('efa9813da5ed72423bf847f916e8d2c47c0d776add972354026a75e10da9aa21' '9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
sha256sums_x86_64=('36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913' 'c678040f14fe0440eb839d37cbd0ce4c051a32da72806ac97de6a6aab6bf728f' '9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
sha256sums_aarch64=('54328bbc2d9c8e0c9f892c544d66c57a83b84139e34909e5ee81758f1ac8fda7' '9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
source_x86_64=(
"bun-x64.zip::https://github.com/oven-sh/bun/releases/download/bun-v${pkgver}/bun-linux-x64.zip"
"bun-x64-baseline.zip::https://github.com/oven-sh/bun/releases/download/bun-v${pkgver}/bun-linux-x64-baseline.zip"
-10
View File
@@ -1,10 +0,0 @@
{
"source": "local",
"release_ring": "fast",
"upstream": {
"watch": {
"github": "block/buzz",
"pattern": "desktop-v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
}
}
-92
View File
@@ -1,92 +0,0 @@
pkgname=buzz
pkgver=0.5.28
pkgrel=1
pkgdesc='Workspace where humans and AI agents build together, on a Nostr relay you own'
arch=('x86_64' 'aarch64')
url='https://github.com/block/buzz'
license=('Apache-2.0')
depends=(
'alsa-lib'
'cairo'
'gcc-libs'
'gdk-pixbuf2'
'glib2'
'glibc'
'gst-plugins-good'
'gtk3'
'hicolor-icon-theme'
'libayatana-appindicator'
'libsoup3'
'opus'
'webkit2gtk-4.1'
)
makedepends=('cargo' 'cmake' 'nodejs' 'pnpm' 'pkgconf')
conflicts=('buzz-appimage' 'buzz-bin' 'buzz-desktop-git')
# Arch's GCC LTO objects cannot be consumed when crates compile bundled C
# libraries (secp256k1-sys among them).
options=('!lto')
_srcdir="$pkgname-desktop-v$pkgver"
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/desktop-v$pkgver.tar.gz"
'buzz.desktop')
sha256sums=('4ba8c250d6ebb075ba60e6c4d217b77ea605b183161715b77629bd8660b81a43'
'3c152420e67c64f5046cc0e10a9d948e1c7d3efa512582ad279f073b0900526a')
# The sidecars the desktop app spawns from its own directory, from upstream's
# scripts/bundle-sidecars.sh.
_sidecars=(buzz-acp buzz-agent buzz-backend-kubernetes buzz-dev-mcp git-credential-nostr buzz)
prepare() {
cd "$_srcdir"
export npm_config_store_dir="$srcdir/pnpm-store"
pnpm install --frozen-lockfile
# The sidecars build in the root workspace; the app has its own lockfile.
cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
cargo fetch --locked --target "$CARCH-unknown-linux-gnu" \
--manifest-path desktop/src-tauri/Cargo.toml
}
build() {
cd "$_srcdir"
export npm_config_store_dir="$srcdir/pnpm-store"
# audiopus_sys's vendored opus fallback declares an old CMake minimum.
export CMAKE_POLICY_VERSION_MINIMUM=3.5
# sha2-asm assembles ARMv8 SHA instructions, which Arch Linux ARM's
# -march=armv8-a leaves out. sha2 still checks for them at runtime.
[[ $CARCH == aarch64 ]] && CFLAGS=${CFLAGS/-march=armv8-a/-march=armv8-a+crypto}
cargo build --frozen --release -p buzz-acp -p buzz-agent -p buzz-backend-kubernetes \
-p buzz-dev-mcp -p git-credential-nostr -p buzz-cli
./scripts/bundle-sidecars.sh
# Upstream's Linux release build, without its bundles. Leaving
# BUZZ_UPDATER_PUBLIC_KEY and BUZZ_UPDATER_ENDPOINT unset compiles the
# self-updater out: pacman owns updates.
cd desktop
pnpm tauri build --ci --no-bundle --features mesh-llm -- --frozen
}
package() {
cd "$_srcdir"
install -Dm755 desktop/src-tauri/target/release/buzz-desktop "$pkgdir/usr/bin/buzz-desktop"
local bin
for bin in "${_sidecars[@]}"; do
install -Dm755 "target/release/$bin" "$pkgdir/usr/bin/$bin"
done
install -Dm644 "$srcdir/buzz.desktop" "$pkgdir/usr/share/applications/buzz-desktop.desktop"
local icons=desktop/src-tauri/icons size
for size in 32 64 128; do
install -Dm644 "$icons/${size}x$size.png" \
"$pkgdir/usr/share/icons/hicolor/${size}x$size/apps/buzz-desktop.png"
done
install -Dm644 "$icons/128x128@2x.png" "$pkgdir/usr/share/icons/hicolor/256x256/apps/buzz-desktop.png"
install -Dm644 "$icons/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/buzz-desktop.png"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}
-10
View File
@@ -1,10 +0,0 @@
[Desktop Entry]
Type=Application
Name=Buzz
Comment=Workspace where humans and AI agents build together
Exec=buzz-desktop %u
Icon=buzz-desktop
Terminal=false
StartupWMClass=buzz-desktop
Categories=Network;InstantMessaging;Chat;
MimeType=x-scheme-handler/buzz;
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
+4 -4
View File
@@ -4,8 +4,8 @@
# Automation repository: https://github.com/fabifont/claude-code-aur
pkgname=claude-code
pkgver=2.1.296
pkgrel=1
pkgver=2.1.291
pkgrel=2
pkgdesc="An agentic coding tool that lives in your terminal"
arch=('x86_64' 'aarch64')
url="https://github.com/anthropics/claude-code"
@@ -29,8 +29,8 @@ source_x86_64=("claude-${pkgver}-x86_64::https://downloads.claude.ai/claude-code
source_aarch64=("claude-${pkgver}-aarch64::https://downloads.claude.ai/claude-code-releases/${pkgver}/linux-arm64/claude")
sha256sums=('SKIP')
sha256sums_x86_64=('24972e3bc859fab2b46ed4c1e51f7d6130f06d3bd550811a114640de3370d0de')
sha256sums_aarch64=('f1f6e96e0d8342b9dbf41d7e88255397a6a52ce3d8736ad6a4c6b59c9b62fefa')
sha256sums_x86_64=('078fad28d0297c9a25d306b635b2d8816c6839347520f29eb54ffea5d56142fb')
sha256sums_aarch64=('c18473a04cc4f077435d5d9081f09ebea46e699eb2825cea64741c4bccb87647')
package() {
install -Dm755 "${srcdir}/claude-${pkgver}-${CARCH}" "${pkgdir}/opt/claude-code/bin/claude"
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"debian": "https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages",
+4 -4
View File
@@ -6,8 +6,8 @@
# repository's package index.
pkgname=claude-desktop
pkgver=2.31226.1
pkgrel=1
pkgver=2.19675.1
pkgrel=2
pkgdesc="Official Claude desktop app with Claude Code"
arch=('x86_64' 'aarch64')
url="https://claude.ai"
@@ -63,8 +63,8 @@ source_x86_64=("${_deb_x86_64}::${_pool}/${_deb_x86_64}")
source_aarch64=("${_deb_aarch64}::${_pool}/${_deb_aarch64}")
noextract=("${_deb_x86_64}" "${_deb_aarch64}")
sha256sums=('edfdbc63b65891ef7c481b07086c7e630fc102c042b6ed65331a52fcaf72b14a')
sha256sums_x86_64=('5a9bebdfcb1df6ce38767b373f3a7fabf77156a2ddd8a3f5d04961da5bde305e')
sha256sums_aarch64=('d2f57a38ffc73cfda660f8772a5f8bada05e6d74399948a268c6f5d53593ee8f')
sha256sums_x86_64=('9ba127eeccf270f6e60d35f5c5333654053bf0540c88fc82a009d01711b106fc')
sha256sums_aarch64=('681d122ae97d0eb302f0e6d92c7f232847064bb01746458dc50a2c095a40ed12')
package() {
cd "${srcdir}"
-1
View File
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"upstream": {
"watch": {
"github": "bjarneo/cliamp",
+6 -6
View File
@@ -3,8 +3,8 @@
# Maintainer: caarlos0 <carlos@charm.sh>
pkgname='crush-bin'
pkgver=0.98.1
pkgrel=1
pkgver=0.97.1
pkgrel=2
pkgdesc='A powerful terminal-based AI assistant for developers, providing intelligent coding assistance directly in your terminal.'
url='https://charm.sh/crush'
arch=('aarch64' 'armv7h' 'i686' 'x86_64')
@@ -13,16 +13,16 @@ provides=('crush')
conflicts=('crush')
source_aarch64=("${pkgname}_${pkgver}_aarch64.tar.gz::https://github.com/charmbracelet/crush/releases/download/v${pkgver}/crush_${pkgver}_Linux_arm64.tar.gz")
sha256sums_aarch64=('098e875c1d9501822d08ef14ab094f24cfc87396385af5402994bf741a673b99')
sha256sums_aarch64=('16c9477a178d69e02a51bf4babe6174e62051314b2cd7f36b1ec5fea47e25a88')
source_armv7h=("${pkgname}_${pkgver}_armv7h.tar.gz::https://github.com/charmbracelet/crush/releases/download/v${pkgver}/crush_${pkgver}_Linux_armv7.tar.gz")
sha256sums_armv7h=('ba4361b5b2bfe28d7c0063cb25a570c4472d4fe0c10b125af12813b3ab27e25b')
sha256sums_armv7h=('1999746feb3778aa65554df726a04161e98f38f00dc7d565617d88ba0ebc7e33')
source_i686=("${pkgname}_${pkgver}_i686.tar.gz::https://github.com/charmbracelet/crush/releases/download/v${pkgver}/crush_${pkgver}_Linux_i386.tar.gz")
sha256sums_i686=('6d972d44e177130c27a76c4316a8a40ac6a4357dcd2f79b7904a0b31801a0fce')
sha256sums_i686=('a46de190551e8c45f27f36c26247e5a8ff01d082e66e61cb39a8bce77b9fbad0')
source_x86_64=("${pkgname}_${pkgver}_x86_64.tar.gz::https://github.com/charmbracelet/crush/releases/download/v${pkgver}/crush_${pkgver}_Linux_x86_64.tar.gz")
sha256sums_x86_64=('d85d913effef350f0152f594a70bd9749ad455bf15af77bb0d2b731036079885')
sha256sums_x86_64=('1b7cbe0600a3797538a74dc00dd8c4bac54ac4b8f4455ba5ff2312b29c7bd598')
package() {
case "$CARCH" in
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
+5 -5
View File
@@ -1,8 +1,8 @@
# Maintainer: Gunther Schulz <dev@guntherschulz.de>
pkgname=cursor-bin
pkgver=3.24.12
pkgrel=1
pkgver=3.23.23
pkgrel=2
pkgdesc='AI-first coding environment'
arch=('x86_64' 'aarch64')
url="https://www.cursor.com"
@@ -18,13 +18,13 @@ depends_aarch64=(
libxkbcommon libxrandr mesa nspr nss pango systemd-libs which
)
options=(!strip !debug) # Don't break ext of VSCode
_commit=37b865941ccb31e2fc90c685157534efca87f964
_commit=2dac2428994fe34f12658d9ecad1541b98db2c04
source_x86_64=("https://downloads.cursor.com/production/${_commit}/linux/x64/deb/amd64/deb/cursor_${pkgver}_amd64.deb"
"https://gitlab.archlinux.org/archlinux/packaging/packages/code/-/raw/main/code."{sh,mjs}
rg.sh)
sha512sums_x86_64=('336dc2b642cbb6d16a65bbd79cd4241e53c0d1fd3d9189850d7a922d1afcc9c37908d8a1e24d8c06429a12b375e7629a06e5f168c51e7be12e0b57430f6f1b86' '937299c6cb6be2f8d25f7dbc95cf77423875c5f8353b8bd6cd7cc8e5603cbf8405b14dbf8bd615db2e3b36ed680fc8e1909410815f7f8587b7267a699e00ab37' '793f9ff6306e3992ac89802d98110cba288ea1181a901467333293b7d76182ef9792c2a39ff49d9347a18a174b1f42bc58862091dff583f4146c2704eea28033' 'e79fe7659f59d1ae02fc68816399bfd31587315df6cdb6ccf1d0ca76f7cdc692c2a42b30591c0091147bd97ef14b1c7745dc26bd7cb3ea6bba45698e5044fa2a')
sha512sums_x86_64=('acdf6f2677cd817c41fe91181075f8fa2cbf6fe21211c937a750769f1b036911a05c2e48c3f5186cc5e3d71a1b56aac8e6fbfead84a3c86902c39b77f8776328' '937299c6cb6be2f8d25f7dbc95cf77423875c5f8353b8bd6cd7cc8e5603cbf8405b14dbf8bd615db2e3b36ed680fc8e1909410815f7f8587b7267a699e00ab37' '793f9ff6306e3992ac89802d98110cba288ea1181a901467333293b7d76182ef9792c2a39ff49d9347a18a174b1f42bc58862091dff583f4146c2704eea28033' 'e79fe7659f59d1ae02fc68816399bfd31587315df6cdb6ccf1d0ca76f7cdc692c2a42b30591c0091147bd97ef14b1c7745dc26bd7cb3ea6bba45698e5044fa2a')
source_aarch64=("https://downloads.cursor.com/production/${_commit}/linux/arm64/deb/arm64/deb/cursor_${pkgver}_arm64.deb")
sha512sums_aarch64=('fd2fc67f4cbec2e35a43046019ecddf14c025ae06398a1652369839a58fbd0edd565cf433525734c8a114b0bb4b5a3a6c9cfce0cc71739f4f803cbf984275876')
sha512sums_aarch64=('28ae4e335d48a8c9aa189645e75e26a2384462c8d2c910a7a1340442259eda55beb065210f049f895eeb809c4cdc331823e0c6b539a79bab209a94594777d5c0')
noextract=(cursor_${pkgver}_amd64.deb cursor_${pkgver}_arm64.deb) # avoid double tarball
_app=usr/share/cursor/resources/app
package() {
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"min_release_age": "24h",
"upstream": {
"github": "tobi/disktree",
-1
View File
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
+3 -3
View File
@@ -1,8 +1,8 @@
# Maintainer: GM <gianmarcomorales@icloud.com>
pkgname=flea
pkgver=0.3.8
pkgrel=1
pkgver=0.3.7
pkgrel=2
pkgdesc='Fast, keyboard-first file manager for Omarchy'
arch=('x86_64' 'aarch64')
url='https://github.com/thisisgm/flea'
@@ -53,7 +53,7 @@ options=('!debug')
source=(
"$url/releases/download/v$pkgver/$pkgname-v$pkgver.tar.gz"
)
sha256sums=('d3740e8fc1f1cf2b10c97c15427c6feb595daad252fd95e5e74a1c083dd8d81f')
sha256sums=('1f17c57377c59b6e8bf211633c73ef5ca9222b79b3331cf7226cb705dc83d71c')
build() {
cd "$pkgname-$pkgver"
-15
View File
@@ -1,15 +0,0 @@
{
"source": "local",
"release_ring": "fast",
"upstream": {
"github": "ferdousbhai/ghost",
"digests": true,
"latest_only": true,
"sources": {
"any": [
"https://github.com/ferdousbhai/ghost/releases/download/{tag}/ghost-{pkgver}.tar.gz",
"https://github.com/ferdousbhai/ghost/releases/download/{tag}/ghost-runtime-{pkgver}-linux-any.tar.zst"
]
}
}
}
-144
View File
@@ -1,144 +0,0 @@
# Maintainer: Ferdous Bhai <https://github.com/ferdousbhai>
pkgbase=ghost
pkgname=(ghost-runtime ghost)
pkgver=0.6.0
pkgrel=2
pkgdesc="Owner-local AI persona with a character, tools, and an Omarchy-native desktop shell"
arch=('any')
url="https://github.com/ferdousbhai/ghost"
license=('Apache-2.0')
_runtime_depends=(
'at-spi2-core>=2.58'
'bun>=1.3.14'
'grim>=1.5'
'hyprland>=0.56'
'libnotify>=0.8'
'qrencode'
'systemd'
'wl-clipboard'
'wtype>=0.4'
)
_ui_depends=('qt6-5compat>=6.8' 'quickshell>=0.3' 'xdg-utils>=1.2')
depends=("${_runtime_depends[@]}" "${_ui_depends[@]}")
makedepends=(
'bun>=1.4.0'
'desktop-file-utils'
)
checkdepends=(
'jq'
'nodejs'
'python-yaml'
'ripgrep'
)
_runtime_optdepends=(
'chromium: the browser the relay drives, and the browser-relay smoke test'
'npm: npx-launched MCP servers'
)
options=('!debug' '!strip')
# Omarchy's sync-upstream follows the GitHub release named in
# .omarchy/package.json and rewrites pkgver and both checksum arrays.
_release="https://github.com/ferdousbhai/ghost/releases/download/v${pkgver}"
# One runtime archive for every architecture: Bun-target JavaScript, no
# native modules (verified on aarch64, ghost#52).
_runtime="ghost-runtime-${pkgver}-linux-any.tar.zst"
source=(
"ghost-${pkgver}.tar.gz::${_release}/ghost-${pkgver}.tar.gz"
"${_runtime}::${_release}/${_runtime}"
)
noextract=("${_runtime}")
sha256sums=(
'681320615fa6192fe57f8fe6ab1e36ae04a7680d7b371721762bfdedf48ac985'
'1c9c66ca0299dbf9d15a37e541f61352658bcb9700a067eec1081a3bfc1549f5'
)
_release_identity() {
local manifest="$1"
local line key value
declare -A fields=()
[[ -f "$manifest" && ! -L "$manifest" ]] || return 1
while IFS= read -r line || [[ -n "$line" ]]; do
[[ "$line" == *=* ]] || return 1
key="${line%%=*}"
value="${line#*=}"
case "$key" in
format|version|source_commit|source_date_epoch) ;;
*) return 1 ;;
esac
[[ ! -v "fields[$key]" ]] || return 1
fields["$key"]="$value"
done < "$manifest"
[[ "${#fields[@]}" -eq 4 ]] || return 1
[[ "${fields[format]}" == ghost-release-source/v1 ]] || return 1
[[ "${fields[version]}" == "$pkgver" ]] || return 1
[[ "${fields[source_commit]}" =~ ^[0-9a-f]{40}$ ]] || return 1
[[ "${fields[source_date_epoch]}" =~ ^[0-9]+$ ]] || return 1
printf '%s\t%s\n' \
"${fields[source_commit]}" "${fields[source_date_epoch]}"
}
prepare() {
local source_root="$srcdir/ghost-${pkgver}"
local runtime_parent="$srcdir/runtime"
local runtime_root="$runtime_parent/ghost-runtime-${pkgver}-linux-any"
local identity source_commit source_date_epoch
identity="$(_release_identity \
"$source_root/RELEASE-SOURCE.MANIFEST")" || return 1
IFS=$'\t' read -r source_commit source_date_epoch <<< "$identity" || return 1
bash "$source_root/packaging/release/verify-release-source.sh" \
"$source_root" "$pkgver" "$source_commit" "$source_date_epoch" || return 1
export SOURCE_DATE_EPOCH="$source_date_epoch"
install -d -m755 "$runtime_parent"
find "$runtime_parent" -mindepth 1 -delete
bsdtar -xf "$srcdir/$_runtime" -C "$runtime_parent" || return 1
bash "$srcdir/ghost-${pkgver}/packaging/release/verify-runtime-source.sh" \
"$runtime_root" "$source_root" "$pkgver" any "$source_commit" \
"$source_date_epoch" || return 1
}
build() {
# The Bun bundles were built from the frozen lock and verified in
# prepare(); there is no network build step.
:
}
check() {
local source_root="$srcdir/ghost-${pkgver}"
local runtime_root="$srcdir/runtime/ghost-runtime-${pkgver}-linux-any"
local identity source_commit source_date_epoch
identity="$(_release_identity \
"$source_root/RELEASE-SOURCE.MANIFEST")" || return 1
IFS=$'\t' read -r source_commit source_date_epoch <<< "$identity" || return 1
bash "$source_root/packaging/arch/test-check-dependencies.sh" || return 1
bash "$source_root/packaging/release/verify-runtime-source.sh" \
"$runtime_root" "$source_root" "$pkgver" any "$source_commit" \
"$source_date_epoch" || return 1
desktop-file-validate \
"$source_root/packages/shell/contrib/ghost.desktop" || return 1
}
package_ghost-runtime() {
pkgdesc="Ghost daemon, API, CLI, and desktop/browser automation without the HUD"
depends=("${_runtime_depends[@]}")
optdepends=("${_runtime_optdepends[@]}")
conflicts=(ghost-runtime-dev)
install=ghost-runtime.install
local source_root="$srcdir/ghost-${pkgver}"
local runtime_root="$srcdir/runtime/ghost-runtime-${pkgver}-linux-any"
bash "$source_root/packaging/release/install-payload.sh" \
"$source_root" "$runtime_root" "$pkgdir" "$pkgname" runtime
}
package_ghost() {
pkgdesc="Ghost HUD and desktop integration for Omarchy"
depends=("ghost-runtime=$pkgver-$pkgrel" "${_ui_depends[@]}")
conflicts=(ghost-dev)
install=ghost.install
local source_root="$srcdir/ghost-${pkgver}"
local runtime_root="$srcdir/runtime/ghost-runtime-${pkgver}-linux-any"
bash "$source_root/packaging/release/install-payload.sh" \
"$source_root" "$runtime_root" "$pkgdir" "$pkgname" ui
}
-37
View File
@@ -1,37 +0,0 @@
post_install() {
cat <<'TEXT'
Ghost runtime is installed without the Ghost HUD. Enable the daemon in the
current graphical session with:
systemctl --user enable --now ghostd.service
ghost status
The runtime keeps the CLI, authenticated API, desktop helper, and the relay
that the Ghost browser extension pairs with. The extension itself is a
separate install: https://github.com/ferdousbhai/ghost-chromium-extension
(pairing: `ghost browser allow <code>`). An app can supply its own UI.
See /usr/share/doc/ghost/ARCH.md for UI installation and removal.
Ghost homes, credentials, and state belong to you; package removal preserves them.
TEXT
}
post_upgrade() {
cat <<'TEXT'
Ghost runtime was upgraded. Restart it in the graphical user's session:
systemctl --user reenable --now ghostd.service
Ghost homes, credentials, and state were preserved.
TEXT
}
pre_remove() {
cat <<'TEXT'
Before removing the runtime, stop it from the graphical user's session:
systemctl --user disable --now ghostd.service
Removal preserves ghost homes, credentials, and state.
TEXT
}
-32
View File
@@ -1,32 +0,0 @@
post_install() {
cat <<'TEXT'
Ghost's Omarchy UI is installed. Enable it in the graphical user's session:
mkdir -p ~/.config/omarchy/plugins
ln -sfn /usr/share/ghost/plugin ~/.config/omarchy/plugins/ferdousbhai.ghost
omarchy-shell shell rescanPlugins && omarchy plugin enable ferdousbhai.ghost
Summon it with `omarchy-shell shell toggle ferdousbhai.ghost`.
Shell snippets are under /usr/share/doc/ghost/shell-contrib/.
The runtime is managed separately; see /usr/share/doc/ghost/ARCH.md.
TEXT
}
post_upgrade() {
cat <<'TEXT'
Ghost's Omarchy UI was upgraded. Reload its QML by restarting the shell:
omarchy-restart-shell
TEXT
}
pre_remove() {
cat <<'TEXT'
Before removing the UI, disable and unlink its plugin in the graphical session:
omarchy plugin remove ferdousbhai.ghost --yes
omarchy-restart-shell
Removing only the UI leaves Ghost runtime and all owner data available.
TEXT
}
-1
View File
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"npm": "@github/copilot",
+4 -4
View File
@@ -6,8 +6,8 @@ _npmmodule=@github/copilot
pkgname=github-copilot-cli
_pkgexec=copilot
pkgver=1.0.95
pkgrel=1
pkgver=1.0.92
pkgrel=2
pkgdesc="GitHub Copilot CLI brings the power of Copilot coding agent directly to your terminal."
@@ -31,8 +31,8 @@ source=("https://registry.npmjs.org/${_npmmodule}/-/copilot-${pkgver}.tgz"
noextract=("copilot-${pkgver}.tgz")
sha256sums=(
'838be5537db8bd0f7c7c2e555063770ab559249cf2c3ff3c23b2efc81c854779'
'69e525b67e95840ce11eaf128e4c4f94ecba2512ff9321335fd22bdd4d252e47'
'4711a32a1917bf4c64308818a76efb1fb633384ac895312d6e9d12f26b78f95a'
'242fb78a1c6e4ff05acb6b9135fc6e3cb0714ecbe87cddbd8bf69d2fa2559c37'
)
# Document: https://wiki.archlinux.org/title/Node.js_package_guidelines
-1
View File
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"upstream": {
"git_tags": "https://github.com/omacom/gliff.git",
"tag_pattern": "v{pkgver}",
+4 -6
View File
@@ -1,10 +1,10 @@
# Maintainer: Kevin McConnell <kevin@37signals.com>
pkgname=gliff
pkgver=0.3.1
pkgver=0.2.0
pkgrel=1
pkgdesc='Hyprland remote desktop over SSH (GPU H.264, 4:4:4)'
arch=('x86_64' 'aarch64')
arch=('x86_64')
url='https://github.com/omacom/gliff'
license=('MIT' 'BSD-2-Clause')
install='gliff.install'
@@ -24,9 +24,7 @@ depends=(
'vulkan-icd-loader'
'wayland'
)
makedepends=('cargo' 'pkgconf')
# openh264 assembles its x86 kernels with nasm; aarch64 uses the C compiler.
makedepends_x86_64=('nasm')
makedepends=('cargo' 'pkgconf' 'nasm')
optdepends=('vulkan-radeon: Vulkan compute on AMD'
'vulkan-intel: Vulkan compute on Intel'
'intel-media-driver: VA-API H.264 codec on Intel'
@@ -34,7 +32,7 @@ optdepends=('vulkan-radeon: Vulkan compute on AMD'
'libva-utils: vainfo for debugging'
'vulkan-validation-layers: driver call validation for development')
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('5073163c91c2d125e3df7d7159d6113b7e7b00c424a28461609026ad02ea62a2')
sha256sums=('e2c2e6754badff93e4ea4878d0137919870ce8c303ab6da6f962b7ce6f3f80a6')
prepare() {
cd "$pkgname-$pkgver"
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
-1
View File
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"origin": {
"aur": "grok-bot",
-1
View File
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"upstream": {
"git_tags": "https://github.com/omacom/hype.git",
"tag_pattern": "v{pkgver}",
@@ -1,3 +0,0 @@
{
"source": "local"
}
@@ -1,19 +0,0 @@
# Run before mkinitcpio and Limine discover installed kernels.
[Trigger]
Type = Path
Operation = Install
Operation = Upgrade
Operation = Remove
Target = usr/lib/modules/*/
[Trigger]
Type = Package
Operation = Install
Operation = Upgrade
Target = linux-aarch64-pkgbase-shim
[Action]
Description = Writing pkgbase and vmlinuz for Arch Linux ARM kernels...
When = PostTransaction
Exec = /usr/share/libalpm/scripts/linux-aarch64-pkgbase-shim
NeedsTargets
@@ -1,26 +0,0 @@
# Maintainer: Jimmy Van Veen
# Add kernel metadata required by mkinitcpio and Limine on Arch Linux ARM.
pkgname=linux-aarch64-pkgbase-shim
pkgver=1
pkgrel=4
pkgdesc="Writes usr/lib/modules/<ver>/{pkgbase,vmlinuz} for Arch Linux ARM kernels so mkinitcpio and Limine hooks find them"
arch=('aarch64')
url="https://github.com/omacom/omarchy-pkgs"
license=('MIT')
depends=('bash' 'diffutils' 'pacman')
optdepends=('limine-mkinitcpio-hook: rebuild the UKI and Limine entry when the shim is installed after the kernel')
options=('!debug')
source=(
'85-linux-aarch64-pkgbase-shim.hook'
'linux-aarch64-pkgbase-shim'
'README.package.md'
)
sha256sums=('SKIP' 'SKIP' 'SKIP')
package() {
install -Dm644 85-linux-aarch64-pkgbase-shim.hook "${pkgdir}/usr/share/libalpm/hooks/85-linux-aarch64-pkgbase-shim.hook"
install -Dm755 linux-aarch64-pkgbase-shim "${pkgdir}/usr/share/libalpm/scripts/linux-aarch64-pkgbase-shim"
install -Dm644 README.package.md "${pkgdir}/usr/share/doc/${pkgname}/README.md"
}
@@ -1,31 +0,0 @@
# linux-aarch64-pkgbase-shim
Arch Linux ARM installs its kernel as `/boot/Image` instead of
`usr/lib/modules/<ver>/vmlinuz`. Limine discovers the kernel through
`modules.builtin`, but needs `vmlinuz` beside it to build a boot entry.
This package is one pacman hook. After a kernel package is installed or
upgraded (`usr/lib/modules/*/`), and when the shim itself is installed, it
writes into each package-owned modules directory without native kernel metadata:
- `pkgbase`: compatibility metadata naming the package that owns the directory
- `vmlinuz`: a copy of `/boot/Image` (the owner must own that too)
Reinstalling a kernel refreshes the shim's copy when the image changes, even
if the module-directory version is unchanged. Package-owned files are preserved.
When the kernel and its headers share ownership of the module directory, the
shim uses the owner of `/boot/Image`, after confirming that it also owns the
directory. Headers-package ownership alone does not qualify a directory.
It runs as `85-`, before `90-mkinitcpio-install`, so the usual hook then builds
the initramfs/UKI and the Limine entry. If that hook would not run in the same
transaction (the shim installed after the kernel, or a kernel package that
does not touch `usr/lib/initcpio/`), the script runs
`limine-mkinitcpio-install` itself. Leftover directories of removed kernels
that hold nothing but these two files are cleaned up.
## Retirement
Delete this package when `linux-aarch64` ships `vmlinuz` in its kernel module
directory. Package-owned metadata is never modified, so both approaches can coexist
during the transition.
@@ -1,108 +0,0 @@
#!/bin/bash
# Supply pkgbase and vmlinuz metadata missing from Arch Linux ARM kernels.
set -uo pipefail
readonly SELF=linux-aarch64-pkgbase-shim
readonly ROOT=${OMARCHY_KERNEL_SHIM_ROOT:-}
readonly IMAGE=$ROOT/boot/Image
readonly LIMINE_INSTALL_SCRIPT=$ROOT/usr/share/libalpm/scripts/limine-mkinitcpio-install
declare -A DIRS=()
modules_in_transaction=0
self_in_transaction=0
rebuild=0
failed=0
owner_of() {
pacman -Qqo "${1#"$ROOT"}" 2>/dev/null
}
# NeedsTargets requires consuming every line from stdin.
while IFS= read -r line; do
case $line in
usr/lib/modules/*)
v=${line#usr/lib/modules/}
v=${v%%/*}
[[ -n $v ]] && DIRS["$ROOT/usr/lib/modules/$v"]=1
modules_in_transaction=1
;;
"$SELF")
self_in_transaction=1
;;
esac
done
# Cover kernels already present when the shim itself is installed.
if ((self_in_transaction)); then
for d in "$ROOT"/usr/lib/modules/*/; do
[[ -d $d ]] && DIRS["${d%/}"]=1
done
fi
for dir in "${!DIRS[@]}"; do
[[ -d $dir ]] || continue
# Not a packaged kernel (a leftover or DKMS-only directory).
owners=$(owner_of "$dir") || continue
owner=$(owner_of "$IMAGE") || continue
# The kernel and its headers can both own the directory. Use the image's
# single owner, provided it is also one of the directory's owners.
[[ -n $owner && $owner != *$'\n'* ]] || continue
if ! printf '%s\n' "$owners" | grep -Fx -- "$owner" >/dev/null; then
echo "$SELF: $owner owns $IMAGE but not $dir, leaving it alone" >&2
continue
fi
# Preserve package-owned files; refresh metadata previously written by the shim.
owner_of "$dir/pkgbase" >/dev/null && continue
owner_of "$dir/vmlinuz" >/dev/null && continue
if [[ -e $dir/pkgbase && $(<"$dir/pkgbase") != "$owner" ]]; then
echo "$SELF: $dir/pkgbase does not name $owner, leaving $dir alone" >&2
continue
fi
if [[ -f $dir/pkgbase ]] && cmp -s "$IMAGE" "$dir/vmlinuz"; then
continue
fi
image_tmp=$(mktemp "$dir/.vmlinuz.XXXXXX") || { failed=1; continue; }
if ! cp --preserve=mode "$IMAGE" "$image_tmp" || ! mv -f "$image_tmp" "$dir/vmlinuz"; then
echo "$SELF: failed to copy $IMAGE to $dir/vmlinuz" >&2
rm -f "$image_tmp"
failed=1
continue
fi
if ! printf '%s\n' "$owner" >"$dir/pkgbase"; then
failed=1
continue
fi
echo "$SELF: wrote pkgbase ($owner) and vmlinuz in $dir"
# Rebuild when the transaction will not trigger mkinitcpio itself.
if ! ((modules_in_transaction)) || ! pacman -Qlq "$owner" 2>/dev/null | grep '^/usr/lib/initcpio/' >/dev/null; then
rebuild=1
fi
done
# Remove directories containing only stale shim metadata.
for dir in "$ROOT"/usr/lib/modules/*/; do
dir=${dir%/}
[[ -f $dir/pkgbase && -f $dir/vmlinuz ]] || continue
owner_of "$dir" >/dev/null && continue
owner_of "$dir/pkgbase" >/dev/null && continue
owner_of "$dir/vmlinuz" >/dev/null && continue
[[ -z $(find "$dir" -mindepth 1 ! -name pkgbase ! -name vmlinuz -print -quit) ]] || continue
rm -f "$dir/pkgbase" "$dir/vmlinuz" && rmdir "$dir" && echo "$SELF: removed leftover $dir"
done
# Respect the installer's deferred boot-image build.
deferred_hook=$ROOT/etc/pacman.d/hooks/90-mkinitcpio-install.hook
if ((rebuild)) && [[ -L $deferred_hook && $(readlink "$deferred_hook") == /dev/null ]]; then
echo "$SELF: $deferred_hook is masked, leaving the rebuild to limine-update"
rebuild=0
fi
if ((rebuild)); then
if [[ -x $LIMINE_INSTALL_SCRIPT ]]; then
echo rebuild | "$LIMINE_INSTALL_SCRIPT" || failed=1
else
echo "$SELF: $LIMINE_INSTALL_SCRIPT not found; run mkinitcpio for the new kernel yourself" >&2
fi
fi
exit "$failed"
@@ -1,136 +0,0 @@
#!/bin/bash
set -euo pipefail
package_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
export OMARCHY_KERNEL_SHIM_ROOT=$scratch
export SHIM_NATIVE=0 SHIM_KERNEL_PRESENT=1
export SHIM_IMAGE_OWNER=linux-aarch64
export SHIM_DIR_OWNERS=linux-aarch64
pacman() {
if [[ $1 == -Qlq ]]; then
printf '/usr/lib/initcpio/install/test\n'
return 0
fi
case $2 in
/boot/Image)
printf '%s\n' "$SHIM_IMAGE_OWNER"
;;
/usr/lib/modules/test)
((SHIM_KERNEL_PRESENT)) || return 1
printf '%s\n' "$SHIM_DIR_OWNERS"
;;
/usr/lib/modules/test/pkgbase | /usr/lib/modules/test/vmlinuz)
[[ $SHIM_NATIVE == 1 || $2 == /usr/lib/modules/test/"$SHIM_NATIVE" ]] || return 1
printf 'linux-aarch64\n'
;;
*) return 1 ;;
esac
}
export -f pacman
modules=$scratch/usr/lib/modules/test
mkdir -p "$modules" "$scratch/boot" "$scratch/etc/pacman.d/hooks" \
"$scratch/usr/share/libalpm/scripts"
printf 'first-kernel' >"$scratch/boot/Image"
cat >"$scratch/usr/share/libalpm/scripts/limine-mkinitcpio-install" <<'SCRIPT'
#!/bin/bash
cat >>"$OMARCHY_KERNEL_SHIM_ROOT/rebuilds"
SCRIPT
chmod +x "$scratch/usr/share/libalpm/scripts/limine-mkinitcpio-install"
run_shim() {
printf '%s\n' "$1" | bash "$package_dir/linux-aarch64-pkgbase-shim"
}
run_shim usr/lib/modules/test/
[[ $(<"$modules/pkgbase") == linux-aarch64 ]]
cmp "$scratch/boot/Image" "$modules/vmlinuz"
[[ $(stat -c %a "$modules/vmlinuz") == $(stat -c %a "$scratch/boot/Image") ]]
[[ ! -e $scratch/rebuilds ]]
echo 'ok - kernel installation supplies metadata before the normal rebuild hook'
for SHIM_DIR_OWNERS in $'linux-aarch64\nlinux-aarch64-headers' \
$'linux-aarch64-headers\nlinux-aarch64'; do
rm "$modules/pkgbase" "$modules/vmlinuz"
run_shim usr/lib/modules/test/
[[ $(<"$modules/pkgbase") == linux-aarch64 ]]
cmp "$scratch/boot/Image" "$modules/vmlinuz"
done
echo 'ok - shared kernel and headers directories use the image owner in either order'
SHIM_DIR_OWNERS=linux-aarch64
printf 'replacement-kernel' >"$scratch/boot/Image"
run_shim usr/lib/modules/test/
cmp "$scratch/boot/Image" "$modules/vmlinuz"
echo 'ok - reinstalling the same kernel version refreshes its image'
printf 'shim-upgrade' >"$scratch/boot/Image"
run_shim linux-aarch64-pkgbase-shim
[[ $(<"$scratch/rebuilds") == rebuild ]]
run_shim linux-aarch64-pkgbase-shim
[[ $(wc -l <"$scratch/rebuilds") == 1 ]]
echo 'ok - unchanged images do not trigger redundant rebuilds'
ln -s /dev/null "$scratch/etc/pacman.d/hooks/90-mkinitcpio-install.hook"
printf 'deferred-kernel' >"$scratch/boot/Image"
run_shim linux-aarch64-pkgbase-shim
cmp "$scratch/boot/Image" "$modules/vmlinuz"
[[ $(wc -l <"$scratch/rebuilds") == 1 ]]
echo 'ok - the installer can defer boot-image regeneration'
for SHIM_NATIVE in pkgbase vmlinuz; do
printf 'package-owned' >"$modules/vmlinuz"
run_shim usr/lib/modules/test/
[[ $(<"$modules/vmlinuz") == package-owned ]]
done
echo 'ok - either package-owned metadata file prevents replacement'
SHIM_NATIVE=0
SHIM_IMAGE_OWNER=another-kernel
run_shim usr/lib/modules/test/
[[ $(<"$modules/vmlinuz") == package-owned ]]
SHIM_IMAGE_OWNER=linux-aarch64
echo 'ok - a different kernel image owner prevents replacement'
SHIM_DIR_OWNERS=linux-aarch64-headers
run_shim usr/lib/modules/test/
[[ $(<"$modules/vmlinuz") == package-owned ]]
SHIM_DIR_OWNERS=$'linux-aarch64\nlinux-aarch64-headers'
SHIM_IMAGE_OWNER=$'linux-aarch64\nlinux-aarch64-headers'
run_shim usr/lib/modules/test/
[[ $(<"$modules/vmlinuz") == package-owned ]]
SHIM_DIR_OWNERS=linux-aarch64
SHIM_IMAGE_OWNER=linux-aarch64
echo 'ok - headers-only directories and ambiguous image ownership prevent replacement'
printf 'another-kernel\n' >"$modules/pkgbase"
run_shim usr/lib/modules/test/ 2>"$scratch/warning"
[[ $(<"$modules/vmlinuz") == package-owned ]]
grep -Fq 'pkgbase does not name linux-aarch64' "$scratch/warning"
printf 'linux-aarch64\n' >"$modules/pkgbase"
echo 'ok - a pkgbase naming another package is reported and preserved'
(
# shellcheck disable=SC2329 # Invoked by the shim in a child shell.
cp() { return 1; }
export -f cp
if run_shim usr/lib/modules/test/; then
echo 'not ok - a failed image copy was accepted' >&2
exit 1
fi
[[ $(<"$modules/vmlinuz") == package-owned ]]
)
echo 'ok - failed copies preserve the previous kernel image'
SHIM_KERNEL_PRESENT=0
mkdir -p "$modules/updates/dkms"
run_shim usr/lib/modules/test/
[[ -d $modules/updates/dkms && -f $modules/vmlinuz ]]
rmdir "$modules/updates/dkms" "$modules/updates"
echo 'ok - kernel removal preserves DKMS leftovers'
run_shim usr/lib/modules/test/
[[ ! -d $modules ]]
echo 'ok - kernel removal cleans up leftover shim metadata'
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
+10 -22
View File
@@ -1,11 +1,11 @@
# Maintainer: noureddinex
pkgname=lmstudio-bin
pkgver=0.4.26
pkgrel=1
_build=4
pkgver=0.4.25
pkgrel=3
_build=1
_pkgver=${pkgver}-${_build}
pkgdesc="LM Studio - A desktop app for exploring and running large language models locally"
arch=('x86_64' 'aarch64')
arch=('x86_64')
url="https://lmstudio.ai"
license=('EULA')
depends=('fuse2' 'zlib' 'hicolor-icon-theme' 'gtk3' 'nss' 'libxcrypt-compat')
@@ -13,25 +13,13 @@ options=('!strip')
install=lmstudio-bin.install
provides=(lmstudio)
conflicts=(lmstudio)
source=("lmstudio.png"
"lmstudio.desktop")
source_x86_64=("https://installers.lmstudio.ai/linux/x64/${_pkgver}/LM-Studio-${_pkgver}-x64.AppImage")
source_aarch64=("https://installers.lmstudio.ai/linux/arm64/${_pkgver}/LM-Studio-${_pkgver}-arm64.AppImage")
sha256sums=('9f791789c959a11316328692807737a5f1bc1c170ae99ec04c56bfd8ee8263e5' '16b67b6cd672a05b9a0e012c8b9a91187ceda8f85b3391db471298c54e002bcd')
sha256sums_x86_64=('2c62a38a93ad03ca5daaef28804880be167566783ffafc173ce2e1cb10b7552e')
sha256sums_aarch64=('8de222cd9d332496494e28f9863cddc9509802ad213c41e7422f15b9f63416d8')
# LM Studio names its AppImages by a vendor architecture suffix. Resolve it in the
# functions so sourcing the PKGBUILD without CARCH still reads the version.
_appimage() {
case "$CARCH" in
x86_64) echo "LM-Studio-${_pkgver}-x64.AppImage" ;;
aarch64) echo "LM-Studio-${_pkgver}-arm64.AppImage" ;;
esac
}
source=("https://installers.lmstudio.ai/linux/x64/${_pkgver}/LM-Studio-${_pkgver}-x64.AppImage"
"lmstudio.png"
"lmstudio.desktop")
sha256sums=('eca467446c833824697e8befab300fe5269fdf984e3ee4385fcbad8502f07c53' '9f791789c959a11316328692807737a5f1bc1c170ae99ec04c56bfd8ee8263e5' '16b67b6cd672a05b9a0e012c8b9a91187ceda8f85b3391db471298c54e002bcd')
prepare() {
chmod +x "${srcdir}/$(_appimage)"
chmod +x "${srcdir}/${source[0]##*/}"
}
package() {
@@ -39,7 +27,7 @@ package() {
install -dm755 "$pkgdir/opt/lm-studio"
# Install AppImage
install -Dm755 "${srcdir}/$(_appimage)" "$pkgdir/opt/lm-studio/lm-studio.AppImage"
install -Dm755 "${srcdir}/${source[0]##*/}" "$pkgdir/opt/lm-studio/lm-studio.AppImage"
# Install static icon
install -Dm644 "${srcdir}/lmstudio.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/lmstudio-bin.png"
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
+3 -3
View File
@@ -10,8 +10,8 @@ pkgname=microsoft-edge-stable-bin
_pkgname=microsoft-edge
_pkgshortname=msedge
_channel=stable
pkgver=155.0.4283.45
pkgrel=1
pkgver=154.0.4258.62
pkgrel=2
pkgdesc="A browser that combines a minimal design with sophisticated technology to make the web faster, safer, and easier"
arch=('x86_64')
url="https://www.microsoftedgeinsider.com/en-us/download"
@@ -33,7 +33,7 @@ options=(!strip !zipman !emptydirs)
source=("https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/${_pkgname}-${_channel}_${pkgver}-1_amd64.deb"
"microsoft-edge-stable.sh"
"Microsoft Standard Application License Terms - Standalone (free) Use Terms.pdf")
sha256sums=('35ac88189ed91b60cff446fb6020ca60513ce20f985010d3c5b996115e3ec4a4' 'dc3765d2de6520b13f105b8001aa0e40291bc9457ac508160b23eea8811e26af' 'edf2ed596eb068f168287fc76aa713ad5e0afb59f0a0a47a4f29c0c124ade15e')
sha256sums=('f425dea59f0a8bac00201685a070e947543c57d135c130ba73e0863cf2c506b1' 'dc3765d2de6520b13f105b8001aa0e40291bc9457ac508160b23eea8811e26af' 'edf2ed596eb068f168287fc76aa713ad5e0afb59f0a0a47a4f29c0c124ade15e')
package() {
bsdtar -xf data.tar.xz -C "$pkgdir/"
-1
View File
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"min_release_age": "24h",
"upstream": {
+4 -4
View File
@@ -1,7 +1,7 @@
# Maintainer: Jeff Dickey <releases@mise.jdx.dev>
pkgname=mise-bin
pkgver=2026.10.7
pkgrel=1
pkgver=2026.10.3
pkgrel=2
pkgdesc="dev tools, env vars, task runner"
arch=('x86_64' 'aarch64')
url="https://github.com/jdx/mise"
@@ -14,8 +14,8 @@ provides=('mise')
conflicts=('mise')
source_x86_64=("https://github.com/jdx/mise/releases/download/v${pkgver}/mise-v${pkgver}-linux-x64.tar.xz")
source_aarch64=("https://github.com/jdx/mise/releases/download/v${pkgver}/mise-v${pkgver}-linux-arm64.tar.xz")
sha256sums_x86_64=('207c9955520eea3f16ef6b0233833e3c72c07484722446b4825f87686e8c16b0')
sha256sums_aarch64=('799d8e121c5c2d833c61f8bf8128fa736fe35f61528279342a1b08e9a7be36b6')
sha256sums_x86_64=('ff0870ddad7f8c5ba673ceb3e7659f0353da8263eabe8b82220f5816a772c786')
sha256sums_aarch64=('a5141f834d883239c2542b9a2136e75428779b2040206537399714d8c534d91f')
package() {
install -Dm755 "${srcdir}/mise/bin/mise" "${pkgdir}/usr/bin/mise"
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"upstream": {
"git_tags": "https://github.com/omacom/monologue.git",
"tag_pattern": "v{pkgver}",
+3 -3
View File
@@ -1,8 +1,8 @@
# Maintainer: David Heinemeier Hansson <david@hey.com>
pkgname=monologue
pkgver=0.3.1
pkgrel=1
pkgver=0.3.0
pkgrel=2
pkgdesc='A simple, theme-synced webcam recorder for Omarchy'
arch=('x86_64' 'aarch64')
url='https://github.com/omacom/monologue'
@@ -20,7 +20,7 @@ depends=(
)
makedepends=('gcc' 'make' 'pkgconf')
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('b32c22d9efbf54bbd56a40174da0029e924dcfcf410188805d4e26a5ffe3907c')
sha256sums=('03761403736a600f9c2b6c53b57fbe95e6c79bb42758412d9e43d91c0ffd6999')
build() {
cd "$srcdir/$pkgname-$pkgver"
+1 -2
View File
@@ -1,4 +1,3 @@
{
"source": "local",
"auto_merge": true
"source": "local"
}
@@ -1,9 +0,0 @@
{
"source": "local",
"upstream": {
"watch": {
"github": "jorgemanrubia/omacharts",
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
}
}
-92
View File
@@ -1,92 +0,0 @@
# Maintainer: Jorge Manrubia <jorge@37signals.com>
pkgname=omacharts
pkgver=0.1.13
pkgrel=1
pkgdesc='Fast, beautiful charting software for Omarchy'
arch=('x86_64' 'aarch64')
url='https://github.com/jorgemanrubia/omacharts'
license=('MIT')
depends=('gtk4' 'libadwaita')
makedepends=('cargo')
# SQLite and ring are compiled from C by build scripts. makepkg's default
# link-time optimisation hands the linker GCC's LTO bytecode, which rustc's
# linker does not read, and every one of their symbols comes back undefined.
options=('!lto')
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('4f5866cca25d955a05743734bf7bfcdcdce385dc3f373eb4dca6ba43c298d7cf')
prepare() {
cd "$pkgname-$pkgver"
# Vendor up front so the build itself needs no network, which is what
# makepkg expects and what a clean chroot enforces.
export RUSTUP_TOOLCHAIN=stable
cargo fetch --locked --target "$(rustc -vV | sed -n 's/^host: //p')"
}
build() {
cd "$pkgname-$pkgver"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo build --frozen --release --bin omacharts
}
check() {
cd "$pkgname-$pkgver"
export RUSTUP_TOOLCHAIN=stable
# The engine's tests and the app's own; neither needs a display.
cargo test --frozen --release --workspace
}
package() {
cd "$pkgname-$pkgver"
local id='com.jorgemanrubia.Omacharts'
install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname"
install -Dm644 "packaging/$id.desktop" "$pkgdir/usr/share/applications/$id.desktop"
for size in 16 24 32 48 64 128 256 512; do
install -Dm644 "assets/icons/${size}x${size}/$id.png" \
"$pkgdir/usr/share/icons/hicolor/${size}x${size}/apps/$id.png"
done
install -Dm644 "assets/icons/$id.svg" \
"$pkgdir/usr/share/icons/hicolor/scalable/apps/$id.svg"
# The agent skill, and the Claude Code plugin manifest that wraps it.
# Installed so that `omacharts skill install` has one directory to link every
# agent at, and so the plugin can be added from a path on the machine rather
# than fetched again. Codex needs no manifest — the skill directory is the
# whole story there.
#
# Putting the files here changes nothing about anybody's agent: no route runs
# on its own. Installing a charting app must not write into an agent's
# configuration, so there is no post-install hook here and there is not going
# to be one — see src/cli/skill.rs.
local plugin="$pkgdir/usr/share/$pkgname/agents"
install -Dm644 agents/.claude-plugin/plugin.json "$plugin/.claude-plugin/plugin.json"
install -Dm644 agents/.claude-plugin/marketplace.json "$plugin/.claude-plugin/marketplace.json"
install -Dm644 agents/skills/omacharts/SKILL.md "$plugin/skills/omacharts/SKILL.md"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
install -Dm644 doc/cli.md "$pkgdir/usr/share/doc/$pkgname/cli.md"
# Generated by running the binary that is about to be installed, so the
# completions and the man page can never describe a different version of
# the command surface from the one on PATH. Needs no display: these are
# answered before anything GTK touches.
#
# Without a bus, though. A command is handed to an omacharts that is
# already running, so building this with the app open would have the
# running one answer — and ship a man page describing whichever binary
# that happened to be. A clean chroot has no bus and never noticed.
local -x DBUS_SESSION_BUS_ADDRESS=
local bin="target/release/$pkgname"
"$bin" surface --completions bash |
install -Dm644 /dev/stdin "$pkgdir/usr/share/bash-completion/completions/$pkgname"
"$bin" surface --completions zsh |
install -Dm644 /dev/stdin "$pkgdir/usr/share/zsh/site-functions/_$pkgname"
"$bin" surface --completions fish |
install -Dm644 /dev/stdin "$pkgdir/usr/share/fish/vendor_completions.d/$pkgname.fish"
"$bin" surface --man |
install -Dm644 /dev/stdin "$pkgdir/usr/share/man/man1/$pkgname.1"
}
@@ -1,9 +0,0 @@
{
"source": "local",
"upstream": {
"watch": {
"github": "michaelmonetized/omadesign",
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
}
}
-34
View File
@@ -1,34 +0,0 @@
# Maintainer: michaelmonetized (https://github.com/michaelmonetized)
pkgname=omadesign-bin
pkgver=0.6.3
pkgrel=1
pkgdesc='Native Linux studio for design, layout, painting, photography, and motion'
arch=('x86_64' 'aarch64')
url='https://github.com/michaelmonetized/omadesign'
license=('MIT' 'CDDL-1.0')
depends=('glibc' 'gcc-libs' 'fontconfig' 'hicolor-icon-theme' 'libglvnd' 'libx11'
'libxcursor' 'libxi' 'libxrandr' 'libxkbcommon' 'libxkbcommon-x11'
'vulkan-icd-loader' 'wayland' 'xdg-desktop-portal')
optdepends=('vulkan-driver: GPU rendering with a Vulkan-capable driver'
'xdg-desktop-portal-impl: file picker backend for your desktop'
'ghostscript: EPS and PostScript-based Illustrator import')
provides=("omadesign=$pkgver")
conflicts=('omadesign')
options=('!strip' '!debug')
source_x86_64=("$url/releases/download/v$pkgver/omadesign-$pkgver-x86_64-unknown-linux-gnu.tar.gz")
source_aarch64=("$url/releases/download/v$pkgver/omadesign-$pkgver-aarch64-unknown-linux-gnu.tar.gz")
sha256sums_x86_64=('b8642376a8ab977988830cec853c2870a6a468b5c337f19e4edb576401247960')
sha256sums_aarch64=('3cac3ace9c737cc12454e29f90bfb705b6389de259a459e9ec3600494cc2c776')
package() {
local release_dir="$srcdir/omadesign-$pkgver-$CARCH-unknown-linux-gnu"
install -Dm755 "$release_dir/omadesign" "$pkgdir/usr/bin/omadesign"
install -Dm644 "$release_dir/omadesign.desktop" "$pkgdir/usr/share/applications/omadesign.desktop"
install -Dm644 "$release_dir/omadesign.svg" "$pkgdir/usr/share/icons/hicolor/scalable/apps/omadesign.svg"
install -Dm644 "$release_dir/omadesign-mime.xml" "$pkgdir/usr/share/mime/packages/omadesign.xml"
install -Dm644 "$release_dir/README.md" "$pkgdir/usr/share/doc/omadesign/README.md"
install -Dm644 "$release_dir/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm644 "$release_dir/LICENSE-Phosphor" "$pkgdir/usr/share/licenses/$pkgname/LICENSE-Phosphor"
install -d "$pkgdir/usr/share/omadesign"
cp -a "$release_dir"/{lib,licenses,docs,skills,plugins} "$pkgdir/usr/share/omadesign/"
}
+3 -3
View File
@@ -1,6 +1,6 @@
pkgname=omakade
pkgver=1.16.1
pkgrel=1
pkgver=1.15.0
pkgrel=2
pkgdesc='A beautiful, local-first game library for Omarchy'
arch=('x86_64' 'aarch64')
url='https://btsouth.github.io/omakade/'
@@ -13,7 +13,7 @@ optdepends=('libpulse: switch Game Mode sound outputs with pactl'
makedepends=('cmake' 'ninja' 'pkgconf' 'wayland-protocols')
options=('!debug')
source=("$pkgname-$pkgver.tar.gz::https://github.com/btsouth/omakade/releases/download/v$pkgver/$pkgname-$pkgver.tar.gz")
sha256sums=('12d0dd6cabc50da00b67bed2a0b78f250fc082ad03799eaed55541b1103c0ae8')
sha256sums=('cb5bfd208a151f6071956555c6afd93efc04a0490e951523bf5ec7d0f5e2b982')
build() {
cmake -S "$pkgname-$pkgver" -B build -G Ninja \
@@ -1,6 +1,5 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
+8 -13
View File
@@ -1,13 +1,13 @@
# Maintainer: Ryan Hughes <ryan@omarchy.org>
pkgname='omarchy-dev'
pkgver=4.0.0.r6911.g9480d1b
pkgver=4.0.0.r6815.g50d687a
pkgrel=1
# Pinned by the upstream watch in .omarchy/package.json (bin/sync-upstream):
# every quattro tip becomes a commit pin here, so the package is versioned,
# checksummed and built exactly like a release, just more often. The r-number
# is the branch's total commit count, not the distance from the last tag: the
# published history used the total, and pacman must never see it go down.
_commit=9480d1b6a98156314dbabe6bfef08f39f8b9cd07
_commit=50d687a1f27063513cf42e6f33c3a4fcb17c24ed
pkgdesc='Beautiful, modern, and opinionated Arch Linux by DHH (quattro branch tip)'
# The payload is architecture-independent, but the dependency set is not: the
# boot stack differs per architecture (see depends_x86_64 / depends_aarch64),
@@ -66,10 +66,10 @@ depends_x86_64=(
# Apple Silicon boots through m1n1 + GRUB from the Asahi packages and keeps
# Arch Linux ARM's kernel and boot layout, so the Limine/Snapper stack does not
# apply. Only Apple Silicon's Broadcom Wi-Fi needs iwd, and omarchy-mac depends
# on it. Elsewhere the package only renames the Wi-Fi interface to wlan0
# (80-iwd.link), and a saved connection tied to the old name stops connecting.
# apply. Wi-Fi on the Broadcom parts only scans reliably through the iwd
# backend, which install/hardware/network.sh selects on Apple Silicon.
depends_aarch64=(
'iwd'
'networkmanager'
)
@@ -81,7 +81,7 @@ makedepends=(
# build from a local source tree instead (mirrors omarchy-zsh/omarchy-fish DX);
# the arrays are emptied below so nothing is downloaded in that case.
source=("omarchy::git+https://github.com/basecamp/omarchy.git#commit=${_commit}")
sha256sums=('a6961a7a1d7d2f60bc966f3177ac6ffad404452410bf660322701791fd5641a3')
sha256sums=('8f26a967de236f5c0d2b34e9e7119ad13fe7a9db67028f0ad1a4bf2ddf8c40cf')
if [[ -n "${OMARCHY_SRC:-}" ]]; then
source=()
sha256sums=()
@@ -96,23 +96,18 @@ prepare() {
}
package() {
# omarchy-update cannot start without the library settings now ships. Set
# here, not above: makepkg checks only the top-level depends before
# building, against a repository that may not publish that settings yet.
depends+=('omarchy-security-functions')
cd "$srcdir/omarchy"
# Runtime binaries — ship everything in bin/ except the debug helpers
# that ship from omarchy-settings (needed before omarchy is installed,
# e.g. on the ISO live env), and the sudo grant helpers settings owns.
# e.g. on the ISO live env).
install -d "$pkgdir/usr/bin" "$pkgdir/usr/share/omarchy/bin"
for bin in bin/*; do
[[ -f $bin ]] || continue
local base
base=$(basename "$bin")
case "$base" in
omarchy-debug|omarchy-debug-idle|omarchy-upload-log|omarchy-sudo-passwordless|omarchy-security-functions)
omarchy-debug|omarchy-debug-idle|omarchy-upload-log)
continue
;;
esac
@@ -1,23 +1,6 @@
{
"source": "local",
"upstream": {
"git_tags": "https://github.com/omacom/omarchy-hyprland-titlebars.git",
"tag_pattern": "v{pkgver}",
"sources": {
"any": [
"https://github.com/omacom/omarchy-hyprland-titlebars/archive/refs/tags/{tag}.tar.gz"
]
}
},
"rebuild_on": [
"hyprland"
],
"rebuilt_against": {
"x86_64": {
"hyprland": "0.56.2-4"
},
"aarch64": {
"hyprland": "0.56.2-5"
}
}
]
}
@@ -0,0 +1,30 @@
#include "BarPassElement.hpp"
#include <hyprland/src/render/OpenGL.hpp>
#include <hyprland/src/render/Renderer.hpp>
#include "barDeco.hpp"
using namespace Render::GL;
COmarchyFloatingBarPassElement::COmarchyFloatingBarPassElement(const COmarchyFloatingBarPassElement::SBarData &data_) : data(data_) { ; }
std::vector<UP<IPassElement>> COmarchyFloatingBarPassElement::draw() {
data.deco->renderPass(g_pHyprRenderer->m_renderData.pMonitor.lock(), data.a);
return {};
}
bool COmarchyFloatingBarPassElement::needsLiveBlur() {
static auto PENABLEBLURGLOBAL = CConfigValue<Config::BOOL>("decoration:blur:enabled");
CHyprColor color = data.deco->m_bForcedBarColor.value_or(CHyprColor{static_cast<uint64_t>(g_pOmarchyFloatingGlobalState->config.barColor->value())});
color.a *= data.a;
const bool SHOULDBLUR = g_pOmarchyFloatingGlobalState->config.barBlur->value() && *PENABLEBLURGLOBAL && color.a < 1.F;
return SHOULDBLUR;
}
std::optional<CBox> COmarchyFloatingBarPassElement::boundingBox() {
// Temporary fix: expand the bar bb a bit, otherwise occlusion gets too aggressive.
return data.deco->assignedBoxGlobal().translate(-g_pHyprRenderer->m_renderData.pMonitor->m_position).expand(10);
}
bool COmarchyFloatingBarPassElement::needsPrecomputeBlur() { return false; }
@@ -0,0 +1,27 @@
#pragma once
#include <hyprland/src/render/pass/PassElement.hpp>
class COmarchyFloatingHyprBar;
class COmarchyFloatingBarPassElement : public IPassElement {
public:
struct SBarData {
COmarchyFloatingHyprBar *deco = nullptr;
float a = 1.F;
};
COmarchyFloatingBarPassElement(const SBarData &data_);
virtual ~COmarchyFloatingBarPassElement() = default;
virtual std::vector<UP<IPassElement>> draw() override;
virtual bool needsLiveBlur() override;
virtual bool needsPrecomputeBlur() override;
virtual std::optional<CBox> boundingBox() override;
virtual const char *passName() override { return "COmarchyFloatingBarPassElement"; }
virtual ePassElementType type() override { return EK_CUSTOM; }
private:
SBarData data;
};
@@ -0,0 +1,34 @@
cmake_minimum_required(VERSION 3.27)
project(omarchy-hyprland-titlebars
DESCRIPTION "Titlebars and edge snapping for Omarchy's floating workspace mode"
VERSION 0.1.0
LANGUAGES CXX
)
set(CMAKE_CXX_STANDARD 23)
set(SRC main.cpp barDeco.cpp BarPassElement.cpp edgeSnap.cpp)
# Built as titlebars.so (no lib prefix). Omarchy loads it from the fixed path
# /usr/lib/omarchy-hyprland-titlebars/titlebars.so. The Hyprland-facing plugin
# name stays "hyprbars" so the hl.plugin.hyprbars Lua namespace is unchanged.
add_library(titlebars SHARED ${SRC})
set_target_properties(titlebars PROPERTIES PREFIX "")
# Hyprland exposes inline globals through its headers, so whole-library hidden
# visibility breaks the host API. Our own types/globals have unique OmarchyFloating names.
target_compile_options(titlebars PRIVATE -fno-gnu-unique)
find_package(PkgConfig REQUIRED)
pkg_check_modules(deps REQUIRED IMPORTED_TARGET
hyprland
libdrm
libinput
libudev
pixman-1
wayland-server
xkbcommon
)
target_link_libraries(titlebars PRIVATE rt PkgConfig::deps)
install(TARGETS titlebars LIBRARY DESTINATION lib/omarchy-hyprland-titlebars)
@@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2023, Hypr Development
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,20 @@
Copyright (c) David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+30 -13
View File
@@ -1,25 +1,42 @@
# Maintainer: Ryan Hughes <ryan@heyoodle.com>
pkgname=omarchy-hyprland-titlebars
pkgver=0.2.1
pkgver=0.1.0
pkgrel=1
pkgdesc="Titlebars for Omarchy's floating workspaces"
pkgdesc="Titlebars and edge snapping for Omarchy's floating workspaces"
arch=('x86_64' 'aarch64')
url='https://github.com/omacom/omarchy-hyprland-titlebars'
url='https://github.com/omacom/omarchy'
license=('BSD-3-Clause' 'MIT')
# A Hyprland plugin only loads into the exact Hyprland build it was compiled
# against. rebuild_on in .omarchy/package.json rebuilds it with every Hyprland,
# and bin/sync-rebuilds moves these pins to the Hyprland it rebuilds against.
depends=('glibc' 'libgcc' 'libstdc++' 'hyprlang' 'hyprutils')
depends_x86_64=('hyprland=0.56.2')
depends_aarch64=('hyprland=0.56.2')
# against. rebuild_on in .omarchy/package.json rebuilds it with every Hyprland.
depends=('hyprland=0.56.2' 'glibc' 'libgcc' 'libstdc++' 'hyprlang' 'hyprutils')
makedepends=('cmake' 'pkgconf')
options=('!lto' '!debug')
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('73320b611eb768a18d61f2ed7ace8f5a94f523a7cbcce132b1db246288efd5b8')
source=('CMakeLists.txt'
'main.cpp'
'barDeco.cpp'
'barDeco.hpp'
'BarPassElement.cpp'
'BarPassElement.hpp'
'edgeSnap.cpp'
'edgeSnap.hpp'
'globals.hpp'
'LICENSE'
'LICENSE.MIT')
sha256sums=('27a292b1b037210774d785a8db17d0e77aceba529a9946b7b555bd6333b9b8e8'
'aa6e0c8cb22dc4d26516fa64aac81d1e493cf7a9b99d7a8bd78b82d61b2f2e89'
'2d9ab463efa263881c23110f9550b10694366e508418bfe06700dc9e18b31579'
'dbdd56902558b36f7e7d01f7b9884530a93f7dda411a2b360acf69255a42c8f2'
'e0074f71d8bbb3ae0a24a91a4647c4d0da6ab8c5effa0dce2b5a1bb8c85d085c'
'dffbf8ddc768fd111b2a41697fe6547e954c46edc16750913c789d8daa8690cc'
'a2b70dd8fc9974a9fca2f4a7db575d14c2c6802c154e4606a3c9e06c1b25a4e6'
'2ebf96f7e7508e4485d706c99c186d6fabf14ba5ceb7349fbc3d72529904424e'
'831147435d58b3071ad26fd756f9755eaf461793d664ea3cb049270b38228993'
'3a1d080d85ce1d722b3bd84d87b74dd2332ba7b68a40a28a7126a86cfca6502a'
'717ba1949502290f8e47688ae2e323acd06c8ca47aec9f7596b15f678c1af4a2')
build() {
cmake -S "$srcdir/$pkgname-$pkgver" -B build \
cmake -S "$srcdir" -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr
cmake --build build
@@ -27,6 +44,6 @@ build() {
package() {
DESTDIR="$pkgdir" cmake --install build
install -Dm644 "$pkgname-$pkgver/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm644 "$pkgname-$pkgver/LICENSE.MIT" "$pkgdir/usr/share/licenses/$pkgname/LICENSE.MIT"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm644 LICENSE.MIT "$pkgdir/usr/share/licenses/$pkgname/LICENSE.MIT"
}
@@ -0,0 +1,91 @@
# omarchy-hyprland-titlebars
A native Hyprland plugin that gives Omarchy's floating workspace mode its window titlebars and edge snapping. It draws theme-neutral titlebars on floating windows and snaps a dragged window to the left or right half of the monitor, or maximizes it at the top edge.
Omarchy core has no build step, so the plugin ships as its own package, built here alongside Hyprland. Omarchy loads it from its Lua config only while a workspace floats, from a fixed path:
```lua
hl.plugin.load("/usr/lib/omarchy-hyprland-titlebars/titlebars.so")
```
That path is a contract with Omarchy core. The plugin still registers itself with Hyprland as `hyprbars` and keeps the `hl.plugin.hyprbars.add_button()` Lua interface and the `plugin.hyprbars.*` options, because Omarchy's config is written against those names.
## Origin and license
This is a fork of Hypr Development's [BSD-licensed hyprbars](https://github.com/hyprwm/hyprland-plugins/tree/main/hyprbars). The source was adapted from the Omarchy Windows 98 theme's `vendor/hyprbars` fork ([theme project](https://github.com/omacom/omarchy-98-theme)), which supplies Hyprland 0.56 Lua API support and safe coexistence with the earlier Windows XP fork. Colors, font, rounding, and actions are supplied by Omarchy's configuration; no theme styling is embedded in the renderer.
The original hyprbars license is in `LICENSE` (BSD-3-Clause). Omarchy's changes are under the MIT license in `LICENSE.MIT`. The package installs both under `/usr/share/licenses/omarchy-hyprland-titlebars/`.
## Added options
These are in addition to the upstream `plugin.hyprbars` titlebar settings.
| Option | Default | Behavior |
| --- | --- | --- |
| `edge_snap` | `false` | Enables edge snapping for moving floating windows. |
| `edge_threshold` | `24` | Edge activation distance in logical pixels. |
| `snap_gap` | `8` | Space around and between half-screen windows, in logical pixels. |
| `bar_color_inactive` | transparent | Inactive titlebar color; transparent falls back to `bar_color`. |
| `workspace_tag` | empty | When set, titlebars and edge snapping apply only to floating windows with this tag. Removing the tag or tiling the window immediately releases titlebar space. |
## Window targeting with `%WINDOW%`
Button `action` commands and `on_double_click` may contain `%WINDOW%`. Every occurrence is replaced synchronously with the clicked titlebar's window address, such as `0x1234abcd`, before the command is spawned. Use it to keep asynchronous actions targeted at the clicked window when focus changes before the command runs:
```lua
hl.plugin.hyprbars.add_button({
bg_color = "rgb(f7768e)", fg_color = "rgb(1a1b26)", size = 18, icon = "×",
action = [[hyprctl dispatch 'hl.dsp.window.close({ window = "address:%WINDOW%" })']],
})
```
The replacement contains only `0x` and hexadecimal digits; no window title or other application-controlled text enters the command.
## Edge snapping
Left/right snapping divides the usable monitor area into equal halves, preserving the configured gap and accounting for the titlebar, window borders, monitor scale, and reserved panels. Windows whose minimum size cannot fit a half stay freely placed. The top edge uses Hyprland's native maximized state so decorations remain reachable and normal fullscreen behavior stays available. Dragging a snapped or maximized titlebar restores its floating size under the pointer.
## `omarchy_snap_preview` event
The plugin emits socket2 events only when the snap preview changes:
```text
omarchy_snap_preview>>monitorName,left,x,y,width,height
omarchy_snap_preview>>monitorName,right,x,y,width,height
omarchy_snap_preview>>monitorName,maximize,x,y,width,height
omarchy_snap_preview>>,none,0,0,0,0
```
Geometry is global and logical, including titlebar and borders. Omarchy's shell renders the preview. Leaving an edge, releasing the pointer, closing the dragged window, reloading configuration, or unloading the plugin clears it.
## Hyprland ABI
A Hyprland plugin is only compatible with the exact Hyprland build whose headers it was compiled against, and Hyprland refuses to load a mismatched build. `rebuild_on` in `.omarchy/package.json` rebuilds this package whenever `hyprland` changes. A new Hyprland version also needs the `hyprland=` dependency in `PKGBUILD` moved to it.
Plugin-owned C++ symbols have an `OmarchyFloating` prefix and the library is built with `-fno-gnu-unique`, so it can be unloaded and reloaded cleanly alongside other hyprbars forks such as the Windows XP theme's. Do not use whole-library hidden visibility: Hyprland shares inline state through its headers.
## Layout
The plugin source sits beside `PKGBUILD` as local sources, since makepkg finds those by file name. `tools/verify.py` is a nested-compositor verifier, with `pointer.c` and the wlr virtual pointer protocol it uses to drive real pointer input; it is not part of the build.
## Building
```bash
bin/repo release --package omarchy-hyprland-titlebars
```
Or locally, with the Hyprland headers from the `hyprland` package installed:
```bash
makepkg -f
```
## Verifying
```bash
python3 tools/verify.py src/build/titlebars.so
```
The verifier needs a running Hyprland session with exactly one Hyprland instance, plus `foot`, `grim`, `wayland-scanner`, and a C compiler. It starts a disposable nested Hyprland with its own runtime directory and never loads the candidate library into the parent compositor. It checks repeated load/reload/unload, interleaved with the Windows XP fork when `/usr/lib/omarchy-windows-xp/hyprbars.so` is installed, then exercises real pointer titlebar dragging, both half-screen snaps, top-edge maximize, drag-down restoration, double-click maximize, and preview events. A two-window regression check deliberately delays button and double-click commands and changes focus before they run, to verify that actions keep the clicked window as their target. A final check confirms that `workspace_tag`-scoped titlebars release their space immediately when a window is tiled or untagged.
Screenshots and logs go to `/tmp/omarchy-hyprland-titlebars-verification` by default; pass `--artifacts DIR` to change it.
@@ -0,0 +1,744 @@
#include "barDeco.hpp"
#include <hyprland/src/Compositor.hpp>
#include <hyprland/src/desktop/state/FocusState.hpp>
#include <hyprland/src/desktop/state/WindowState.hpp>
#include <hyprland/src/desktop/state/LayerState.hpp>
#include <hyprland/src/desktop/state/ViewHitTester.hpp>
#include <hyprland/src/desktop/view/Window.hpp>
#include <hyprland/src/desktop/view/LayerSurface.hpp>
#include <hyprland/src/helpers/MiscFunctions.hpp>
#include <hyprland/src/managers/SeatManager.hpp>
#include <hyprland/src/managers/input/InputManager.hpp>
#include <hyprland/src/render/Renderer.hpp>
#include <hyprland/src/config/ConfigManager.hpp>
#include <hyprland/src/config/shared/animation/AnimationTree.hpp>
#include <hyprland/src/config/shared/parserUtils/ParserUtils.hpp>
#include <hyprland/src/config/supplementary/executor/Executor.hpp>
#include <hyprland/src/config/shared/actions/ConfigActions.hpp>
#include <hyprland/src/animation/AnimationManager.hpp>
#include <hyprland/src/protocols/LayerShell.hpp>
#include <hyprland/src/event/EventBus.hpp>
#include <hyprland/src/layout/LayoutManager.hpp>
#include <hyprland/src/render/OpenGL.hpp>
#include <hyprland/src/state/MonitorState.hpp>
#include "globals.hpp"
#include "BarPassElement.hpp"
#include "edgeSnap.hpp"
#include <hyprland/src/managers/fullscreen/FullscreenController.hpp>
#include <algorithm>
#include <climits>
using namespace Render::GL;
static CHyprColor configColor(Config::INTEGER color) { return CHyprColor{static_cast<uint64_t>(color)}; }
static std::string windowCommand(std::string command, const PHLWINDOW &window) {
// Resolve the decoration owner synchronously. An asynchronously spawned
// command must not choose its target from whatever happens to be focused.
constexpr std::string_view token = "%WINDOW%";
const auto address = std::format("0x{:x}", reinterpret_cast<uintptr_t>(window.get()));
size_t offset = 0;
while ((offset = command.find(token, offset)) != std::string::npos) {
command.replace(offset, token.size(), address);
offset += address.size();
}
return command;
}
COmarchyFloatingHyprBar::COmarchyFloatingHyprBar(PHLWINDOW pWindow) : IHyprWindowDecoration(pWindow) {
m_pWindow = pWindow;
const auto PMONITOR = pWindow->m_monitor.lock();
PMONITOR->m_scheduledRecalc = true;
// button events
m_pMouseButtonCallback =
Event::bus()->m_events.input.mouse.button.listen([&](IPointer::SButtonEvent e, Event::SCallbackInfo &info) { onMouseButton(info, e); });
m_pTouchDownCallback = Event::bus()->m_events.input.touch.down.listen([&](ITouch::SDownEvent e, Event::SCallbackInfo &info) { onTouchDown(info, e); });
m_pTouchUpCallback = Event::bus()->m_events.input.touch.up.listen([&](ITouch::SUpEvent e, Event::SCallbackInfo &info) { onTouchUp(info, e); });
// move events
m_pTouchMoveCallback = Event::bus()->m_events.input.touch.motion.listen([&](ITouch::SMotionEvent e, Event::SCallbackInfo &info) { onTouchMove(info, e); });
m_pMouseMoveCallback = Event::bus()->m_events.input.mouse.move.listen([&](Vector2D c, Event::SCallbackInfo &info) { onMouseMove(c); });
Animation::mgr()->createAnimation(configColor(g_pOmarchyFloatingGlobalState->config.barColor->value()), m_cRealBarColor,
Config::animationTree()->getAnimationPropertyConfig("border"), pWindow, AVARDAMAGE_NONE);
m_cRealBarColor->setUpdateCallback([&](auto) { damageEntire(); });
}
COmarchyFloatingHyprBar::~COmarchyFloatingHyprBar() { std::erase(g_pOmarchyFloatingGlobalState->bars, m_self); }
SDecorationPositioningInfo COmarchyFloatingHyprBar::getPositioningInfo() {
const auto HEIGHT = g_pOmarchyFloatingGlobalState->config.barHeight->value();
const auto ENABLED = g_pOmarchyFloatingGlobalState->config.enabled->value();
const auto PRECEDENCE = g_pOmarchyFloatingGlobalState->config.barPrecedenceOverBorder->value();
SDecorationPositioningInfo info;
info.policy = m_hidden ? DECORATION_POSITION_ABSOLUTE : DECORATION_POSITION_STICKY;
info.edges = DECORATION_EDGE_TOP;
info.priority = PRECEDENCE ? 10005 : 5000;
info.reserved = true;
info.desiredExtents = {{0, m_hidden || !ENABLED ? 0 : HEIGHT}, {0, 0}};
return info;
}
void COmarchyFloatingHyprBar::onPositioningReply(const SDecorationPositioningReply &reply) {
if (reply.assignedGeometry.size() != m_bAssignedBox.size())
m_bWindowSizeChanged = true;
m_bAssignedBox = reply.assignedGeometry;
}
std::string COmarchyFloatingHyprBar::getDisplayName() { return "Hyprbar"; }
bool COmarchyFloatingHyprBar::inputIsValid() {
if (m_hidden || !g_pOmarchyFloatingGlobalState->config.enabled->value())
return false;
if (g_pSeatManager->m_seatGrab && !g_pSeatManager->m_seatGrab->accepts(m_pWindow->wlSurface()->resource()))
return false;
const auto MOUSE = g_pInputManager->getMouseCoordsInternal();
auto PMONITOR = Desktop::focusState()->monitor();
if (!PMONITOR)
return false;
Desktop::CViewHitTester hitTester{*Desktop::viewState()};
const auto WINDOWATCURSOR = hitTester.windowAt(MOUSE, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING);
auto focusState = Desktop::focusState();
auto window = focusState->window();
if (WINDOWATCURSOR != m_pWindow && m_pWindow != window)
return false;
PHLLS foundSurface = nullptr;
Vector2D surfaceCoords;
// Check Top Layer
hitTester.layerSurfaceAt(MOUSE, &PMONITOR->m_layerSurfaceLayers[ZWLR_LAYER_SHELL_V1_LAYER_TOP], &surfaceCoords, &foundSurface);
if (foundSurface)
return false;
// Check Overlay Layer
hitTester.layerSurfaceAt(MOUSE, &PMONITOR->m_layerSurfaceLayers[ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY], &surfaceCoords, &foundSurface);
if (foundSurface)
return false;
return true;
}
void COmarchyFloatingHyprBar::onMouseButton(Event::SCallbackInfo &info, IPointer::SButtonEvent e) {
if (e.button != BTN_LEFT)
return;
if (e.state != WL_POINTER_BUTTON_STATE_PRESSED) {
// A titlebar drag can finish over a panel or another surface.
if (m_bDraggingThis || m_bCancelledDown || m_bDragPending)
handleUpEvent(info);
return;
}
if (!inputIsValid())
return;
handleDownEvent(info, std::nullopt);
}
void COmarchyFloatingHyprBar::onTouchDown(Event::SCallbackInfo &info, ITouch::SDownEvent e) {
// Don't do anything if you're already grabbed a window with another finger
if (!inputIsValid() || e.touchID != 0)
return;
handleDownEvent(info, e);
}
void COmarchyFloatingHyprBar::onTouchUp(Event::SCallbackInfo &info, ITouch::SUpEvent e) {
if (!m_bDragPending || !m_bTouchEv || e.touchID != m_touchId)
return;
handleUpEvent(info);
}
void COmarchyFloatingHyprBar::onMouseMove(Vector2D coords) {
// ensure proper redraws of button icons on hover when using hardware cursors
if (g_pOmarchyFloatingGlobalState->config.iconOnHover->value())
damageOnButtonHover();
if (!m_bDragPending || m_bTouchEv || !validMapped(m_pWindow) || m_touchId != 0)
return;
m_bDragPending = false;
handleMovement();
}
void COmarchyFloatingHyprBar::onTouchMove(Event::SCallbackInfo &info, ITouch::SMotionEvent e) {
if (!m_bDragPending || !m_bTouchEv || !validMapped(m_pWindow) || e.touchID != m_touchId)
return;
auto PMONITOR = m_pWindow->m_monitor.lock();
PMONITOR = PMONITOR ? PMONITOR : Desktop::focusState()->monitor();
const auto COORDS = Vector2D(PMONITOR->m_position.x + e.pos.x * PMONITOR->m_size.x, PMONITOR->m_position.y + e.pos.y * PMONITOR->m_size.y);
if (!m_bDraggingThis) {
// Initial setup for dragging a window.
g_pKeybindManager->m_dispatchers["setfloating"]("activewindow");
g_pKeybindManager->m_dispatchers["resizewindowpixel"]("exact 50% 50%,activewindow");
// pin it so you can change workspaces while dragging a window
g_pKeybindManager->m_dispatchers["pin"]("activewindow");
}
g_pKeybindManager->m_dispatchers["movewindowpixel"](std::format("exact {} {},activewindow", (int)(COORDS.x - (assignedBoxGlobal().w / 2)), (int)COORDS.y));
m_bDraggingThis = true;
}
void COmarchyFloatingHyprBar::handleDownEvent(Event::SCallbackInfo &info, std::optional<ITouch::SDownEvent> touchEvent) {
m_bTouchEv = touchEvent.has_value();
if (m_bTouchEv)
m_touchId = touchEvent.value().touchID;
const auto PWINDOW = m_pWindow.lock();
auto COORDS = cursorRelativeToBar();
if (m_bTouchEv) {
ITouch::SDownEvent e = touchEvent.value();
PHLMONITOR PMONITOR = nullptr;
for (auto &m : State::monitorState()->monitors()) {
if (m->m_name == (!e.device->m_boundOutput.empty() ? e.device->m_boundOutput : "")) {
PMONITOR = m;
break;
}
}
PMONITOR = PMONITOR ? PMONITOR : Desktop::focusState()->monitor();
COORDS = Vector2D(PMONITOR->m_position.x + e.pos.x * PMONITOR->m_size.x, PMONITOR->m_position.y + e.pos.y * PMONITOR->m_size.y) - assignedBoxGlobal().pos();
}
const auto HEIGHT = g_pOmarchyFloatingGlobalState->config.barHeight->value();
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
const auto ALIGNBUTTONS = g_pOmarchyFloatingGlobalState->config.barButtonsAlignment->value();
const auto ON_DOUBLE_CLICK = g_pOmarchyFloatingGlobalState->config.onDoubleClick->value();
const bool BUTTONSRIGHT = ALIGNBUTTONS != "left";
if (!VECINRECT(COORDS, 0, 0, assignedBoxGlobal().w, HEIGHT - 1)) {
if (m_bDraggingThis) {
if (m_bTouchEv)
g_pKeybindManager->m_dispatchers["settiled"]("activewindow");
g_pKeybindManager->m_dispatchers["mouse"]("0movewindow");
Log::logger->log(Log::DEBUG, "[hyprbars] Dragging ended on {:x}", (uintptr_t)PWINDOW.get());
}
m_bDraggingThis = false;
m_bDragPending = false;
m_bTouchEv = false;
return;
}
if (Desktop::focusState()->window() != PWINDOW)
Desktop::focusState()->fullWindowFocus(PWINDOW, Desktop::FOCUS_REASON_CLICK);
if (PWINDOW->m_isFloating)
Desktop::windowState()->raise(PWINDOW);
info.cancelled = true;
m_bCancelledDown = true;
if (doButtonPress(BARPADDING, BARBUTTONPADDING, HEIGHT, COORDS, BUTTONSRIGHT))
return;
if (!ON_DOUBLE_CLICK.empty() &&
std::chrono::duration_cast<std::chrono::milliseconds>(Time::steadyNow() - m_lastMouseDown).count() < 400 /* Arbitrary delay I found suitable */) {
Config::Supplementary::executor()->spawn(windowCommand(ON_DOUBLE_CLICK, PWINDOW));
m_bDragPending = false;
} else {
m_lastMouseDown = Time::steadyNow();
m_bDragPending = true;
}
}
void COmarchyFloatingHyprBar::handleUpEvent(Event::SCallbackInfo &info) {
if (m_pWindow.lock() != Desktop::focusState()->window())
return;
if (m_bCancelledDown)
info.cancelled = true;
m_bCancelledDown = false;
if (m_bDraggingThis) {
g_pKeybindManager->changeMouseBindMode(MBIND_INVALID);
m_bDraggingThis = false;
if (m_bTouchEv)
(void)Config::Actions::floatWindow(Config::Actions::eTogglableAction::TOGGLE_ACTION_DISABLE);
Log::logger->log(Log::DEBUG, "[hyprbars] Dragging ended on {:x}", (uintptr_t)m_pWindow.lock().get());
}
m_bDragPending = false;
m_bTouchEv = false;
m_touchId = 0;
}
void COmarchyFloatingHyprBar::handleMovement() {
const auto window = m_pWindow.lock();
const auto cursor = g_pInputManager->getMouseCoordsInternal();
const auto bar = assignedBoxGlobal();
const auto anchor = std::clamp((cursor.x - bar.x) / std::max(1.0, bar.w), 0.0, 1.0);
const auto offset = std::clamp(cursor.y - bar.y, 0.0, bar.h);
const bool maximized = Fullscreen::controller()->isFullscreen(window, Fullscreen::FSMODE_MAXIMIZED);
if (maximized)
Fullscreen::controller()->setFullscreenMode(window, Fullscreen::FSMODE_NONE);
const bool restored = omarchyFloatingRestoreSnap(window);
if (maximized || restored) {
auto box = window->layoutTarget()->position();
const auto reserved = window->getFullWindowReservedArea();
box.x = cursor.x - box.w * anchor;
box.y = cursor.y + reserved.topLeft.y - offset;
g_layoutManager->setTargetGeom(box, window->layoutTarget());
window->layoutTarget()->warpPositionSize();
}
g_pKeybindManager->changeMouseBindMode(MBIND_MOVE);
m_bDraggingThis = true;
Log::logger->log(Log::DEBUG, "[hyprbars] Dragging initiated on {:x}", (uintptr_t)m_pWindow.lock().get());
return;
}
bool COmarchyFloatingHyprBar::doButtonPress(Config::INTEGER barPadding, Config::INTEGER barButtonPadding, Config::INTEGER barHeight, Vector2D COORDS,
const bool BUTTONSRIGHT) {
// check if on a button
float offset = barPadding;
for (auto &b : g_pOmarchyFloatingGlobalState->buttons) {
const auto BARBUF = Vector2D{(int)assignedBoxGlobal().w, barHeight};
Vector2D currentPos = Vector2D{(BUTTONSRIGHT ? BARBUF.x - barButtonPadding - b.size - offset : offset), (BARBUF.y - b.size) / 2.0}.floor();
if (VECINRECT(COORDS, currentPos.x, currentPos.y, currentPos.x + b.size + barButtonPadding, currentPos.y + b.size)) {
// hit on close
g_pKeybindManager->m_dispatchers["exec"](windowCommand(b.cmd, m_pWindow.lock()));
return true;
}
offset += barButtonPadding + b.size;
}
return false;
}
void COmarchyFloatingHyprBar::renderBarTitle(const Vector2D &bufferSize, const float scale) {
const auto COLORVAL = g_pOmarchyFloatingGlobalState->config.textColor->value();
const auto SIZE = g_pOmarchyFloatingGlobalState->config.barTextSize->value();
const auto WEIGHT = g_pOmarchyFloatingGlobalState->config.barTextWeight->value();
const auto FONT = g_pOmarchyFloatingGlobalState->config.barTextFont->value();
const auto ALIGN = g_pOmarchyFloatingGlobalState->config.barTextAlign->value();
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
float buttonSizes = BARBUTTONPADDING;
for (auto &b : g_pOmarchyFloatingGlobalState->buttons) {
buttonSizes += b.size + BARBUTTONPADDING;
}
const int scaledSize = std::round(SIZE * scale);
const auto scaledButtonsSize = buttonSizes * scale;
const auto scaledBarPadding = BARPADDING * scale;
const int paddingTotal = scaledBarPadding * 2 + scaledButtonsSize + (ALIGN != "left" ? scaledButtonsSize : 0);
const int maxWidth = std::clamp(static_cast<int>(bufferSize.x - paddingTotal), 0, INT_MAX);
if (m_szLastTitle.empty() || maxWidth < 1) {
m_pTextTex = nullptr;
return;
}
const CHyprColor COLOR = m_bForcedTitleColor.value_or(configColor(COLORVAL));
m_pTextTex = g_pHyprRenderer->renderText(m_szLastTitle, COLOR, scaledSize, false, FONT, maxWidth, WEIGHT.m_value);
}
size_t COmarchyFloatingHyprBar::getVisibleButtonCount(Config::INTEGER barButtonPadding, Config::INTEGER barPadding, const Vector2D &bufferSize,
const float scale) {
float availableSpace = bufferSize.x - barPadding * scale * 2;
size_t count = 0;
for (const auto &button : g_pOmarchyFloatingGlobalState->buttons) {
const float buttonSpace = (button.size + barButtonPadding) * scale;
if (availableSpace >= buttonSpace) {
count++;
availableSpace -= buttonSpace;
} else
break;
}
return count;
}
void COmarchyFloatingHyprBar::renderBarButtons(CBox *barBox, const float scale, const float a) {
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
const auto ALIGNBUTTONS = g_pOmarchyFloatingGlobalState->config.barButtonsAlignment->value();
const auto INACTIVECOLOR = g_pOmarchyFloatingGlobalState->config.inactiveButtonColor->value();
const auto BUTTONROUNDING = g_pOmarchyFloatingGlobalState->config.buttonRounding->value();
const auto BUTTONBORDERSIZE = g_pOmarchyFloatingGlobalState->config.buttonBorderSize->value();
const auto BUTTONBORDERCOLOR = g_pOmarchyFloatingGlobalState->config.buttonBorderColor->value();
const bool BUTTONSRIGHT = ALIGNBUTTONS != "left";
const auto visibleCount = getVisibleButtonCount(BARBUTTONPADDING, BARPADDING, Vector2D{barBox->w, barBox->h}, scale);
const bool INVALIDATEICONS = m_bButtonsDirty || m_bWindowSizeChanged;
int offset = BARPADDING * scale;
for (size_t i = 0; i < visibleCount; ++i) {
auto &button = g_pOmarchyFloatingGlobalState->buttons[i];
const auto scaledButtonSize = button.size * scale;
const auto scaledButtonsPad = BARBUTTONPADDING * scale;
auto color = button.bgcol;
if (INACTIVECOLOR > 0) {
color = m_bWindowHasFocus ? color : configColor(INACTIVECOLOR);
if (INVALIDATEICONS && button.userfg && button.iconTex)
button.iconTex = nullptr;
}
color.a *= a;
CBox buttonBox = {barBox->x + (BUTTONSRIGHT ? barBox->w - offset - scaledButtonSize : offset), barBox->y + (barBox->h - scaledButtonSize) / 2.0,
scaledButtonSize, scaledButtonSize};
buttonBox.round();
const int maxRounding = static_cast<int>(std::round(scaledButtonSize / 2.0));
const int rounding = BUTTONROUNDING < 0 ? maxRounding : std::clamp(static_cast<int>(std::round(BUTTONROUNDING * scale)), 0, maxRounding);
const int borderSize = std::clamp(static_cast<int>(std::round(BUTTONBORDERSIZE * scale)), 0, maxRounding);
if (borderSize > 0) {
auto borderColor = configColor(BUTTONBORDERCOLOR);
borderColor.a *= a;
g_pHyprOpenGL->renderRect(buttonBox, borderColor, {.round = rounding});
auto face = buttonBox.copy().expand(-borderSize);
g_pHyprOpenGL->renderRect(face, color, {.round = std::max(0, rounding - borderSize)});
} else
g_pHyprOpenGL->renderRect(buttonBox, color, {.round = rounding});
offset += scaledButtonsPad + scaledButtonSize;
}
}
void COmarchyFloatingHyprBar::renderBarButtonsText(CBox *barBox, const float scale, const float a) {
const auto HEIGHT = g_pOmarchyFloatingGlobalState->config.barHeight->value();
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
const auto ALIGNBUTTONS = g_pOmarchyFloatingGlobalState->config.barButtonsAlignment->value();
const auto ICONONHOVER = g_pOmarchyFloatingGlobalState->config.iconOnHover->value();
const bool BUTTONSRIGHT = ALIGNBUTTONS != "left";
const auto visibleCount = getVisibleButtonCount(BARBUTTONPADDING, BARPADDING, Vector2D{barBox->w, barBox->h}, scale);
const auto COORDS = cursorRelativeToBar();
int offset = BARPADDING * scale;
float noScaleOffset = BARPADDING;
for (size_t i = 0; i < visibleCount; ++i) {
auto &button = g_pOmarchyFloatingGlobalState->buttons[i];
const auto scaledButtonSize = button.size * scale;
const auto scaledButtonsPad = BARBUTTONPADDING * scale;
// check if hovering here
const auto BARBUF = Vector2D{(int)assignedBoxGlobal().w, HEIGHT};
Vector2D currentPos =
Vector2D{(BUTTONSRIGHT ? BARBUF.x - BARBUTTONPADDING - button.size - noScaleOffset : noScaleOffset), (BARBUF.y - button.size) / 2.0}.floor();
bool hovering = VECINRECT(COORDS, currentPos.x, currentPos.y, currentPos.x + button.size + BARBUTTONPADDING, currentPos.y + button.size);
noScaleOffset += BARBUTTONPADDING + button.size;
if ((!button.iconTex || button.iconTex->m_texID == 0) && !button.icon.empty()) {
// render icon
auto fgcol = button.userfg ? button.fgcol : (button.bgcol.r + button.bgcol.g + button.bgcol.b < 1) ? CHyprColor(0xFFFFFFFF) : CHyprColor(0xFF000000);
button.iconTex = g_pHyprRenderer->renderText(button.icon, fgcol, std::round(button.size * 0.62 * scale), false, "sans", scaledButtonSize);
}
if (!button.iconTex || button.iconTex->m_texID == 0)
continue;
const auto iconX =
barBox->x + (BUTTONSRIGHT ? barBox->width - offset - scaledButtonSize / 2.0 : offset + scaledButtonSize / 2.0) - button.iconTex->m_size.x / 2.0;
const auto iconY = barBox->y + barBox->height / 2.0 - button.iconTex->m_size.y / 2.0;
CBox pos = {iconX, iconY, button.iconTex->m_size.x, button.iconTex->m_size.y};
if (!ICONONHOVER || (ICONONHOVER && m_iButtonHoverState > 0))
g_pHyprOpenGL->renderTexture(button.iconTex, pos, {.a = a});
offset += scaledButtonsPad + scaledButtonSize;
bool currentBit = (m_iButtonHoverState & (1 << i)) != 0;
if (hovering != currentBit) {
m_iButtonHoverState ^= (1 << i);
// damage to get rid of some artifacts when icons are "hidden"
damageEntire();
}
}
}
void COmarchyFloatingHyprBar::draw(PHLMONITOR pMonitor, const float &a) {
const auto ENABLED = g_pOmarchyFloatingGlobalState->config.enabled->value();
if (m_bLastEnabledState != ENABLED) {
m_bLastEnabledState = ENABLED;
g_pDecorationPositioner->repositionDeco(this);
}
if (m_hidden || !validMapped(m_pWindow) || !ENABLED)
return;
const auto PWINDOW = m_pWindow.lock();
if (!PWINDOW->m_ruleApplicator->decorate().valueOrDefault())
return;
auto data = COmarchyFloatingBarPassElement::SBarData{this, a};
g_pHyprRenderer->m_renderPass.add(makeUnique<COmarchyFloatingBarPassElement>(data));
}
void COmarchyFloatingHyprBar::renderPass(PHLMONITOR pMonitor, const float &a) {
const auto PWINDOW = m_pWindow.lock();
static auto PENABLEBLURGLOBAL = CConfigValue<Config::BOOL>("decoration:blur:enabled");
const auto BARCOLOR = g_pOmarchyFloatingGlobalState->config.barColor->value();
const auto HEIGHT = g_pOmarchyFloatingGlobalState->config.barHeight->value();
const auto PRECEDENCE = g_pOmarchyFloatingGlobalState->config.barPrecedenceOverBorder->value();
const auto ALIGNBUTTONS = g_pOmarchyFloatingGlobalState->config.barButtonsAlignment->value();
const auto ENABLETITLE = g_pOmarchyFloatingGlobalState->config.barTitleEnabled->value();
const auto ENABLEBLUR = g_pOmarchyFloatingGlobalState->config.barBlur->value();
const auto INACTIVECOLOR = g_pOmarchyFloatingGlobalState->config.inactiveButtonColor->value();
if (INACTIVECOLOR > 0) {
bool currentWindowFocus = PWINDOW == Desktop::focusState()->window();
if (currentWindowFocus != m_bWindowHasFocus) {
m_bWindowHasFocus = currentWindowFocus;
m_bButtonsDirty = true;
}
}
const auto INACTIVEBAR = g_pOmarchyFloatingGlobalState->config.inactiveBarColor->value();
const auto baseColor = PWINDOW != Desktop::focusState()->window() && INACTIVEBAR != 0 ? INACTIVEBAR : BARCOLOR;
const CHyprColor DEST_COLOR = m_bForcedBarColor.value_or(configColor(baseColor));
if (DEST_COLOR != m_cRealBarColor->goal())
*m_cRealBarColor = DEST_COLOR;
CHyprColor color = m_cRealBarColor->value();
color.a *= a;
const bool BUTTONSRIGHT = ALIGNBUTTONS != "left";
const bool SHOULDBLUR = ENABLEBLUR && *PENABLEBLURGLOBAL && color.a < 1.F;
if (HEIGHT < 1) {
m_iLastHeight = HEIGHT;
return;
}
const auto PWORKSPACE = PWINDOW->m_workspace;
const auto WORKSPACEOFFSET = PWORKSPACE && !PWINDOW->m_pinned ? PWORKSPACE->m_renderOffset->value() : Vector2D();
const auto ROUNDING = PWINDOW->rounding() + (PRECEDENCE ? 0 : PWINDOW->getRealBorderSize());
const auto scaledRounding = ROUNDING > 0 ? ROUNDING * pMonitor->m_scale - 2 /* idk why but otherwise it looks bad due to the gaps */ : 0;
m_seExtents = {{0, HEIGHT}, {}};
const auto DECOBOX = assignedBoxGlobal();
const auto BARBUF = DECOBOX.size() * pMonitor->m_scale;
CBox titleBarBox = {DECOBOX.x - pMonitor->m_position.x, DECOBOX.y - pMonitor->m_position.y, DECOBOX.w,
DECOBOX.h + ROUNDING * 3 /* to fill the bottom cuz we can't disable rounding there */};
titleBarBox.translate(PWINDOW->m_floatingOffset).scale(pMonitor->m_scale).round();
if (titleBarBox.w < 1 || titleBarBox.h < 1)
return;
g_pHyprOpenGL->scissor(titleBarBox);
if (ROUNDING) {
// the +1 is a shit garbage temp fix until renderRect supports an alpha matte
CBox windowBox = {PWINDOW->position(Desktop::View::IGeometric::GEOMETRIC_CURRENT).x + PWINDOW->m_floatingOffset.x - pMonitor->m_position.x + 1,
PWINDOW->position(Desktop::View::IGeometric::GEOMETRIC_CURRENT).y + PWINDOW->m_floatingOffset.y - pMonitor->m_position.y + 1,
PWINDOW->size(Desktop::View::IGeometric::GEOMETRIC_CURRENT).x - 2, PWINDOW->size(Desktop::View::IGeometric::GEOMETRIC_CURRENT).y - 2};
if (windowBox.w < 1 || windowBox.h < 1)
return;
glClearStencil(0);
glClear(GL_STENCIL_BUFFER_BIT);
g_pHyprOpenGL->setCapStatus(GL_STENCIL_TEST, true);
glStencilFunc(GL_ALWAYS, 1, -1);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
windowBox.translate(WORKSPACEOFFSET).scale(pMonitor->m_scale).round();
g_pHyprOpenGL->renderRect(windowBox, CHyprColor(0, 0, 0, 0), {.round = scaledRounding, .roundingPower = m_pWindow->roundingPower()});
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glStencilFunc(GL_NOTEQUAL, 1, -1);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
}
g_pHyprOpenGL->renderRect(titleBarBox, color, {.round = static_cast<int>(scaledRounding), .roundingPower = PWINDOW->roundingPower()});
// render title
if (ENABLETITLE && (m_szLastTitle != PWINDOW->m_title || m_bWindowSizeChanged || !m_pTextTex || m_pTextTex->m_texID == 0 || m_bTitleColorChanged)) {
m_szLastTitle = PWINDOW->m_title;
renderBarTitle(BARBUF, pMonitor->m_scale);
}
if (ROUNDING) {
// cleanup stencil
glClearStencil(0);
glClear(GL_STENCIL_BUFFER_BIT);
g_pHyprOpenGL->setCapStatus(GL_STENCIL_TEST, false);
glStencilMask(-1);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
}
CBox textBox = {titleBarBox.x, titleBarBox.y, (int)BARBUF.x, (int)BARBUF.y};
if (ENABLETITLE && m_pTextTex) {
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
const auto ALIGN = g_pOmarchyFloatingGlobalState->config.barTextAlign->value();
float buttonSizes = BARBUTTONPADDING;
for (auto &b : g_pOmarchyFloatingGlobalState->buttons) {
buttonSizes += b.size + BARBUTTONPADDING;
}
const auto scaledBorderSize = PWINDOW->getRealBorderSize() * pMonitor->m_scale;
const auto scaledButtonsSize = buttonSizes * pMonitor->m_scale;
const auto scaledBarPadding = BARPADDING * pMonitor->m_scale;
const auto xOffset = ALIGN == "left" ? std::round(scaledBarPadding + (BUTTONSRIGHT ? 0 : scaledButtonsSize))
: std::round(((BARBUF.x - scaledBorderSize) / 2.0 - m_pTextTex->m_size.x / 2.0));
const auto yOffset = std::round((BARBUF.y - m_pTextTex->m_size.y) / 2.0);
CBox titleBox = {textBox.x + xOffset, textBox.y + yOffset, m_pTextTex->m_size.x, m_pTextTex->m_size.y};
g_pHyprOpenGL->renderTexture(m_pTextTex, titleBox, {.a = a});
}
renderBarButtons(&textBox, pMonitor->m_scale, a);
m_bButtonsDirty = false;
g_pHyprOpenGL->scissor(nullptr);
renderBarButtonsText(&textBox, pMonitor->m_scale, a);
m_bWindowSizeChanged = false;
m_bTitleColorChanged = false;
// dynamic updates change the extents
if (m_iLastHeight != HEIGHT) {
PWINDOW->layoutTarget()->recalc();
m_iLastHeight = HEIGHT;
}
}
eDecorationType COmarchyFloatingHyprBar::getDecorationType() { return DECORATION_CUSTOM; }
void COmarchyFloatingHyprBar::updateWindow(PHLWINDOW pWindow) {
updateRules();
damageEntire();
}
void COmarchyFloatingHyprBar::onConfigReloaded() {
m_bButtonsDirty = true;
m_bTitleColorChanged = true;
m_pTextTex = nullptr;
g_pDecorationPositioner->repositionDeco(this);
damageEntire();
}
void COmarchyFloatingHyprBar::damageEntire() { g_pHyprRenderer->damageBox(assignedBoxGlobal()); }
Vector2D COmarchyFloatingHyprBar::cursorRelativeToBar() { return g_pInputManager->getMouseCoordsInternal() - assignedBoxGlobal().pos(); }
eDecorationLayer COmarchyFloatingHyprBar::getDecorationLayer() { return DECORATION_LAYER_UNDER; }
uint64_t COmarchyFloatingHyprBar::getDecorationFlags() {
return DECORATION_ALLOWS_MOUSE_INPUT | (g_pOmarchyFloatingGlobalState->config.barPartOfWindow->value() ? DECORATION_PART_OF_MAIN_WINDOW : 0);
}
CBox COmarchyFloatingHyprBar::assignedBoxGlobal() {
if (!validMapped(m_pWindow))
return {};
CBox box = m_bAssignedBox;
box.translate(g_pDecorationPositioner->getEdgeDefinedPoint(DECORATION_EDGE_TOP, m_pWindow.lock()));
const auto PWORKSPACE = m_pWindow->m_workspace;
const auto WORKSPACEOFFSET = PWORKSPACE && !m_pWindow->m_pinned ? PWORKSPACE->m_renderOffset->value() : Vector2D();
return box.translate(WORKSPACEOFFSET);
}
PHLWINDOW COmarchyFloatingHyprBar::getOwner() { return m_pWindow.lock(); }
void COmarchyFloatingHyprBar::updateRules() {
const auto PWINDOW = m_pWindow.lock();
if (!PWINDOW)
return;
auto prevHidden = m_hidden;
auto prevForcedTitleColor = m_bForcedTitleColor;
m_bForcedBarColor = std::nullopt;
m_bForcedTitleColor = std::nullopt;
m_hidden = false;
if (PWINDOW->m_ruleApplicator->m_otherProps.props.contains(g_pOmarchyFloatingGlobalState->nobarRuleIdx))
m_hidden = truthy(PWINDOW->m_ruleApplicator->m_otherProps.props.at(g_pOmarchyFloatingGlobalState->nobarRuleIdx)->effect);
const auto workspaceTag = g_pOmarchyFloatingGlobalState->config.workspaceTag->value();
if (!workspaceTag.empty())
m_hidden = m_hidden || !PWINDOW->m_isFloating || !PWINDOW->m_ruleApplicator->m_tagKeeper.isTagged(workspaceTag);
if (PWINDOW->m_ruleApplicator->m_otherProps.props.contains(g_pOmarchyFloatingGlobalState->barColorRuleIdx))
m_bForcedBarColor = CHyprColor(
Config::ParserUtils::parseColor(PWINDOW->m_ruleApplicator->m_otherProps.props.at(g_pOmarchyFloatingGlobalState->barColorRuleIdx)->effect).value_or(0));
if (PWINDOW->m_ruleApplicator->m_otherProps.props.contains(g_pOmarchyFloatingGlobalState->titleColorRuleIdx))
m_bForcedTitleColor =
CHyprColor(Config::ParserUtils::parseColor(PWINDOW->m_ruleApplicator->m_otherProps.props.at(g_pOmarchyFloatingGlobalState->titleColorRuleIdx)->effect)
.value_or(0));
if (prevHidden != m_hidden) {
g_pDecorationPositioner->repositionDeco(this);
if (const auto monitor = PWINDOW->m_monitor.lock())
monitor->m_scheduledRecalc = true;
}
if (prevForcedTitleColor != m_bForcedTitleColor)
m_bTitleColorChanged = true;
}
void COmarchyFloatingHyprBar::damageOnButtonHover() {
const auto BARPADDING = g_pOmarchyFloatingGlobalState->config.barPadding->value();
const auto BARBUTTONPADDING = g_pOmarchyFloatingGlobalState->config.barButtonPadding->value();
const auto HEIGHT = g_pOmarchyFloatingGlobalState->config.barHeight->value();
const auto ALIGNBUTTONS = g_pOmarchyFloatingGlobalState->config.barButtonsAlignment->value();
const bool BUTTONSRIGHT = ALIGNBUTTONS != "left";
float offset = BARPADDING;
const auto COORDS = cursorRelativeToBar();
for (auto &b : g_pOmarchyFloatingGlobalState->buttons) {
const auto BARBUF = Vector2D{(int)assignedBoxGlobal().w, HEIGHT};
Vector2D currentPos = Vector2D{(BUTTONSRIGHT ? BARBUF.x - BARBUTTONPADDING - b.size - offset : offset), (BARBUF.y - b.size) / 2.0}.floor();
bool hover = VECINRECT(COORDS, currentPos.x, currentPos.y, currentPos.x + b.size + BARBUTTONPADDING, currentPos.y + b.size);
if (hover != m_bButtonHovered) {
m_bButtonHovered = hover;
damageEntire();
}
offset += BARBUTTONPADDING + b.size;
}
}
@@ -0,0 +1,124 @@
#pragma once
#define WLR_USE_UNSTABLE
#include <hyprland/src/render/decorations/IHyprWindowDecoration.hpp>
#include <hyprland/src/render/OpenGL.hpp>
#include <hyprland/src/render/gl/GLTexture.hpp>
#include <hyprland/src/devices/IPointer.hpp>
#include <hyprland/src/devices/ITouch.hpp>
#include <hyprland/src/desktop/rule/windowRule/WindowRule.hpp>
#include <hyprland/src/helpers/AnimatedVariable.hpp>
#include <hyprland/src/helpers/time/Time.hpp>
#include <hyprland/src/helpers/signal/Signal.hpp>
#include "globals.hpp"
#define private public
#include <hyprland/src/managers/input/InputManager.hpp>
#undef private
namespace Event {
struct SCallbackInfo;
}
class COmarchyFloatingHyprBar : public IHyprWindowDecoration {
public:
COmarchyFloatingHyprBar(PHLWINDOW);
virtual ~COmarchyFloatingHyprBar();
virtual SDecorationPositioningInfo getPositioningInfo();
virtual void onPositioningReply(const SDecorationPositioningReply &reply);
virtual void draw(PHLMONITOR, float const &a);
virtual eDecorationType getDecorationType();
virtual void updateWindow(PHLWINDOW);
virtual void damageEntire();
virtual eDecorationLayer getDecorationLayer();
virtual uint64_t getDecorationFlags();
bool m_bButtonsDirty = true;
virtual std::string getDisplayName();
PHLWINDOW getOwner();
void updateRules();
void onConfigReloaded();
WP<COmarchyFloatingHyprBar> m_self;
private:
SBoxExtents m_seExtents;
PHLWINDOWREF m_pWindow;
CBox m_bAssignedBox;
SP<Render::ITexture> m_pTextTex;
bool m_bWindowSizeChanged = false;
bool m_hidden = false;
bool m_bTitleColorChanged = false;
bool m_bButtonHovered = false;
bool m_bLastEnabledState = false;
bool m_bWindowHasFocus = false;
std::optional<CHyprColor> m_bForcedBarColor;
std::optional<CHyprColor> m_bForcedTitleColor;
Time::steady_tp m_lastMouseDown{};
PHLANIMVAR<CHyprColor> m_cRealBarColor;
Vector2D cursorRelativeToBar();
void renderPass(PHLMONITOR, float const &a);
void renderBarTitle(const Vector2D &bufferSize, const float scale);
void renderBarButtons(CBox *barBox, const float scale, const float a);
void renderBarButtonsText(CBox *barBox, const float scale, const float a);
void damageOnButtonHover();
bool inputIsValid();
void onMouseButton(Event::SCallbackInfo &info, IPointer::SButtonEvent e);
void onTouchDown(Event::SCallbackInfo &info, ITouch::SDownEvent e);
void onTouchUp(Event::SCallbackInfo &info, ITouch::SUpEvent e);
void onMouseMove(Vector2D coords);
void onTouchMove(Event::SCallbackInfo &info, ITouch::SMotionEvent e);
void handleDownEvent(Event::SCallbackInfo &info, std::optional<ITouch::SDownEvent> touchEvent);
void handleUpEvent(Event::SCallbackInfo &info);
void handleMovement();
bool doButtonPress(Config::INTEGER barPadding, Config::INTEGER barButtonPadding, Config::INTEGER barHeight, Vector2D COORDS, bool BUTTONSRIGHT);
CBox assignedBoxGlobal();
CHyprSignalListener m_pMouseButtonCallback;
CHyprSignalListener m_pTouchDownCallback;
CHyprSignalListener m_pTouchUpCallback;
CHyprSignalListener m_pTouchMoveCallback;
CHyprSignalListener m_pMouseMoveCallback;
std::string m_szLastTitle;
bool m_bDraggingThis = false;
bool m_bTouchEv = false;
bool m_bDragPending = false;
bool m_bCancelledDown = false;
int m_touchId = 0;
// store hover state for buttons as a bitfield
unsigned int m_iButtonHoverState = 0;
// for dynamic updates
int m_iLastHeight = 0;
size_t getVisibleButtonCount(Config::INTEGER barButtonPadding, Config::INTEGER barPadding, const Vector2D &bufferSize, const float scale);
friend class COmarchyFloatingBarPassElement;
};
@@ -0,0 +1,160 @@
#include "edgeSnap.hpp"
#include "globals.hpp"
#include <hyprland/src/desktop/view/Window.hpp>
#include <hyprland/src/event/EventBus.hpp>
#include <hyprland/src/layout/LayoutManager.hpp>
#include <hyprland/src/managers/EventManager.hpp>
#include <hyprland/src/managers/KeybindManager.hpp>
#include <hyprland/src/managers/fullscreen/FullscreenController.hpp>
#include <hyprland/src/output/Monitor.hpp>
#include <hyprland/src/state/MonitorState.hpp>
#include <algorithm>
#include <unordered_map>
namespace {
PHLWINDOWREF dragged;
CBox originalBox;
CBox snapBox;
std::string zone;
std::string preview;
std::unordered_map<PHLWINDOWREF, CBox> restoreBoxes;
std::vector<CHyprSignalListener> listeners;
void hidePreview() {
if (!preview.empty())
g_pEventManager->postEvent({"omarchy_snap_preview", ",none,0,0,0,0"});
preview.clear();
zone.clear();
}
bool enabled() { return g_pOmarchyFloatingGlobalState->config.enabled->value() && g_pOmarchyFloatingGlobalState->config.edgeSnap->value(); }
void updatePreview(const Vector2D &cursor) {
const auto &drag = g_layoutManager->dragController();
const auto target = drag->target();
if (!enabled() || drag->mode() != MBIND_MOVE || !target || !target->floating() || !validMapped(target->window())) {
omarchyFloatingSnapReset();
return;
}
const auto window = target->window();
const auto workspaceTag = g_pOmarchyFloatingGlobalState->config.workspaceTag->value();
if (!workspaceTag.empty() && !window->m_ruleApplicator->m_tagKeeper.isTagged(workspaceTag)) {
omarchyFloatingSnapReset();
return;
}
if (dragged != window) {
dragged = window;
originalBox = target->position();
}
const auto monitor = State::monitorState()->query().vec(cursor).run();
if (!monitor) {
hidePreview();
return;
}
const auto work = monitor->logicalBoxMinusReserved();
const auto threshold = std::clamp<int>(g_pOmarchyFloatingGlobalState->config.edgeThreshold->value(), 1, 100);
const auto gap = std::clamp<int>(g_pOmarchyFloatingGlobalState->config.snapGap->value(), 0, 100);
std::string nextZone;
if (cursor.y <= work.y + threshold)
nextZone = "maximize";
else if (cursor.x <= monitor->m_position.x + threshold)
nextZone = "left";
else if (cursor.x >= monitor->m_position.x + monitor->m_size.x - threshold)
nextZone = "right";
if (nextZone.empty()) {
hidePreview();
return;
}
auto box = nextZone == "maximize" ? work : work.copy().expand(-gap);
if (nextZone != "maximize") {
box.w = std::floor((work.w - 3 * gap) / 2.0);
if (nextZone == "right")
box.x = work.x + work.w - gap - box.w;
}
const auto reserved = window->getFullWindowReservedArea();
const auto min = target->minSize().value_or(Vector2D{1, 1});
// Applications with a larger minimum cannot occupy a half without overlap.
// Keep their free placement instead of previewing geometry we cannot honor.
if (nextZone != "maximize" && (box.w - reserved.topLeft.x - reserved.bottomRight.x < min.x || box.h - reserved.topLeft.y - reserved.bottomRight.y < min.y)) {
hidePreview();
return;
}
zone = nextZone;
snapBox = box;
const auto next = std::format("{},{},{:.0f},{:.0f},{:.0f},{:.0f}", monitor->m_name, zone, box.x, box.y, box.w, box.h);
if (next != preview) {
preview = next;
g_pEventManager->postEvent({"omarchy_snap_preview", preview});
}
}
void release(IPointer::SButtonEvent event) {
if (event.button != BTN_LEFT || event.state != WL_POINTER_BUTTON_STATE_RELEASED)
return;
const auto window = dragged.lock();
const auto target = g_layoutManager->dragController()->target();
const auto nextZone = zone;
const auto box = snapBox;
const auto before = originalBox;
const bool commit =
enabled() && validMapped(window) && target && target->window() == window && g_layoutManager->dragController()->mode() == MBIND_MOVE && !nextZone.empty();
omarchyFloatingSnapReset();
if (!commit)
return;
// End the native drag before changing geometry, otherwise its final motion
// overwrites the snap. The titlebar's release handler is idempotent.
g_pKeybindManager->changeMouseBindMode(MBIND_INVALID);
if (nextZone == "maximize") {
restoreBoxes.erase(window);
Fullscreen::controller()->setFullscreenMode(window, Fullscreen::FSMODE_MAXIMIZED);
} else {
restoreBoxes.try_emplace(window, before);
Fullscreen::controller()->setFullscreenMode(window, Fullscreen::FSMODE_NONE);
const auto reserved = window->getFullWindowReservedArea();
const CBox clientBox{box.pos() + reserved.topLeft, box.size() - reserved.topLeft - reserved.bottomRight};
g_layoutManager->setTargetGeom(clientBox, window->layoutTarget());
}
}
} // namespace
void omarchyFloatingSnapReset() {
hidePreview();
dragged.reset();
}
bool omarchyFloatingRestoreSnap(PHLWINDOW window) {
const auto found = restoreBoxes.find(window);
if (found == restoreBoxes.end())
return false;
const auto box = found->second;
restoreBoxes.erase(found);
g_layoutManager->setTargetGeom(box, window->layoutTarget());
return true;
}
void omarchyFloatingSnapInit() {
listeners.emplace_back(Event::bus()->m_events.input.mouse.move.listen([](Vector2D cursor, Event::SCallbackInfo &) { updatePreview(cursor); }));
listeners.emplace_back(Event::bus()->m_events.input.mouse.button.listen([](IPointer::SButtonEvent event, Event::SCallbackInfo &) { release(event); }));
listeners.emplace_back(Event::bus()->m_events.window.close.listen([](PHLWINDOW window) {
restoreBoxes.erase(window);
if (dragged == window)
omarchyFloatingSnapReset();
}));
}
void omarchyFloatingSnapExit() {
omarchyFloatingSnapReset();
listeners.clear();
restoreBoxes.clear();
}
Loaded 100 of 186 files, more files were not shown because too many files have changed in this diff. Show more