Merge current package defaults into clipboard fix
This commit is contained in:
commit
d9748c6a15
737 files changed
+216530
-1514
No files matched your search
@@ -0,0 +1,27 @@
|
||||
# Trust list for PR builds.
|
||||
#
|
||||
# A pull request only builds packages (and spins up builder droplets) when
|
||||
# its author is trusted: repository collaborators are trusted automatically
|
||||
# and do not need listing; external contributors listed here are trusted
|
||||
# too. Anyone else gets the plan only, until a maintainer either adds them
|
||||
# here or applies the "build-approved" label to that one PR. The label also
|
||||
# releases GitHub's approval hold for that PR's build and test workflows.
|
||||
# It remains effective while attached, without vouching for the author's
|
||||
# other PRs. An explicit denouncement cannot be overridden by the label.
|
||||
#
|
||||
# Syntax:
|
||||
# github:username
|
||||
# -github:username reason for denouncement
|
||||
#
|
||||
# Keep entries sorted alphabetically.
|
||||
github:bjarneo
|
||||
github:DanWahlin
|
||||
github:dhh
|
||||
github:f-trycua
|
||||
github:HANCORE-linux
|
||||
github:kwilczynski
|
||||
github:ryanrhughes
|
||||
github:scottjones
|
||||
github:spencerbull
|
||||
github:tcballard
|
||||
github:tobi
|
||||
@@ -0,0 +1,78 @@
|
||||
const BUILD = '.github/workflows/build-pr.yml';
|
||||
const TESTS = '.github/workflows/test.yml';
|
||||
|
||||
module.exports = async function approve({ github, context, core, vouchStatus,
|
||||
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), attempts = 36 }) {
|
||||
// Missing/failed vouch lookups must not become approval. Denouncements
|
||||
// remain absolute, just as they are in the package build gate.
|
||||
if (!['unknown', 'bot', 'collaborator', 'vouched'].includes(vouchStatus)) {
|
||||
throw new Error(`Cannot approve workflows: vouch status is ${vouchStatus || 'missing'}.`);
|
||||
}
|
||||
|
||||
const expected = context.payload.pull_request;
|
||||
const eventTime = Date.parse(expected.updated_at);
|
||||
if (!Number.isFinite(eventTime)) throw new Error('Missing PR event timestamp.');
|
||||
const approved = new Set();
|
||||
let precedingBuild;
|
||||
|
||||
const stillApproved = async () => {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
...context.repo, pull_number: expected.number,
|
||||
});
|
||||
return pr.state === 'open' && pr.head.sha === expected.head.sha &&
|
||||
pr.labels.some(label => label.name === 'build-approved');
|
||||
};
|
||||
|
||||
// The label and PR-run events arrive independently. Wait for the build
|
||||
// belonging to this event, rather than returning after approving an older
|
||||
// run and leaving the new label-triggered run stuck behind GitHub's gate.
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
if (attempt) await sleep(5000);
|
||||
if (!await stillApproved()) {
|
||||
core.info('PR closed, head changed, or build-approved removed; stopping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const all = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, {
|
||||
...context.repo, event: 'pull_request', head_sha: expected.head.sha, per_page: 100,
|
||||
});
|
||||
const runs = all.filter(run =>
|
||||
run.event === 'pull_request' && run.head_sha === expected.head.sha &&
|
||||
run.head_repository?.id === expected.head.repo.id && run.head_branch === expected.head.ref &&
|
||||
[BUILD, TESTS].includes(run.path) &&
|
||||
// Fork runs awaiting approval often have no pull_requests entries.
|
||||
(!run.pull_requests?.length || run.pull_requests.some(pr => pr.number === expected.number))
|
||||
).sort((a, b) => a.id - b.id);
|
||||
|
||||
const newestBuild = runs.findLast(run => run.path === BUILD);
|
||||
if (!newestBuild || !(Date.parse(newestBuild.created_at) >= eventTime) ||
|
||||
!runs.some(run => run.path === TESTS &&
|
||||
(context.payload.action === 'labeled' || Date.parse(run.created_at) >= eventTime))) continue;
|
||||
|
||||
if (precedingBuild) {
|
||||
const { data: run } = await github.rest.actions.getWorkflowRun({
|
||||
...context.repo, run_id: precedingBuild,
|
||||
});
|
||||
// Approve older builds first, and let them acquire concurrency before
|
||||
// releasing a newer build. Otherwise an older queued run could start
|
||||
// last and cancel the label-triggered build that carries approval.
|
||||
if (!['in_progress', 'completed'].includes(run.status) || run.conclusion === 'action_required') continue;
|
||||
precedingBuild = undefined;
|
||||
}
|
||||
|
||||
const pending = runs.filter(run => run.conclusion === 'action_required' && !approved.has(run.id) &&
|
||||
// If the newest build already runs (e.g. a maintainer approved it),
|
||||
// don't resurrect an obsolete hold that could cancel that newer run.
|
||||
(run.path !== BUILD || run.id === newestBuild.id || newestBuild.conclusion === 'action_required'));
|
||||
if (!pending.length) return;
|
||||
const run = pending[0];
|
||||
// Recheck after the API reads, immediately before exercising write access.
|
||||
if (!await stillApproved()) return;
|
||||
await github.rest.actions.approveWorkflowRun({ ...context.repo, run_id: run.id });
|
||||
approved.add(run.id);
|
||||
core.info(`Approved ${run.path} run ${run.id} for PR #${expected.number}.`);
|
||||
if (run.path === BUILD) precedingBuild = run.id;
|
||||
if (pending.length === 1) return;
|
||||
}
|
||||
throw new Error('Timed out waiting for PR workflows. Remove and reapply build-approved to retry.');
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Approve PR workflows
|
||||
|
||||
# A pull_request workflow cannot approve itself: GitHub can hold it before
|
||||
# any job starts. This workflow only runs trusted default-branch code and
|
||||
# releases the ordinary, unprivileged PR workflows after build approval.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: write
|
||||
|
||||
concurrency:
|
||||
group: approve-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
approve:
|
||||
# Match build-pr.yml's events, including other labels applied while this
|
||||
# PR still carries build-approved: each labeled event creates a build.
|
||||
if: contains(github.event.pull_request.labels.*.name, 'build-approved')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
# Never check out the PR head or its merge ref with this write token.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
persist-credentials: false
|
||||
- id: vouch
|
||||
uses: mitchellh/vouch/action/check-user@f23dbb5e745334f97414ec70463ce7301071a661 # v1
|
||||
with:
|
||||
user: ${{ github.event.pull_request.user.login }}
|
||||
allow-fail: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Approve this PR's pending build and test runs
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
VOUCH_STATUS: ${{ steps.vouch.outputs.status }}
|
||||
with:
|
||||
script: |
|
||||
const approve = require('./.github/scripts/approve-pr-workflows.cjs');
|
||||
await approve({ github, context, core, vouchStatus: process.env.VOUCH_STATUS });
|
||||
@@ -0,0 +1,254 @@
|
||||
name: Build changed packages
|
||||
|
||||
# Build every package directory a PR touches, one job per package per arch, on
|
||||
# the self-hosted droplet pool. Artifacts are unsigned; publish.yml signs and
|
||||
# publishes them on merge.
|
||||
#
|
||||
# Tooling runs from the base branch; a PR supplies only pkgbuilds/. The
|
||||
# vouch gate limits who may spend compute; this limits what their PR can run.
|
||||
|
||||
# No paths filter: approved PRs must report the required `result` even when
|
||||
# no package directory changed. Those PRs get an empty matrix and a passing
|
||||
# result in seconds; unapproved PRs wait for maintainer approval.
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
packages:
|
||||
description: "Space-separated package directories to build"
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Builds cost real machines, so they run only for trusted authors:
|
||||
# collaborators, anyone in .github/VOUCHED.td (read from the default
|
||||
# branch, so a PR cannot vouch for itself), or a PR a maintainer has
|
||||
# labelled "build-approved". Everyone else gets this job's plan output
|
||||
# while the required `result` stays pending until a maintainer approves.
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.list.outputs.matrix }}
|
||||
count: ${{ steps.gate.outputs.count }}
|
||||
trusted: ${{ steps.gate.outputs.trusted }}
|
||||
vouch_status: ${{ steps.vouch.outputs.status }}
|
||||
empty: ${{ steps.list.outputs.empty }}
|
||||
steps:
|
||||
# Same rule as the build job: bin/build-matrix comes from the base
|
||||
# branch tip, the package directories from the PR head.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref || github.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.head.sha }}"
|
||||
git checkout "${{ github.event.pull_request.head.sha }}" -- pkgbuilds/
|
||||
# Bootstrap: the PR that introduces this tooling has a base without
|
||||
# it. Take the plan helper from the PR head in that one case; it
|
||||
# runs on a hosted runner and only prints a plan.
|
||||
if [[ ! -x bin/build-matrix ]]; then
|
||||
git checkout "${{ github.event.pull_request.head.sha }}" -- bin/build-matrix helpers/
|
||||
echo "::notice::base branch has no bin/build-matrix; using the PR's copy for planning"
|
||||
fi
|
||||
- id: vouch
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: mitchellh/vouch/action/check-user@f23dbb5e745334f97414ec70463ce7301071a661 # v1
|
||||
with:
|
||||
user: ${{ github.event.pull_request.user.login }}
|
||||
allow-fail: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- id: approval
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
...context.repo, pull_number: context.payload.pull_request.number,
|
||||
});
|
||||
// Approving or rerunning a held run keeps its original event,
|
||||
// which may predate the label. Read the current approval instead.
|
||||
core.setOutput('approved', pr.state === 'open' &&
|
||||
pr.head.sha === context.payload.pull_request.head.sha &&
|
||||
pr.labels.some(label => label.name === 'build-approved'));
|
||||
# One matrix entry per package per architecture. Every package builds
|
||||
# once, against edge; the channels it ships to on merge are carried
|
||||
# along for information. A filename means one set of bytes.
|
||||
- id: list
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [[ -n "${{ github.event.inputs.packages }}" ]]; then
|
||||
names="${{ github.event.inputs.packages }}"
|
||||
else
|
||||
names=$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" -- pkgbuilds \
|
||||
| awk -F/ '$1=="pkgbuilds" && NF>2 {print $2}' | sort -u)
|
||||
fi
|
||||
matrix=$(printf '%s\n' $names | bin/build-matrix)
|
||||
# A package directory whose exact tree already has a build artifact
|
||||
# (label <pkg>-<arch>-<tree hash>, uploaded only after a successful
|
||||
# build) is not built again. Pushing a fix for one package to a PR
|
||||
# that touches fifty rebuilds one, not fifty; publish.yml finds the
|
||||
# same artifacts on merge. workflow_dispatch is an explicit request
|
||||
# and always builds.
|
||||
if [[ "${{ github.event_name }}" == pull_request ]]; then
|
||||
head="${{ github.event.pull_request.head.sha }}"
|
||||
kept=(); reused=()
|
||||
while read -r entry; do
|
||||
package=$(jq -r .package <<<"$entry"); arch=$(jq -r .arch <<<"$entry")
|
||||
label="$package-$arch-$(git rev-parse "$head:pkgbuilds/$package")"
|
||||
found=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts?name=$label&per_page=5" \
|
||||
| jq -r '[.artifacts[] | select(.expired|not)] | length' || echo 0)
|
||||
if (( found > 0 )); then reused+=("$label"); else kept+=("$entry"); fi
|
||||
done < <(jq -c '.include[]' <<<"$matrix")
|
||||
matrix=$(printf '%s\n' "${kept[@]}" | jq -sc '{include: .}')
|
||||
if (( ${#reused[@]} )); then
|
||||
printf '==> already built, reusing the artifact: %s\n' "${reused[@]}"
|
||||
{ echo "Reused existing build artifacts (${#reused[@]}):"; printf -- '- %s\n' "${reused[@]}"; } >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
fi
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
echo "planned=$(jq '.include | length' <<<"$matrix")" >> "$GITHUB_OUTPUT"
|
||||
jq -r '.include[] | "\(.package) \(.arch) -> \(.channels)"' <<<"$matrix"
|
||||
# A PR whose diff against its base is empty changes nothing: its
|
||||
# content already landed some other way (a sync PR beat it, or a
|
||||
# merge from master swallowed it). Merging it would record a change
|
||||
# that isn't one. Flag it so `result` fails rather than passes.
|
||||
if [[ "${{ github.event_name }}" == pull_request ]]; then
|
||||
total=$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" | wc -l)
|
||||
echo "empty=$([[ $total -eq 0 ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
||||
echo "files changed vs base: $total"
|
||||
else
|
||||
echo "empty=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- id: gate
|
||||
env:
|
||||
STATUS: ${{ github.event_name == 'workflow_dispatch' && 'dispatch' || steps.vouch.outputs.status }}
|
||||
AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
APPROVED: ${{ steps.approval.outputs.approved || 'false' }}
|
||||
PLANNED: ${{ steps.list.outputs.planned }}
|
||||
run: |
|
||||
case "$STATUS" in
|
||||
bot|collaborator|vouched|dispatch) trusted=true ;;
|
||||
# A denouncement is absolute: the label cannot override it.
|
||||
denounced) trusted=false ;;
|
||||
unknown) trusted=$APPROVED ;;
|
||||
*) trusted=false ;;
|
||||
esac
|
||||
echo "trusted=$trusted" >> "$GITHUB_OUTPUT"
|
||||
if [[ $trusted == true ]]; then
|
||||
echo "count=$PLANNED" >> "$GITHUB_OUTPUT"
|
||||
echo "Author $AUTHOR is trusted ($STATUS); building $PLANNED package(s)."
|
||||
else
|
||||
echo "count=0" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::Author $AUTHOR is not trusted ($STATUS). $PLANNED package build(s) planned but not run."
|
||||
if [[ $STATUS == denounced ]]; then
|
||||
echo "::warning::The author is denounced in .github/VOUCHED.td; the build-approved label does not apply."
|
||||
else
|
||||
echo "::warning::A maintainer can add the author to .github/VOUCHED.td, or apply the 'build-approved' label to build this PR."
|
||||
fi
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.count != '0'
|
||||
runs-on: [self-hosted, omarchy-builder]
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.changes.outputs.matrix) }}
|
||||
steps:
|
||||
# Tooling from base: everything that executes on this droplet's host
|
||||
# (bin/, helpers/, build/) comes from the base branch. Only the PR's
|
||||
# package directories are overlaid. A PR can therefore change what
|
||||
# gets built, never how the runner builds it. A PR that changes both
|
||||
# tooling and a package builds the package with the OLD tooling; land
|
||||
# the tooling first. workflow_dispatch has no PR and runs as checked out.
|
||||
# The base branch tip, not the event's base.sha: that sha is a snapshot
|
||||
# taken at the PR's last push, so a tooling fix on master would never
|
||||
# reach an open PR until someone pushed to it (seen on the daily sync
|
||||
# PR after the artifact packing fix landed).
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref || github.sha }}
|
||||
persist-credentials: false
|
||||
- name: Overlay the PR's package directories onto base tooling
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.head.sha }}"
|
||||
git checkout "${{ github.event.pull_request.head.sha }}" -- pkgbuilds/
|
||||
echo "tooling: $(git rev-parse --short HEAD) (base) packages: ${{ github.event.pull_request.head.sha }} (PR head)"
|
||||
git status --short | head
|
||||
- name: Build ${{ matrix.package }} (${{ matrix.arch }}, ships to ${{ matrix.channels }})
|
||||
id: build
|
||||
env:
|
||||
CONTAINER_ENGINE: docker
|
||||
run: bin/build --mirror edge --arch ${{ matrix.arch }} --package ${{ matrix.package }}
|
||||
# The artifact label carries the package directory's git tree hash so
|
||||
# the publish step can find the build for exactly the tree that merged.
|
||||
# The package file inside keeps makepkg's standard name untouched.
|
||||
# The artifact label uses the PR head's tree for this package: that is
|
||||
# the tree that merges, and what publish looks up.
|
||||
- name: Tree hash
|
||||
id: tree
|
||||
run: echo "hash=$(git rev-parse "${{ github.event.pull_request.head.sha || github.sha }}:pkgbuilds/${{ matrix.package }}")" >> "$GITHUB_OUTPUT"
|
||||
# The upload action rejects a path containing ':', which is how makepkg
|
||||
# names a package with an epoch. The files ride inside packages.tar
|
||||
# (helpers/artifact-helpers.sh); publish.yml unpacks it. Only a
|
||||
# successful build uploads: the artifact's existence is what lets the
|
||||
# planner above and publish.yml skip rebuilding this exact tree.
|
||||
- name: Pack artifact
|
||||
id: pack
|
||||
run: |
|
||||
source helpers/artifact-helpers.sh
|
||||
pack_packages build-output/edge/${{ matrix.arch }} packages.tar
|
||||
tar -tvf packages.tar
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.package }}-${{ matrix.arch }}-${{ steps.tree.outputs.hash }}
|
||||
path: packages.tar
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
# `result` is required by branch protection. An unvouched author awaiting
|
||||
# approval gets a differently named informational check, leaving `result`
|
||||
# unreported (pending). Skipping or passing a job named `result` would count
|
||||
# as satisfying the requirement even though no build was authorized.
|
||||
# Actual planning/build failures and denouncements still report `result`.
|
||||
result:
|
||||
name: ${{ needs.changes.result == 'success' && needs.changes.outputs.trusted == 'false' && needs.changes.outputs.vouch_status == 'unknown' && needs.changes.outputs.empty == 'false' && 'Awaiting build approval' || 'result' }}
|
||||
needs: [changes, build]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "trusted=${{ needs.changes.outputs.trusted }} build=${{ needs.build.result }}"
|
||||
if [[ "${{ needs.changes.result }}" != "success" ]]; then
|
||||
echo "::error::Build planning or the trust check failed. See the changes job."
|
||||
exit 1
|
||||
fi
|
||||
# Nothing to merge: the PR's diff against its base is empty. Its
|
||||
# change already landed elsewhere. Close it rather than merge it.
|
||||
if [[ "${{ needs.changes.outputs.empty }}" == "true" ]]; then
|
||||
echo "::error::This PR changes no files relative to its base. Its content is already on the target branch; close it instead of merging."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${{ needs.changes.outputs.trusted }}" == "false" && "${{ needs.changes.outputs.vouch_status }}" == "unknown" && "${{ needs.changes.outputs.empty }}" == "false" ]]; then
|
||||
echo "::notice::Awaiting maintainer build approval. Apply 'build-approved' to this PR or vouch for the author in .github/VOUCHED.td."
|
||||
echo "Package builds are waiting for maintainer approval. Apply **build-approved** to this PR to start them. The required **result** check remains pending." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${{ needs.changes.outputs.trusted }}" != "true" ]]; then
|
||||
echo "::error::Builds are blocked: the author is denounced or the trust result is invalid. The build-approved label cannot override this."
|
||||
exit 1
|
||||
fi
|
||||
[[ "${{ needs.build.result }}" == "success" || "${{ needs.build.result }}" == "skipped" ]]
|
||||
@@ -0,0 +1,118 @@
|
||||
name: Refresh builder images
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '23 4 * * *'
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- build/**
|
||||
- bin/builder-image
|
||||
- helpers/paths.sh
|
||||
- helpers/docker-helpers.sh
|
||||
- tests/build-isolation.sh
|
||||
- .github/workflows/builder-images.yml
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- build/**
|
||||
- bin/builder-image
|
||||
- helpers/paths.sh
|
||||
- helpers/docker-helpers.sh
|
||||
- tests/build-isolation.sh
|
||||
- .github/workflows/builder-images.yml
|
||||
|
||||
# Complete each refresh before another can replace its tested image tags.
|
||||
concurrency:
|
||||
group: builder-images-${{ github.event.pull_request.number || 'master' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Exercise proposed image changes on native runners with a read-only token.
|
||||
# Publishing is a separate master-only job with its own write permission.
|
||||
validate:
|
||||
if: github.event_name == 'pull_request'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CONTAINER_ENGINE: docker
|
||||
CANDIDATE_IMAGE: omarchy-builder-candidate:${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build a fresh environment
|
||||
run: bin/builder-image build --arch "${{ matrix.arch }}" --mirror edge --tag "$CANDIDATE_IMAGE" --fresh
|
||||
- name: Test isolated package builds
|
||||
env:
|
||||
TEST_BUILDER_IMAGE: ${{ env.CANDIDATE_IMAGE }}
|
||||
run: tests/build-isolation.sh
|
||||
|
||||
refresh:
|
||||
if: github.repository == 'omacom/omarchy-pkgs' && github.ref == 'refs/heads/master'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
env:
|
||||
CONTAINER_ENGINE: docker
|
||||
REGISTRY_IMAGE: ghcr.io/omacom/omarchy-pkg-builder
|
||||
CANDIDATE_IMAGE: omarchy-builder-candidate:${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build a fresh environment
|
||||
run: bin/builder-image build --arch "${{ matrix.arch }}" --mirror edge --tag "$CANDIDATE_IMAGE" --fresh
|
||||
- name: Test isolated package builds
|
||||
env:
|
||||
TEST_BUILDER_IMAGE: ${{ env.CANDIDATE_IMAGE }}
|
||||
run: tests/build-isolation.sh
|
||||
- name: Publish tested image
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_ACTOR: ${{ github.actor }}
|
||||
DOCKER_CONFIG: ${{ runner.temp }}/builder-registry-auth
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$DOCKER_CONFIG"
|
||||
trap 'rm -rf "$DOCKER_CONFIG"' EXIT
|
||||
printf '%s' "$GH_TOKEN" | docker login ghcr.io --username "$GH_ACTOR" --password-stdin
|
||||
key=$(bin/builder-image key --arch "${{ matrix.arch }}" --mirror edge)
|
||||
version="$REGISTRY_IMAGE:$key-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
||||
docker tag "$CANDIDATE_IMAGE" "$version"
|
||||
docker push "$version"
|
||||
# GHCR creates new packages private. Do not advertise an image to
|
||||
# fork PRs until it is public. This is a one-time package setting.
|
||||
anonymous_config=$(mktemp -d "$RUNNER_TEMP/builder-anonymous.XXXXXX")
|
||||
if ! DOCKER_CONFIG="$anonymous_config" docker manifest inspect "$version" >/dev/null; then
|
||||
rm -rf "$anonymous_config"
|
||||
echo "::error::Make the omacom/omarchy-pkg-builder GHCR package public, then rerun this job. The previous matching image remains selected."
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$anonymous_config"
|
||||
docker tag "$CANDIDATE_IMAGE" "$REGISTRY_IMAGE:$key"
|
||||
docker push "$REGISTRY_IMAGE:$key"
|
||||
digest=$(docker image inspect "$version" --format '{{index .RepoDigests 0}}')
|
||||
printf '### Builder image (%s)\n\nInput key: `%s`\n\nImage: `%s`\n' \
|
||||
"${{ matrix.arch }}" "$key" "$digest" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,326 @@
|
||||
name: Publish merged packages
|
||||
|
||||
# On every push to master: for each package directory the push touched and
|
||||
# each architecture it supports, find the PR build artifact for exactly that
|
||||
# tree (label = <pkg>-<arch>-<treehash>), or build it now when there is
|
||||
# none, then publish that one artifact into every channel the package ships
|
||||
# to. One build, one file, several databases: a filename means one set of
|
||||
# bytes everywhere, and channels are views over a shared pool.
|
||||
#
|
||||
# Secrets live in the "publish" environment, restricted to master:
|
||||
# GPG_PRIVATE_KEY, GPG_PASSPHRASE the channel signing key
|
||||
# R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT
|
||||
# OMARCHY_PUBLISH_PREFIX (environment variable, not secret) points a proof
|
||||
# run at a scratch prefix inside the live bucket; empty means the real
|
||||
# channel paths.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths: ["pkgbuilds/**"]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
packages:
|
||||
description: "Space-separated package directories to publish from master"
|
||||
required: true
|
||||
|
||||
# Merges serialize. Two publishes into one channel at once would race on
|
||||
# the database; queued is fine, cancelled is not.
|
||||
concurrency:
|
||||
group: publish
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.list.outputs.matrix }}
|
||||
count: ${{ steps.list.outputs.count }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- id: list
|
||||
run: |
|
||||
if [[ -n "${{ github.event.inputs.packages }}" ]]; then
|
||||
names="${{ github.event.inputs.packages }}"
|
||||
else
|
||||
names=$(git diff --name-only "${{ github.event.before }}..${{ github.sha }}" -- pkgbuilds \
|
||||
| awk -F/ '$1=="pkgbuilds" && NF>2 {print $2}' | sort -u)
|
||||
fi
|
||||
matrix=$(printf '%s\n' $names | bin/build-matrix)
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
echo "count=$(jq '.include | length' <<<"$matrix")" >> "$GITHUB_OUTPUT"
|
||||
jq -r '.include[] | "\(.package) \(.arch) -> \(.channels)"' <<<"$matrix"
|
||||
|
||||
# One job for the whole merge. It collects every PR artifact for the
|
||||
# merged tree (building only what has none), then walks each channel and
|
||||
# architecture slot exactly once: pull that database, add every package
|
||||
# that belongs in it, upload. Six slots, six round trips, however many
|
||||
# packages the merge carried. One process is the only writer, so there
|
||||
# is no race between packages; the run-level concurrency group above
|
||||
# keeps one merge from overlapping the next.
|
||||
publish:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.count != '0'
|
||||
runs-on: [self-hosted, omarchy-builder]
|
||||
environment: publish
|
||||
timeout-minutes: 240
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Every matrix entry, as a file the shell steps can loop over:
|
||||
# package arch channels publish_arches
|
||||
- name: Plan
|
||||
run: |
|
||||
jq -r '.include[] | "\(.package) \(.arch) \(.channels|gsub(" ";",")) \(.publish_arches|gsub(" ";","))"' \
|
||||
<<'EOF_MATRIX' > plan.txt
|
||||
${{ needs.changes.outputs.matrix }}
|
||||
EOF_MATRIX
|
||||
cat plan.txt
|
||||
|
||||
# Fetch each package's PR artifact into build-output/edge/<arch>/, or
|
||||
# build it when no artifact exists for exactly this tree. An artifact
|
||||
# carries its package files inside packages.tar (see build-pr.yml and
|
||||
# helpers/artifact-helpers.sh: the upload action rejects the colon in
|
||||
# an epoch filename).
|
||||
- name: Collect artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CONTAINER_ENGINE: docker
|
||||
run: |
|
||||
set -uo pipefail
|
||||
source helpers/artifact-helpers.sh
|
||||
# sources.jsonl: where each package's files came from, or that the
|
||||
# build failed. A failed build ends the run before any publish, and
|
||||
# the record says so instead of the report job finding nothing.
|
||||
: > sources.jsonl
|
||||
failed=0
|
||||
while read -r package arch channels publish_arches; do
|
||||
hash=$(git rev-parse "HEAD:pkgbuilds/$package")
|
||||
label="$package-$arch-$hash"
|
||||
found=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts?name=$label&per_page=5" \
|
||||
| jq -r '[.artifacts[] | select(.expired|not)] | sort_by(.created_at) | last | .archive_download_url // empty')
|
||||
mkdir -p "build-output/edge/$arch"
|
||||
if [[ -n "$found" ]]; then
|
||||
echo "==> $label: PR artifact"
|
||||
rm -rf /tmp/artifact; mkdir -p /tmp/artifact
|
||||
if curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -o /tmp/artifact.zip "$found" \
|
||||
&& unzip -oq /tmp/artifact.zip -d /tmp/artifact \
|
||||
&& unpack_packages /tmp/artifact "build-output/edge/$arch"; then
|
||||
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"pr-artifact"}' >> sources.jsonl
|
||||
else
|
||||
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"artifact-download-failed"}' >> sources.jsonl; failed=1; break
|
||||
fi
|
||||
else
|
||||
# bin/build plans against the public channel first. If the
|
||||
# channel already holds master's version there is nothing to
|
||||
# build and nothing to publish: a re-run for a package that
|
||||
# turned out to be fine. Record it and move on.
|
||||
plan=$(CONTAINER_ENGINE=docker bin/build --dry-run --mirror edge --arch "$arch" --package "$package" 2>&1 | grep -E '^==> Plan complete' || true)
|
||||
# "Packages that would build:" followed by nothing means none.
|
||||
if [[ -n "$plan" && -z "$(sed -E 's/.*would build: *//' <<<"$plan" | tr -d '[:space:]')" ]]; then
|
||||
echo "==> $label: already published at master's version, nothing to do"
|
||||
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"already-published"}' >> sources.jsonl
|
||||
continue
|
||||
fi
|
||||
echo "==> $label: no artifact for this tree, building"
|
||||
if OMARCHY_KEEP_BUILD_WORKSPACE=1 bin/build --mirror edge --arch "$arch" --package "$package"; then
|
||||
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"built"}' >> sources.jsonl
|
||||
else
|
||||
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"build-failed"}' >> sources.jsonl; failed=1; break
|
||||
fi
|
||||
fi
|
||||
done < plan.txt
|
||||
ls -1 build-output/edge/*/*.pkg.tar.zst 2>/dev/null || true
|
||||
if (( failed )); then
|
||||
# Write the record now; the publish step will not run.
|
||||
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||||
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
|
||||
--slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
|
||||
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:[]}' \
|
||||
> publish-record.json
|
||||
cat publish-record.json
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
RCLONE_CONFIG_R2_TYPE: s3
|
||||
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
|
||||
# The token is scoped to the bucket; it may not CreateBucket, and
|
||||
# rclone's existence check is a CreateBucket in disguise.
|
||||
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true"
|
||||
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
OMARCHY_PUBLISH_PREFIX: ${{ vars.OMARCHY_PUBLISH_PREFIX }}
|
||||
# repo-add, gpg and bsdtar are Arch tools; run the publish inside the
|
||||
# builder image (host-native, edge) with the workspace mounted.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! compgen -G "build-output/edge/*/*.pkg.tar.zst" >/dev/null; then
|
||||
echo "Nothing to publish: every requested package is already published at master's version."
|
||||
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||||
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
|
||||
--slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
|
||||
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:[]}' \
|
||||
> publish-record.json
|
||||
cat publish-record.json
|
||||
exit 0
|
||||
fi
|
||||
docker image inspect omarchy-pkg-builder:latest-x86_64-edge >/dev/null 2>&1 \
|
||||
|| docker buildx build --load -t omarchy-pkg-builder:latest-x86_64-edge --build-arg MIRROR=edge build
|
||||
|
||||
# Group the merge's files by the (channel, architecture) slot each
|
||||
# belongs to. A package's files live under build-output/edge/<built
|
||||
# arch>/ and are named <pkgname>-<ver>-<arch|any>.pkg.tar.zst; a
|
||||
# split package's outputs share the pkgbase's directory, so match
|
||||
# on the artifact list rather than the name.
|
||||
# pkgbase is read inside the builder image: the Ubuntu host has no
|
||||
# bsdtar. One container call maps every file to its pkgbase.
|
||||
docker run --rm -v "$PWD:/w:ro" -w /w omarchy-pkg-builder:latest-x86_64-edge bash -c '
|
||||
for f in build-output/edge/*/*.pkg.tar.zst; do
|
||||
printf "%s %s\n" "$f" "$(bsdtar -xOf "$f" .PKGINFO | awk -F" = " "\$1==\"pkgbase\"{print \$2}")"
|
||||
done' > pkgbase.txt
|
||||
declare -A slot_files=()
|
||||
while read -r package arch channels publish_arches; do
|
||||
for f in build-output/edge/"$arch"/*.pkg.tar.zst; do
|
||||
# Only files this package produced (its PKGINFO pkgbase).
|
||||
[[ $(awk -v f="$f" '$1==f{print $2}' pkgbase.txt) == "$package" ]] || continue
|
||||
for mirror in ${channels//,/ }; do
|
||||
for parch in ${publish_arches//,/ }; do
|
||||
slot_files["$mirror/$parch"]+="$f "
|
||||
done
|
||||
done
|
||||
done
|
||||
done < plan.txt
|
||||
|
||||
# Deterministic slot order: edge before rc before stable, x86_64
|
||||
# before aarch64, so a failure leaves the earlier rings consistent.
|
||||
# Every slot's outcome goes into publish-record.json for the report
|
||||
# job: what was published, where, from which artifact, and whether
|
||||
# the slot succeeded. A failing slot stops the loop (set -e) but the
|
||||
# record still shows everything before it landed.
|
||||
: > slots.jsonl
|
||||
record_slot() { jq -nc --arg m "$1" --arg a "$2" --arg st "$3" --arg files "$4" \
|
||||
'{mirror:$m, arch:$a, status:$st, packages:[$files | split(" ") | .[] | select(length>0) | sub(".*/";"") | sub("\\.pkg\\.tar\\.zst$";"")]}' >> slots.jsonl; }
|
||||
status=0
|
||||
for mirror in edge rc stable; do
|
||||
for parch in x86_64 aarch64; do
|
||||
files=${slot_files["$mirror/$parch"]:-}
|
||||
[[ -n "$files" ]] || continue
|
||||
echo "==> $mirror/$parch: $files"
|
||||
if docker run --rm \
|
||||
-e GPG_PRIVATE_KEY -e GPG_PASSPHRASE -e OMARCHY_PUBLISH_PREFIX \
|
||||
-e RCLONE_CONFIG_R2_TYPE -e RCLONE_CONFIG_R2_PROVIDER -e RCLONE_CONFIG_R2_ENDPOINT -e RCLONE_CONFIG_R2_NO_CHECK_BUCKET \
|
||||
-e RCLONE_CONFIG_R2_ACCESS_KEY_ID -e RCLONE_CONFIG_R2_SECRET_ACCESS_KEY \
|
||||
-v "$PWD:/w:ro" -w /w \
|
||||
omarchy-pkg-builder:latest-x86_64-edge \
|
||||
bin/publish-artifact --remote R2:omarchy-pkgs --mirror "$mirror" --arch "$parch" $files; then
|
||||
record_slot "$mirror" "$parch" published "$files"
|
||||
else
|
||||
record_slot "$mirror" "$parch" failed "$files"
|
||||
status=1
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||||
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
|
||||
--slurpfile slots slots.jsonl --slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
|
||||
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:$slots}' \
|
||||
> publish-record.json
|
||||
cat publish-record.json
|
||||
exit $status
|
||||
|
||||
- name: Keep the publish record
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: publish-record-${{ github.run_id }}
|
||||
path: publish-record.json
|
||||
retention-days: 90
|
||||
|
||||
# Tell people what happened. A comment on the merged PR (found by the
|
||||
# merge commit, so squash and rebase merges work too) and a line appended
|
||||
# to a running JSON log in the bucket, next to the packages it describes,
|
||||
# so the history is public and can be rendered later.
|
||||
report:
|
||||
needs: [changes, publish]
|
||||
if: always() && needs.publish.result != 'skipped'
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: publish-record-${{ github.run_id }}
|
||||
- name: Render
|
||||
id: render
|
||||
run: |
|
||||
jq -r --arg outcome "${{ needs.publish.result }}" '
|
||||
def pkgs: [.packages[] | select(test("-debug-")|not)] | join(", ");
|
||||
def src: if .source=="pr-artifact" then "PR artifact" elif .source=="built" then "built here" elif .source=="already-published" then "already published, skipped" else "**" + .source + "**" end;
|
||||
"### Publish " + (if $outcome=="success" then "succeeded" else "FAILED" end) +
|
||||
" → **" + .target + "**" + (if .target!="live" then " (proof prefix, not live)" else "" end),
|
||||
"",
|
||||
"Packages: " + ([.sources[] | .package + " (" + .arch + ", " + src + ")"] | join("; ")),
|
||||
"",
|
||||
(if (.slots|length) > 0 then (.slots[] | "- `" + .mirror + "/" + .arch + "`: " + (if .status=="published" then "" else "**" + .status + "** " end) + pkgs)
|
||||
elif ([.sources[] | select(.source=="already-published")] | length) == (.sources|length) then "_Nothing to publish: already at master'"'"'s version everywhere._"
|
||||
else "_Nothing was published._" end),
|
||||
"",
|
||||
(if ([.sources[] | select(.source|test("failed"))] | length) > 0 then "_A build failed, so no channel was touched._\n"
|
||||
elif (.plan|length) > (.slots|length) then "_Some planned slots did not run because an earlier slot failed._\n" else "" end),
|
||||
"Commit " + .commit[0:7] + " · [run](" + .run + ")"
|
||||
' publish-record.json > comment.md
|
||||
cat comment.md
|
||||
- name: Append to the publish log in the bucket
|
||||
env:
|
||||
RCLONE_CONFIG_R2_TYPE: s3
|
||||
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
|
||||
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true"
|
||||
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
run: |
|
||||
curl -fsSL https://downloads.rclone.org/rclone-current-linux-amd64.zip -o rclone.zip && unzip -jq rclone.zip '*/rclone' && chmod +x rclone
|
||||
# One JSON object per line, newest last. Served at
|
||||
# https://pkgs.omarchy.org/publish-log.jsonl
|
||||
./rclone copy R2:omarchy-pkgs/publish-log.jsonl . --s3-no-head 2>/dev/null || : > publish-log.jsonl
|
||||
jq -c . publish-record.json >> publish-log.jsonl
|
||||
./rclone copyto publish-log.jsonl R2:omarchy-pkgs/publish-log.jsonl --s3-no-head
|
||||
echo "log now has $(wc -l < publish-log.jsonl) entries"
|
||||
|
||||
- name: Comment on the merged PR
|
||||
# Only for a push: the merge commit names its PR. A dispatch runs
|
||||
# from master's head, whose PR merged something else entirely, so
|
||||
# commenting there would attach this run's report to the wrong PR.
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
pr=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" --jq '.[0].number // empty')
|
||||
if [[ -n "$pr" ]]; then
|
||||
gh pr comment "$pr" -R "${{ github.repository }}" --body-file comment.md
|
||||
echo "commented on #$pr"
|
||||
else
|
||||
echo "no PR for ${{ github.sha }} (manual dispatch?); skipping PR comment"
|
||||
fi
|
||||
result:
|
||||
needs: [changes, publish]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "publish result: ${{ needs.publish.result }}"
|
||||
[[ "${{ needs.publish.result }}" == "success" || "${{ needs.publish.result }}" == "skipped" ]]
|
||||
@@ -1,91 +0,0 @@
|
||||
name: Sync AUR Packages
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every 6 hours
|
||||
- cron: '0 */6 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
packages:
|
||||
description: 'Specific packages to sync (space-separated, leave empty for all)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Sync AUR packages
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e PACKAGES="$PACKAGES" \
|
||||
-e HOST_UID="$(id -u)" \
|
||||
-e HOST_GID="$(id -g)" \
|
||||
-v "$PWD/bin:/workspace/bin:ro" \
|
||||
-v "$PWD/helpers:/workspace/helpers:ro" \
|
||||
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
|
||||
-w /workspace \
|
||||
archlinux:base-devel bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
pacman -Syu --noconfirm git jq
|
||||
|
||||
groupadd -g "$HOST_GID" runner
|
||||
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
|
||||
chown -R runner:runner /workspace/pkgbuilds
|
||||
|
||||
if [[ -n "${PACKAGES:-}" ]]; then
|
||||
read -r -a package_args <<< "$PACKAGES"
|
||||
runuser -u runner -- ./bin/sync-aur "${package_args[@]}"
|
||||
else
|
||||
runuser -u runner -- ./bin/sync-aur
|
||||
fi
|
||||
'
|
||||
env:
|
||||
PACKAGES: ${{ github.event.inputs.packages }}
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: sync AUR packages'
|
||||
title: 'chore: sync AUR packages'
|
||||
body: |
|
||||
Automated AUR package sync.
|
||||
|
||||
Package sync behavior is controlled by `.omarchy/package.json`.
|
||||
branch: auto/sync-aur
|
||||
delete-branch: true
|
||||
labels: automated
|
||||
reviewers: ryanrhughes
|
||||
|
||||
- name: Notify Basecamp on failure
|
||||
if: failure() && env.BASECAMP_CHATBOT_URL != ''
|
||||
env:
|
||||
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
|
||||
run: |
|
||||
curl -s -o /dev/null \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg content \
|
||||
"🔴 <strong>AUR sync failed</strong><br><a href=\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\">View run</a>" \
|
||||
'{content: $content}')" \
|
||||
"$BASECAMP_CHATBOT_URL"
|
||||
@@ -27,9 +27,11 @@ jobs:
|
||||
# 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
|
||||
id: sync
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e PACKAGES="$PACKAGES" \
|
||||
-e UPSTREAM_GITHUB_TOKEN="$UPSTREAM_GITHUB_TOKEN" \
|
||||
-e HOST_UID="$(id -u)" \
|
||||
-e HOST_GID="$(id -g)" \
|
||||
-v "$PWD/bin:/workspace/bin:ro" \
|
||||
@@ -39,7 +41,7 @@ jobs:
|
||||
archlinux:base-devel bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
pacman -Syu --noconfirm git jq
|
||||
pacman -Syu --noconfirm git jq python libarchive
|
||||
|
||||
groupadd -g "$HOST_GID" runner
|
||||
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
|
||||
@@ -54,8 +56,12 @@ jobs:
|
||||
'
|
||||
env:
|
||||
PACKAGES: ${{ github.event.inputs.packages }}
|
||||
UPSTREAM_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Failed feeds leave their recipes untouched; completed updates still
|
||||
# reach review. The failed sync step keeps the workflow red and notifies.
|
||||
- name: Check for changes
|
||||
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
|
||||
id: changes
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
@@ -65,7 +71,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -75,8 +81,9 @@ jobs:
|
||||
Automated update of packages that track an upstream vendor release
|
||||
feed rather than the AUR.
|
||||
|
||||
Each package reports its newest release through
|
||||
`.omarchy/upstream.sh`.
|
||||
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: auto/sync-upstream
|
||||
delete-branch: true
|
||||
labels: automated
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
name: Tests
|
||||
|
||||
# PR-only. Publishing on push has its own workflow and is what verifies the
|
||||
# merged tree: it resolves every package against the live channel and refuses
|
||||
# a filename that already exists with different bytes, so two PRs cannot land
|
||||
# the same version twice. A post-merge test run would only repeat the PR's.
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -30,6 +32,12 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Test PR workflow approval
|
||||
run: node --test tests/pr-workflow-approval.cjs
|
||||
|
||||
- name: Test builder images
|
||||
run: node --test tests/builder-image.cjs
|
||||
|
||||
# An Arch container for vercmp: version ordering has to be decided by
|
||||
# the same comparator pacman uses on users' machines.
|
||||
- name: Run self-tests
|
||||
@@ -39,11 +47,20 @@ jobs:
|
||||
-w /workspace \
|
||||
archlinux:base-devel bash -lc '
|
||||
set -euo pipefail
|
||||
pacman -Syu --noconfirm git jq neovim tmux python
|
||||
pacman -Syu --noconfirm git jq python libarchive neovim tmux
|
||||
python tests/oma-service-removal.py
|
||||
python tests/upstream-watch.py
|
||||
./bin/sync-upstream self-test
|
||||
./bin/sync-rebuilds --self-test
|
||||
./bin/omarchy-pkgs self-test
|
||||
./bin/omarchy-release self-test
|
||||
./tests/neovim-remote-clipboard.sh
|
||||
python tests/neovim-clipboard-tmux.py
|
||||
./tests/partial-release.sh
|
||||
./tests/published-build-plan.sh
|
||||
./tests/controller.sh
|
||||
./tests/artifact-helpers.sh
|
||||
./tests/limine-mkinitcpio-hook.sh
|
||||
pacman -S --noconfirm --quiet rclone >/dev/null
|
||||
./tests/publish-artifact.sh
|
||||
'
|
||||
@@ -37,3 +37,7 @@ pkgbuilds/yay/yay/
|
||||
.srcdest/
|
||||
.repo-host
|
||||
.worktrees/
|
||||
|
||||
# Python helpers and offline tests
|
||||
__pycache__/
|
||||
.release-verification/
|
||||
@@ -23,9 +23,9 @@ The filesystem no longer encodes release policy. Instead:
|
||||
(`OMARCHY_RC_PINS=1`, which `omarchy-release rc` sets) may build it for rc — master's
|
||||
shipped pins can never overwrite an in-flight RC. The dev pair
|
||||
(`omarchy-dev`, `omarchy-settings-dev`) is pinned to `edge`
|
||||
- AUR sync behavior is controlled by `source`, `sync`, `aur`, patches, and hooks in `.omarchy/`
|
||||
- Omarchy owns every checked-in recipe; upstream watches update release metadata without replacing packaging or architecture support
|
||||
- packages can opt out of unscoped builds with `skip_build`; explicit `--package` builds remain available
|
||||
- packages that follow a vendor release feed instead of the AUR carry an `.omarchy/upstream.sh` hook
|
||||
- packages follow direct upstream watches/providers in `.omarchy/package.json`, or a custom `.omarchy/upstream.sh` hook
|
||||
|
||||
## Prerequisites
|
||||
### aarch64 Builds (Optional)
|
||||
@@ -125,6 +125,14 @@ bin/repo advance --from edge --to rc
|
||||
|
||||
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
|
||||
@@ -321,14 +329,16 @@ 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.
|
||||
|
||||
### Sync AUR PKGBUILDs
|
||||
### Import an initial AUR recipe
|
||||
|
||||
```bash
|
||||
bin/sync-aur # Sync all AUR packages with sync enabled
|
||||
bin/sync-aur yay v4l2-relayd # Sync specific packages
|
||||
bin/add-package package-name --source aur
|
||||
```
|
||||
|
||||
AUR sync is metadata-driven. It preserves `.omarchy/`, replaces the package root with AUR contents, applies `.omarchy/patches/*.patch`, runs `.omarchy/post-sync.sh` when present, applies pkgrel metadata, removes AUR-only `.SRCINFO` and `.gitignore` files, and records `upstream_commit`.
|
||||
AUR is an optional source for an initial recipe. Imported packages become
|
||||
Omarchy-owned immediately; subsequent updates use direct upstream releases.
|
||||
There is no scheduled AUR sync. Edit the checked-in PKGBUILD to maintain
|
||||
architecture support and packaging behavior.
|
||||
|
||||
### Sync Upstream Releases
|
||||
|
||||
@@ -542,8 +552,8 @@ bin/omarchy-release # Release front door (start / pick / rc / s
|
||||
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/add-package <package> # Add an AUR/local package with metadata
|
||||
bin/package-worktree <package> # Create upstream/patched/current scratch workspace
|
||||
bin/add-package <package> # Add an Omarchy-owned package with metadata
|
||||
bin/package-worktree <package> # Inspect historical AUR provenance in a scratch workspace
|
||||
bin/repo remove <package> # Remove package
|
||||
bin/sync-upstream # Update packages that track a vendor release feed
|
||||
bin/sync-rebuilds # Bump pkgrel for packages whose dependencies moved
|
||||
@@ -562,7 +572,7 @@ bin/repo list # Table view of source package metadata
|
||||
bin/repo list --json # Agent/script-friendly JSON
|
||||
bin/repo list --repo --mirror stable # List packages in a published repo database
|
||||
|
||||
bin/package-worktree v4l2-relayd # Create upstream/patched/current scratch workspace
|
||||
bin/package-worktree yay # Compare with the original imported AUR recipe
|
||||
```
|
||||
|
||||
## Cutting an Omarchy Release
|
||||
@@ -676,9 +686,7 @@ omarchy-pkgs/
|
||||
│ ├── PKGBUILD
|
||||
│ └── .omarchy/
|
||||
│ ├── package.json # Source/sync/release metadata
|
||||
│ ├── patches/ # Omarchy patches reapplied after AUR sync
|
||||
│ ├── post-sync.sh # Optional dynamic post-sync customization hook
|
||||
│ └── upstream.sh # Optional vendor release feed hook (non-AUR packages)
|
||||
│ └── upstream.sh # Optional custom vendor release feed hook
|
||||
├── build/
|
||||
├── build-output/ # Unsigned packages (temporary)
|
||||
│ ├── edge/ # (rc/ and stable/ alongside, each x86_64 + aarch64)
|
||||
@@ -698,45 +706,28 @@ Each source package has Omarchy metadata at `pkgbuilds/<package>/.omarchy/packag
|
||||
|
||||
Minimal examples:
|
||||
|
||||
```json
|
||||
{ "source": "aur" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "source": "aur", "sync": false }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "source": "aur", "release_ring": "fast" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "source": "local" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "source": "local", "release_ring": "fast" }
|
||||
{ "source": "local", "skip_build": true }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "source": "aur", "pkgrel": { "suffix": 1 } }
|
||||
{ "source": "local", "upstream": { "watch": { "github": "abenz1267/walker", "pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)" } } }
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `source`: `aur` or `local`. A `local` package can still follow an upstream release, either declaratively via `upstream` or with an `.omarchy/upstream.sh` hook.
|
||||
- `upstream`: optional for `local` packages following GitHub releases, git tags, npm dist-tags, or a Debian `Packages` index. GitHub architecture assets may be a string or an ordered array, and can be combined with disjoint versioned `sources` — see [Sync Upstream Releases](#sync-upstream-releases). Mutually exclusive with `.omarchy/upstream.sh`.
|
||||
- `source`: `local` for maintained packages. The legacy `aur` value is used only during an initial import. A local recipe can follow an upstream watch, provider, or `.omarchy/upstream.sh` hook.
|
||||
- `upstream`: optional direct release watch (see [Upstream watches](docs/upstream-sources.md)), or an existing GitHub, git-tag, npm, or Debian provider. GitHub architecture assets may be a string or an ordered array, and can be combined with disjoint versioned `sources` — see [Sync Upstream Releases](#sync-upstream-releases). Mutually exclusive with `.omarchy/upstream.sh`.
|
||||
- `min_release_age`: optional quarantine for upstream releases (`"24h"`, `"2d"`, or bare seconds). The newest release older than the window ships; anything younger waits, and a release whose age cannot be proven fails the sync. Bypass deliberately with `BYPASS_MIN_RELEASE_AGE=1 bin/sync-upstream <package>`.
|
||||
- `sync`: optional for AUR packages; defaults to `true`. Set `false` for AUR-origin packages that Omarchy maintains manually.
|
||||
- `aur`: optional AUR package name when it differs from the local package directory, usually for split packages.
|
||||
- `sync`: `false` records an existing manual maintenance hold. Held packages have no upstream watch/provider/hook and are excluded from automatic updates.
|
||||
- `origin`: optional historical import provenance, with `aur` (package name) and `commit`. It does not control updates.
|
||||
- `release_ring`: optional. `fast` means the package is built directly for stable as well as edge, with the artifacts replicated into rc for parity. Packages without a ring build in edge and reach stable through the pipeline (`bin/repo advance`).
|
||||
- `channels`: optional array bounding where the package may be built (`edge`, `rc`, `stable`). Without the key a package is a member of every channel and follows the default build rules above; `bin/repo advance` refuses to carry a package anywhere it isn't a member.
|
||||
- `pinned`: optional boolean. A pinned package's version is set per release by `omarchy-release` on the `rc` branch, so it is never built for stable (promotion only) and is built for rc only from that branch's worktree (`OMARCHY_RC_PINS=1`). Used by `omarchy` and `omarchy-settings`.
|
||||
- `skip_build`: optional boolean; defaults to `false`. Set `true` to exclude a package from scheduled version checks and unscoped builds. The package can still be built explicitly with `bin/repo release --package <name>`.
|
||||
- `pkgrel`: optional Omarchy pkgrel suffix for a version-pinned rebuild bump. This emits `<aur pkgrel>.<suffix>` instead of replacing AUR's pkgrel. `offset` can be used only when preserving monotonic upgrades from old absolute pkgrel bumps. The metadata is removed automatically when AUR sync changes `pkgver`; the current package version is read from the checked-in PKGBUILD, so the version is not duplicated in JSON.
|
||||
- `pkgrel`: legacy import customization metadata. Maintained recipes keep their complete package release directly in PKGBUILD; rebuilds increment it there.
|
||||
- `rebuild_on`: optional array of package names this package links against closely enough that it must be rebuilt when they change, independent of its own source. Read by `bin/sync-rebuilds`.
|
||||
- `rebuilt_against`: written by `bin/sync-rebuilds`. Maps each published architecture to the versions of its `rebuild_on` packages that the current pkgrel was bumped for.
|
||||
- `upstream_commit`: set by `bin/sync-aur` for AUR packages. Used by `bin/package-worktree` to recreate the exact raw AUR package that Omarchy last synced.
|
||||
- `upstream_commit`: legacy AUR metadata, superseded by `origin.commit`. `bin/package-worktree` can use historical provenance to inspect the original recipe.
|
||||
|
||||
### Build Matrix
|
||||
|
||||
@@ -748,78 +739,26 @@ Fields:
|
||||
|
||||
## Adding Packages
|
||||
|
||||
### From AUR
|
||||
### Start from an existing recipe
|
||||
|
||||
```bash
|
||||
bin/add-package package-name
|
||||
bin/add-package package-name --source aur --fast
|
||||
# Review the imported files, own any architecture/packaging changes directly,
|
||||
# and declare an upstream watch/provider or hook in .omarchy/.
|
||||
bin/sync-upstream package-name
|
||||
bin/repo release --package package-name
|
||||
```
|
||||
|
||||
### From AUR, fast release ring
|
||||
The import records historical provenance in `origin`. It does not opt a package
|
||||
into future AUR imports. Upstream watches update only release scalars and source
|
||||
checksums; downstream build behavior stays in the recipe. Ordinary source-code
|
||||
patches still belong beside PKGBUILD and are applied by `prepare()` as needed.
|
||||
|
||||
### Custom package
|
||||
|
||||
```bash
|
||||
bin/add-package package-name --fast
|
||||
bin/repo release --package package-name
|
||||
bin/repo release --mirror stable --package package-name
|
||||
```
|
||||
|
||||
### AUR-origin, manually maintained by Omarchy
|
||||
|
||||
```bash
|
||||
bin/add-package package-name --no-sync
|
||||
```
|
||||
|
||||
### Local Customizations for AUR Packages
|
||||
|
||||
For static changes, create `pkgbuilds/package-name/.omarchy/patches/*.patch` to maintain modifications across AUR syncs.
|
||||
|
||||
The recommended workflow is to use a scratch workspace:
|
||||
|
||||
```bash
|
||||
bin/package-worktree package-name --dir /tmp/package-name-worktree
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
||||
```text
|
||||
upstream/ # raw AUR package at upstream_commit
|
||||
patched/ # AUR + existing Omarchy .omarchy customizations
|
||||
current/ # current checked-in package directory
|
||||
```
|
||||
|
||||
Patch-authoring flow:
|
||||
|
||||
```bash
|
||||
# 1. Make the intended change in pkgbuilds/package-name/
|
||||
|
||||
# 2. Recreate the scratch workspace
|
||||
bin/package-worktree package-name --dir /tmp/package-name-worktree
|
||||
|
||||
# 3. Inspect drift from patched -> current
|
||||
# For multi-file changes, inspect this and split into focused patches.
|
||||
diff -ruN /tmp/package-name-worktree/patched /tmp/package-name-worktree/current
|
||||
|
||||
# For a single PKGBUILD change, write a patch like this:
|
||||
mkdir -p pkgbuilds/package-name/.omarchy/patches
|
||||
(
|
||||
cd /tmp/package-name-worktree/patched
|
||||
diff -u --label a/PKGBUILD --label b/PKGBUILD \
|
||||
PKGBUILD /tmp/package-name-worktree/current/PKGBUILD || true
|
||||
) > pkgbuilds/package-name/.omarchy/patches/my-fix.patch
|
||||
|
||||
# 4. Verify the package is reproducible from AUR + .omarchy
|
||||
bin/sync-aur package-name
|
||||
bin/package-worktree package-name --dir /tmp/package-name-check
|
||||
diff -ruN /tmp/package-name-check/patched /tmp/package-name-check/current
|
||||
```
|
||||
|
||||
For dynamic changes that depend on the current upstream version, add `pkgbuilds/package-name/.omarchy/post-sync.sh`. The hook runs after the AUR package is copied into a temporary worktree and before the Omarchy pkgrel suffix is applied. After patches/hooks/metadata pkgrel overrides, `bin/sync-aur` removes AUR-only `.SRCINFO` and `.gitignore` files before writing the package back.
|
||||
|
||||
### Custom Package
|
||||
|
||||
```bash
|
||||
bin/add-package my-package --local --scaffold
|
||||
# Fill in PKGBUILD and package files
|
||||
bin/add-package my-package --scaffold
|
||||
# Fill in PKGBUILD, package files, and upstream metadata
|
||||
bin/repo release --package my-package
|
||||
```
|
||||
|
||||
@@ -886,6 +825,45 @@ 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. **First publication needs one package setting:** 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. This change only produces images;
|
||||
package jobs keep their existing behavior until image consumption is enabled.
|
||||
|
||||
## Version Management
|
||||
|
||||
Packages are only rebuilt if:
|
||||
@@ -902,9 +880,24 @@ The repository includes GitHub workflows and systemd services for automated rele
|
||||
|
||||
#### GitHub Workflows
|
||||
|
||||
1. **sync-aur.yml** (Every 6 hours): Syncs AUR packages according to `.omarchy/package.json` and opens a PR when changes are found.
|
||||
2. **sync-upstream.yml** (Every 6 hours): Runs `.omarchy/upstream.sh` for packages that track a vendor release feed and opens a PR when a newer version is out.
|
||||
3. **sync-rebuilds.yml** (Every 6 hours): Bumps pkgrel for packages whose `rebuild_on` dependencies have moved in the official repositories and opens a PR.
|
||||
1. **sync-upstream.yml** (Every 6 hours): Watches direct upstream feeds and updates owned recipes. Successful package updates reach a PR even if another feed fails; failed recipes stay untouched and the workflow remains red.
|
||||
2. **sync-rebuilds.yml** (Every 6 hours): Bumps pkgrel for packages whose `rebuild_on` dependencies have moved in the official repositories and opens a PR.
|
||||
|
||||
To approve builds for an unvouched contributor's PR, apply **`build-approved`**.
|
||||
Until approval, the PR shows **Awaiting build approval** and its required
|
||||
`result` check stays pending, keeping the PR blocked from merging without
|
||||
reporting a failed build. Actual build failures and denouncements still fail.
|
||||
Applying the label triggers a package build and automatically releases GitHub's
|
||||
pending build and test workflows for that PR's current commit. The approval workflow
|
||||
runs only trusted default-branch code; package builds and tests stay in the
|
||||
ordinary PR workflows. It may take a few minutes for GitHub to register and
|
||||
release all the runs.
|
||||
|
||||
The label stays effective for that PR while attached, including later commits;
|
||||
it does not vouch for the author's other PRs. Removing it stops further label
|
||||
approvals, but does not cancel runs already released. An explicit denouncement
|
||||
in `.github/VOUCHED.td` still blocks builds. If the approval workflow times out,
|
||||
remove and reapply the label to retry.
|
||||
|
||||
#### Systemd Services
|
||||
|
||||
|
||||
+19
-20
@@ -6,7 +6,7 @@ source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
|
||||
PACKAGE=""
|
||||
SOURCE="aur"
|
||||
SOURCE="local"
|
||||
SYNC="true"
|
||||
RELEASE_RING=""
|
||||
AUR_PACKAGE=""
|
||||
@@ -17,14 +17,14 @@ usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <package> [OPTIONS]
|
||||
|
||||
Create pkgbuilds/<package> with Omarchy metadata. AUR packages are synced
|
||||
immediately after metadata is written.
|
||||
Create an Omarchy-owned package. --source aur imports a starting recipe once;
|
||||
future releases must use an upstream watch, provider, or hook.
|
||||
|
||||
Options:
|
||||
--source <aur|local> Package source (default: aur)
|
||||
--source <aur|local> Initial recipe source (default: local)
|
||||
--local Shortcut for --source local
|
||||
--aur <name> AUR package name when different from local directory
|
||||
--no-sync For AUR packages, mark sync disabled after initial setup
|
||||
--no-sync Keep the imported recipe manually maintained
|
||||
--fast Put package in the fast release ring
|
||||
--release-ring <ring> Release ring (currently: fast)
|
||||
--scaffold For local packages, create a starter PKGBUILD
|
||||
@@ -32,9 +32,9 @@ Options:
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$0 yay
|
||||
$0 spotify --fast
|
||||
$0 signal-desktop --no-sync
|
||||
$0 yay --source aur
|
||||
$0 spotify --source aur --fast
|
||||
$0 signal-desktop --source aur --no-sync
|
||||
$0 omarchy-zsh --local --scaffold
|
||||
EOF
|
||||
}
|
||||
@@ -97,6 +97,10 @@ if [[ -z "$PACKAGE" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! $PACKAGE =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
|
||||
print_error "Invalid package name: $PACKAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$SOURCE" in
|
||||
aur|local) ;;
|
||||
@@ -112,6 +116,11 @@ PACKAGE_DIR="$PKGBUILDS_DIR/$PACKAGE"
|
||||
OMARCHY_DIR="$PACKAGE_DIR/.omarchy"
|
||||
METADATA_FILE="$OMARCHY_DIR/package.json"
|
||||
|
||||
if [[ "$SOURCE" == aur && -f "$PACKAGE_DIR/PKGBUILD" ]]; then
|
||||
print_error "Refusing to replace the maintained recipe for $PACKAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$METADATA_FILE" && "$FORCE" != true ]]; then
|
||||
print_error "Package metadata already exists: $METADATA_FILE"
|
||||
print_info "Use --force to overwrite it"
|
||||
@@ -146,18 +155,8 @@ jq -n "${jq_args[@]}" "$jq_filter" > "$METADATA_FILE"
|
||||
print_success "Wrote $METADATA_FILE"
|
||||
|
||||
if [[ "$SOURCE" == "aur" ]]; then
|
||||
if [[ "$SYNC" == "false" ]]; then
|
||||
# Temporarily sync once, then restore sync=false so future automated syncs skip it.
|
||||
tmpfile=$(mktemp)
|
||||
jq 'del(.sync)' "$METADATA_FILE" > "$tmpfile"
|
||||
mv "$tmpfile" "$METADATA_FILE"
|
||||
"$BUILD_ROOT/bin/sync-aur" "$PACKAGE"
|
||||
jq '. + {sync: false}' "$METADATA_FILE" > "$tmpfile"
|
||||
mv "$tmpfile" "$METADATA_FILE"
|
||||
print_info "AUR sync disabled for future runs"
|
||||
else
|
||||
"$BUILD_ROOT/bin/sync-aur" "$PACKAGE"
|
||||
fi
|
||||
"$BUILD_ROOT/bin/import-aur" "$PACKAGE"
|
||||
|
||||
else
|
||||
if [[ "$SCAFFOLD" == true && ! -f "$PACKAGE_DIR/PKGBUILD" ]]; then
|
||||
cat > "$PACKAGE_DIR/PKGBUILD" <<EOF
|
||||
|
||||
@@ -92,6 +92,8 @@ while [[ $# -gt 0 ]]; do
|
||||
echo " OMARCHY_KEEP_BUILD_WORKSPACE=1 keep build-output and reuse packages already there"
|
||||
echo " OMARCHY_SKIP_BUILDER_IMAGE=1 use the existing builder image instead of building it"
|
||||
echo " OMARCHY_DEFER_RUNTIME_DEPS=true build the omarchy pair with --nodeps (transaction verified later)"
|
||||
echo " OMARCHY_PUBLISHED_REPO_URL=<url> channel to plan and resolve against when no local tree exists"
|
||||
echo " (default https://pkgs.omarchy.org; empty disables the fallback)"
|
||||
echo ""
|
||||
exit 0
|
||||
;;
|
||||
@@ -238,6 +240,9 @@ PACKAGE_CACHE_DIR="$BUILD_ROOT/cache/pacman/$MIRROR/$ARCH"
|
||||
mkdir -p "$PACKAGE_CACHE_DIR"
|
||||
PLAN_DIR=$(mktemp -d "$SRC_DIR/build-plan.XXXXXX")
|
||||
trap 'rm -rf "$PLAN_DIR"' EXIT
|
||||
# Keep manifest directories host-owned so cleanup also works when Docker's
|
||||
# builder uid differs from the caller (as on GitHub runners).
|
||||
mkdir -p "$PLAN_DIR/artifacts"
|
||||
|
||||
# Rootful Docker writes as the image uid, so retain its existing permission
|
||||
# workaround. Rootless Podman uses keep-id and must leave ownership/modes alone.
|
||||
@@ -253,6 +258,7 @@ DOCKER_ARGS=(
|
||||
-e MIRROR="$MIRROR"
|
||||
-e PACKAGES="$PACKAGES"
|
||||
-e OMARCHY_RC_PINS="${OMARCHY_RC_PINS:-}"
|
||||
-e OMARCHY_PUBLISHED_REPO_URL="${OMARCHY_PUBLISHED_REPO_URL-https://pkgs.omarchy.org}"
|
||||
-e DEFER_RUNTIME_DEPS="$DEFER_RUNTIME_DEPS"
|
||||
-e BUILD_PLAN_DIR=/build-plan
|
||||
-v "$PLAN_DIR:/build-plan"
|
||||
@@ -320,6 +326,20 @@ echo " Skipped: ${#SKIPPED_PACKAGES[@]} (up-to-date or excluded)"
|
||||
echo " Failed: ${#FAILED_PACKAGES[@]}"
|
||||
echo " Blocked: ${#BLOCKED_PACKAGES[@]}"
|
||||
|
||||
# The release caller supplies a fresh directory. A completed result
|
||||
# distinguishes package failures from an interrupted/failed orchestrator;
|
||||
# only artifacts belonging to fully successful builds may be published.
|
||||
if [[ -n "${OMARCHY_BUILD_RESULT_DIR:-}" ]]; then
|
||||
mkdir -p "$OMARCHY_BUILD_RESULT_DIR"
|
||||
: > "$OMARCHY_BUILD_RESULT_DIR/artifacts"
|
||||
for package in "${SUCCESSFUL_PACKAGES[@]}"; do
|
||||
cat "$PLAN_DIR/artifacts/$package" >> "$OMARCHY_BUILD_RESULT_DIR/artifacts"
|
||||
done
|
||||
printf '%s\n' "${FAILED_PACKAGES[@]}" | sed '/^$/d' > "$OMARCHY_BUILD_RESULT_DIR/failed"
|
||||
printf '%s\n' "${BLOCKED_PACKAGES[@]}" | sed '/^$/d' > "$OMARCHY_BUILD_RESULT_DIR/blocked"
|
||||
touch "$OMARCHY_BUILD_RESULT_DIR/complete"
|
||||
fi
|
||||
|
||||
if (( ${#FAILED_PACKAGES[@]} + ${#BLOCKED_PACKAGES[@]} )); then
|
||||
if (( ${#FAILED_PACKAGES[@]} )); then
|
||||
echo "Failed packages:"
|
||||
@@ -330,7 +350,9 @@ if (( ${#FAILED_PACKAGES[@]} + ${#BLOCKED_PACKAGES[@]} )); then
|
||||
printf ' - %s\n' "${BLOCKED_PACKAGES[@]}"
|
||||
fi
|
||||
print_warning "Some packages failed (see details above)"
|
||||
exit 1
|
||||
# Reserved for a completed run with unsuccessful packages. Other failures
|
||||
# must not let release publish arbitrary files left in the workspace.
|
||||
exit 2
|
||||
fi
|
||||
|
||||
print_success "Build completed successfully!"
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Print the PR build matrix for a set of package directories as JSON: one
|
||||
# entry per package per supported architecture. Every package builds exactly
|
||||
# once, against edge, and that one artifact is what every channel ships:
|
||||
# channels are databases over a shared pool of files, and a filename must
|
||||
# mean one set of bytes. "channels" lists where the artifact is published on
|
||||
# merge: edge for everything, plus rc and stable immediately for the fast
|
||||
# ring. Eligibility comes from package_builds_for_mirror, the rule the
|
||||
# release host uses, so CI and the host cannot disagree.
|
||||
#
|
||||
# Usage: build-matrix [--arch <arch>|all] <package>...
|
||||
# Reads package names on stdin when none are given. With no --arch, every
|
||||
# architecture in CI_ARCHES (default "x86_64 aarch64") the package supports.
|
||||
# Output: {"include":[{"package":"x","arch":"x86_64","channels":"edge rc stable","publish_arches":"x86_64"},...]}
|
||||
# arch is where it builds; publish_arches lists every architecture
|
||||
# database the file goes into (all of them for arch=any).
|
||||
set -euo pipefail
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
source "$BUILD_ROOT/helpers/package-metadata.sh"
|
||||
|
||||
ARCHES=${CI_ARCHES:-x86_64 aarch64}
|
||||
if [[ "${1:-}" == --arch ]]; then [[ $2 == all ]] || ARCHES=$2; shift 2; fi
|
||||
for a in $ARCHES; do require_valid_arch "$a"; done
|
||||
|
||||
if (( $# )); then names=("$@"); else mapfile -t names; fi
|
||||
|
||||
entries=()
|
||||
for name in "${names[@]}"; do
|
||||
[[ -n "$name" ]] || continue
|
||||
pkgdir="$PKGBUILDS_DIR/$name"
|
||||
[[ -d "$pkgdir" ]] || continue
|
||||
# skip_build packages still build on their own PR (explicit --package
|
||||
# semantics); the host's unscoped runs are what skip them.
|
||||
channels=""
|
||||
for mirror in $VALID_MIRRORS; do
|
||||
package_builds_for_mirror "$pkgdir" "$mirror" && channels="$channels $mirror"
|
||||
done
|
||||
channels=${channels# }
|
||||
[[ -n "$channels" ]] || continue
|
||||
# An arch=any package produces one architecture-independent file, so it
|
||||
# builds once, on the first architecture, and that file serves every
|
||||
# channel database of every architecture.
|
||||
if [[ " $(package_arches "$pkgdir" "${ARCHES%% *}") " == *" any "* ]]; then
|
||||
entries+=("$(jq -nc --arg p "$name" --arg a "${ARCHES%% *}" --arg c "$channels" --arg pa "$ARCHES" '{package:$p, arch:$a, channels:$c, publish_arches:$pa}')")
|
||||
continue
|
||||
fi
|
||||
for arch in $ARCHES; do
|
||||
package_supports_arch "$pkgdir" "$arch" || continue
|
||||
entries+=("$(jq -nc --arg p "$name" --arg a "$arch" --arg c "$channels" '{package:$p, arch:$a, channels:$c, publish_arches:$a}')")
|
||||
done
|
||||
done
|
||||
|
||||
printf '%s\n' "${entries[@]}" | jq -sc '{include: .}'
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
# Build a reusable package environment from this checkout's own inputs.
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
|
||||
usage() {
|
||||
echo "Usage: bin/builder-image {key|build} [--arch x86_64|aarch64] [--mirror edge|rc|stable] [--tag IMAGE] [--fresh]"
|
||||
}
|
||||
|
||||
command=${1:-}
|
||||
[[ $# -eq 0 ]] || shift
|
||||
tag=""
|
||||
fresh=false
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--arch) ARCH=${2:?Missing architecture}; shift 2 ;;
|
||||
--mirror) MIRROR=${2:?Missing mirror}; shift 2 ;;
|
||||
--tag) tag=${2:?Missing image tag}; shift 2 ;;
|
||||
--fresh) fresh=true; shift ;;
|
||||
*) usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
require_valid_arch "$ARCH"
|
||||
validate_mirror "$MIRROR" || { echo "Invalid mirror: $MIRROR" >&2; exit 1; }
|
||||
case "$command" in key|build) ;; *) usage >&2; exit 1 ;; esac
|
||||
if [[ $command == key && ( -n $tag || $fresh == true ) ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Include the whole build context, conservatively including mounted build
|
||||
# scripts too. Normalize timestamps and ownership so fresh checkouts agree;
|
||||
# retain file contents, names, executable bits and symlink targets. Bump v1
|
||||
# if the image build invocation or this compatibility contract changes.
|
||||
hash=$(tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner \
|
||||
--format=gnu -cf - -C "$BUILD_DIR" . | sha256sum | cut -d' ' -f1)
|
||||
key="v1-$ARCH-$MIRROR-$hash"
|
||||
if [[ $command == key ]]; then
|
||||
echo "$key"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/docker-helpers.sh"
|
||||
check_engine
|
||||
platform=$(get_platform_arg "$ARCH")
|
||||
tag=${tag:-omarchy-pkg-builder:latest-$ARCH-$MIRROR}
|
||||
revision=$(git -C "$BUILD_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)
|
||||
args=("$platform" --build-arg "MIRROR=$MIRROR"
|
||||
--label "org.omarchy.builder.key=$key"
|
||||
--label "org.opencontainers.image.source=https://github.com/omacom/omarchy-pkgs"
|
||||
--label "org.opencontainers.image.revision=$revision"
|
||||
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
--tag "$tag" --file "$BUILD_DIR/Dockerfile")
|
||||
if [[ $fresh == true ]]; then
|
||||
# A daily build must refresh Arch even when its Dockerfile has not changed.
|
||||
args+=(--no-cache)
|
||||
if [[ $CONTAINER_ENGINE == docker ]]; then args+=(--pull); else args+=(--pull=always); fi
|
||||
fi
|
||||
if [[ $CONTAINER_ENGINE == docker ]]; then
|
||||
docker buildx build --load "${args[@]}" "$BUILD_DIR"
|
||||
else
|
||||
podman build "${args[@]}" "$BUILD_DIR"
|
||||
fi
|
||||
+2
-2
@@ -172,8 +172,8 @@ check_package() {
|
||||
# it because that exact filename is already published with different bytes.
|
||||
# If the artifact for this version already exists in the channel, there is
|
||||
# nothing to build regardless of which direction the versions differ.
|
||||
if compgen -G "$REPO_ROOT/$mirror/$ARCH/${pkg}-${pkgbuild_version}-*.pkg.tar."[!s]* >/dev/null 2>&1; then
|
||||
print_warning "$pkg $pkgbuild_version is already published — this checkout is behind the channel; not queueing"
|
||||
if package_version_is_published "$REPO_ROOT/$mirror/$ARCH" "$pkg" "$pkgbuild_version" "$ARCH"; then
|
||||
print_warning "$pkg $pkgbuild_version is already published; not queueing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Internal initial-recipe import used by add-package. Maintained recipes are
|
||||
# never replaced; subsequent releases go through sync-upstream.
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
|
||||
if [[ ${1:-} == --help || ${1:-} == -h ]]; then
|
||||
echo "Usage: bin/add-package <package> --source aur [--aur <upstream-name>]"
|
||||
exit 0
|
||||
fi
|
||||
if [[ $# != 1 || ! $1 =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
|
||||
print_error "Use bin/add-package <package> --source aur for an initial import"
|
||||
exit 1
|
||||
fi
|
||||
package=$1
|
||||
package_dir="$PKGBUILDS_DIR/$package"
|
||||
metadata="$package_dir/.omarchy/package.json"
|
||||
if [[ -f "$package_dir/PKGBUILD" ]]; then
|
||||
print_error "Refusing to replace the maintained recipe for $package"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$metadata" ]] || [[ $(jq -r .source "$metadata") != aur ]]; then
|
||||
print_error "Initial import requires metadata created by bin/add-package --source aur"
|
||||
exit 1
|
||||
fi
|
||||
aur_package=$(jq -r --arg name "$package" '.aur // $name' "$metadata")
|
||||
if [[ ! $aur_package =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
|
||||
print_error "Invalid AUR package name"
|
||||
exit 1
|
||||
fi
|
||||
# Refuse unrelated package files: an import only starts from .omarchy metadata.
|
||||
shopt -s dotglob nullglob
|
||||
for item in "$package_dir"/*; do
|
||||
if [[ ${item##*/} != .omarchy ]]; then
|
||||
print_error "Initial import needs an empty package directory: $package_dir"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
work=$(mktemp -d "$PKGBUILDS_DIR/.import-aur.XXXXXX")
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
print_info "Importing $package from AUR package $aur_package..."
|
||||
git clone --quiet "https://aur.archlinux.org/${aur_package}.git" "$work/recipe"
|
||||
[[ -f "$work/recipe/PKGBUILD" ]] || { print_error "AUR package has no PKGBUILD"; exit 1; }
|
||||
commit=$(git -C "$work/recipe" rev-parse HEAD)
|
||||
rm -rf "$work/recipe/.git" "$work/recipe/.omarchy"
|
||||
rm -f "$work/recipe/.SRCINFO" "$work/recipe/.gitignore"
|
||||
cp -a "$package_dir/.omarchy" "$work/recipe/.omarchy"
|
||||
jq --arg name "$aur_package" --arg commit "$commit" '
|
||||
.source = "local" | .origin = {aur: $name, commit: $commit}
|
||||
| del(.aur, .upstream_commit)
|
||||
' "$metadata" > "$work/recipe/.omarchy/package.json"
|
||||
# Stage on the same filesystem, restoring the metadata directory on failure.
|
||||
mv "$package_dir" "$work/original"
|
||||
if ! mv "$work/recipe" "$package_dir"; then
|
||||
mv "$work/original" "$package_dir"
|
||||
exit 1
|
||||
fi
|
||||
print_success "Imported $package; review the recipe and configure its direct upstream watch"
|
||||
@@ -17,13 +17,13 @@ Usage: $0 <package> [OPTIONS]
|
||||
Create a scratch workspace for inspecting an AUR-backed package.
|
||||
|
||||
The workspace contains:
|
||||
upstream/ Raw AUR package at .omarchy/package.json upstream_commit, or HEAD
|
||||
upstream/ Raw AUR package at the recorded origin.commit, or HEAD
|
||||
patched/ Raw AUR package with Omarchy .omarchy patches/hooks/pkgrel applied
|
||||
current/ Current checked-in package directory
|
||||
|
||||
Options:
|
||||
--dir <path> Workspace directory (default: mktemp under /tmp)
|
||||
--commit <sha> Use a specific AUR commit instead of upstream_commit
|
||||
--commit <sha> Use a specific AUR commit instead of origin.commit
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
@@ -75,13 +75,13 @@ if [[ ! -f "$METADATA" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(jq -r '.source // ""' "$METADATA")" != "aur" ]]; then
|
||||
if [[ "$(jq -r '.origin.aur // .aur // (if .source == "aur" then "legacy" else "" end)' "$METADATA")" == "" ]]; then
|
||||
print_error "package-worktree only supports AUR-backed packages"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
AUR_PACKAGE=$(jq -r --arg package "$PACKAGE" '.aur // $package' "$METADATA")
|
||||
UPSTREAM_COMMIT=${COMMIT_OVERRIDE:-$(jq -r '.upstream_commit // ""' "$METADATA")}
|
||||
AUR_PACKAGE=$(jq -r --arg package "$PACKAGE" '.origin.aur // .aur // $package' "$METADATA")
|
||||
UPSTREAM_COMMIT=${COMMIT_OVERRIDE:-$(jq -r '.origin.commit // .upstream_commit // ""' "$METADATA")}
|
||||
|
||||
if [[ -z "$DEST_DIR" ]]; then
|
||||
DEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/omarchy-${PACKAGE}.XXXXXX")
|
||||
@@ -224,7 +224,7 @@ print_info "AUR package: $AUR_PACKAGE"
|
||||
if [[ -n "$UPSTREAM_COMMIT" ]]; then
|
||||
print_info "Upstream commit: $UPSTREAM_COMMIT"
|
||||
else
|
||||
print_warning "No upstream_commit recorded; using AUR HEAD"
|
||||
print_warning "No origin.commit recorded; using AUR HEAD"
|
||||
fi
|
||||
|
||||
rm -rf "$UPSTREAM_DIR" "$PATCHED_DIR" "$CURRENT_DIR"
|
||||
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
# Publish built packages into one channel of the remote repository,
|
||||
# incrementally and immutably.
|
||||
#
|
||||
# publish-artifact --mirror <edge|rc|stable> --arch <arch> <pkg files...>
|
||||
#
|
||||
# What it does, in order:
|
||||
# 1. pull the channel's current database from the remote
|
||||
# 2. refuse if any package filename already exists on the remote
|
||||
# 3. sign each package (GPG_PRIVATE_KEY / GPG_PASSPHRASE)
|
||||
# 4. repo-add the packages into the pulled database (replaces the entry
|
||||
# for that name; nothing else in the channel is touched)
|
||||
# 5. upload packages, then signatures, then the database last
|
||||
#
|
||||
# Never overwrites: uploads use --ignore-existing for packages and the
|
||||
# pre-check in step 2 makes a same-name collision a hard failure rather than
|
||||
# a silent skip. The database is the only object rewritten, and it is
|
||||
# uploaded only after every file it references is present.
|
||||
#
|
||||
# The remote is an rclone remote (REMOTE, default the production one);
|
||||
# OMARCHY_PUBLISH_PREFIX can point a proof run at a scratch prefix.
|
||||
set -euo pipefail
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
|
||||
REMOTE=${REMOTE:-pkgs.omarchy.org:omarchy-pkgs}
|
||||
PREFIX=${OMARCHY_PUBLISH_PREFIX:-}
|
||||
FILES=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--mirror) MIRROR=$2; require_valid_mirror "$MIRROR"; shift 2 ;;
|
||||
--arch) ARCH=$2; require_valid_arch "$ARCH"; shift 2 ;;
|
||||
--remote) REMOTE=$2; shift 2 ;;
|
||||
-h|--help) sed -n '2,22p' "$0"; exit 0 ;;
|
||||
-*) print_error "Unknown option: $1"; exit 1 ;;
|
||||
*) FILES+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
(( ${#FILES[@]} )) || { print_error "No package files given"; exit 1; }
|
||||
: "${GPG_PRIVATE_KEY:?}"; GPG_PASSPHRASE=${GPG_PASSPHRASE-}
|
||||
|
||||
DEST="$REMOTE/${PREFIX:+$PREFIX/}$MIRROR/$ARCH"
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
print_header "Publish to $DEST"
|
||||
|
||||
# --- 0. sanity: every file is a package, named as makepkg names it ---------
|
||||
for f in "${FILES[@]}"; do
|
||||
[[ -f "$f" && "$f" == *.pkg.tar.* && "$f" != *.sig ]] || { print_error "Not a package file: $f"; exit 1; }
|
||||
name=$(bsdtar -xOf "$f" .PKGINFO | awk -F' = ' '$1=="pkgname"{print $2}')
|
||||
ver=$(bsdtar -xOf "$f" .PKGINFO | awk -F' = ' '$1=="pkgver"{print $2}')
|
||||
pkgarch=$(bsdtar -xOf "$f" .PKGINFO | awk -F' = ' '$1=="arch"{print $2}')
|
||||
[[ "$(basename "$f")" == "$name-$ver-$pkgarch.pkg.tar."* ]] || {
|
||||
print_error "Filename does not match PKGINFO ($name-$ver-$pkgarch): $(basename "$f")"; exit 1; }
|
||||
[[ "$pkgarch" == any || "$pkgarch" == "$ARCH" ]] || { print_error "$f is $pkgarch, publishing to $ARCH"; exit 1; }
|
||||
done
|
||||
|
||||
# --- 1. pull the current database -----------------------------------------
|
||||
mkdir -p "$WORK/repo"
|
||||
listing=$(rclone lsf "$DEST/" --s3-no-head 2>/dev/null || true)
|
||||
if grep -q '^omarchy.db.tar.zst$' <<<"$listing"; then
|
||||
rclone copy "$DEST/omarchy.db.tar.zst" "$WORK/repo" --s3-no-head
|
||||
rclone copy "$DEST/omarchy.files.tar.zst" "$WORK/repo" --s3-no-head 2>/dev/null || true
|
||||
print_info "Pulled current database ($(tar -tf "$WORK/repo/omarchy.db.tar.zst" | grep -c '/$') entries)"
|
||||
else
|
||||
print_warning "No database at $DEST — creating a new one"
|
||||
fi
|
||||
|
||||
# --- 2. same-name collisions ----------------------------------------------
|
||||
# A filename must mean one set of bytes across every channel. The same file
|
||||
# reaching a channel that already holds it (a fast-ring publish after edge,
|
||||
# a re-run, a later promotion) is fine: it is skipped on upload and only the
|
||||
# database entry is added. Different bytes under a name the channel already
|
||||
# has is the one thing this must never do.
|
||||
for f in "${FILES[@]}"; do
|
||||
b=$(basename "$f")
|
||||
grep -qxF "$b" <<<"$listing" || continue
|
||||
remote_sum=$(rclone hashsum md5 "$DEST/$b" --s3-no-head 2>/dev/null | awk '{print $1}')
|
||||
local_sum=$(md5sum "$f" | awk '{print $1}')
|
||||
if [[ -n "$remote_sum" && "$remote_sum" == "$local_sum" ]]; then
|
||||
print_info "Already published with identical bytes, adding to the database only: $b"
|
||||
else
|
||||
print_error "Already published with DIFFERENT bytes, refusing to overwrite: $b"
|
||||
echo " Bump pkgrel; published filenames are immutable."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 3. sign ---------------------------------------------------------------
|
||||
export GNUPGHOME="$WORK/gnupg"; mkdir -m 700 "$GNUPGHOME"
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --quiet --import
|
||||
KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '$1=="sec"{print $5; exit}')
|
||||
[[ -n "$KEY_ID" ]] || { print_error "No secret key imported"; exit 1; }
|
||||
for f in "${FILES[@]}"; do
|
||||
cp "$f" "$WORK/repo/"
|
||||
gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \
|
||||
--detach-sign --no-armor --local-user "$KEY_ID" "$WORK/repo/$(basename "$f")"
|
||||
print_step "signed $(basename "$f")"
|
||||
done
|
||||
|
||||
# --- 4. repo-add (replaces the entry for each pkgname) ---------------------
|
||||
( cd "$WORK/repo" && repo-add --quiet omarchy.db.tar.zst "${FILES[@]/#*\//}" )
|
||||
ln -sf omarchy.db.tar.zst "$WORK/repo/omarchy.db"
|
||||
ln -sf omarchy.files.tar.zst "$WORK/repo/omarchy.files"
|
||||
print_info "Database now has $(tar -tf "$WORK/repo/omarchy.db.tar.zst" | grep -c '/$') entries"
|
||||
|
||||
# --- 5. upload: packages, signatures, database last -----------------------
|
||||
rclone copy "$WORK/repo" "$DEST" --s3-no-head --ignore-existing --filter '- *.sig' --filter '+ *.pkg.tar.*' --filter '- *'
|
||||
rclone copy "$WORK/repo" "$DEST" --s3-no-head --ignore-existing --filter '+ *.pkg.tar.*.sig' --filter '- *'
|
||||
# Re-verify every referenced file is really there before the db goes up.
|
||||
listing=$(rclone lsf "$DEST/" --s3-no-head)
|
||||
for f in "${FILES[@]}"; do
|
||||
b=$(basename "$f")
|
||||
grep -qxF "$b" <<<"$listing" && grep -qxF "$b.sig" <<<"$listing" || { print_error "Upload incomplete: $b"; exit 1; }
|
||||
done
|
||||
rclone copy "$WORK/repo" "$DEST" --s3-no-head --checksum --copy-links --filter '+ omarchy.db*' --filter '+ omarchy.files*' --filter '- *'
|
||||
print_success "Published ${#FILES[@]} package(s) to $DEST"
|
||||
+56
-3
@@ -138,11 +138,43 @@ if [[ "$DRY_RUN" == true ]]; then
|
||||
else
|
||||
print_info "Step 1/6: Building packages..."
|
||||
fi
|
||||
"$BUILD_ROOT/bin/build" "${BUILD_ARGS[@]}" || {
|
||||
BUILD_RESULT_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$BUILD_RESULT_DIR"' EXIT
|
||||
build_status=0
|
||||
OMARCHY_BUILD_RESULT_DIR="$BUILD_RESULT_DIR" "$BUILD_ROOT/bin/build" "${BUILD_ARGS[@]}" || build_status=$?
|
||||
partial=false
|
||||
if [[ "$build_status" == 2 && -f "$BUILD_RESULT_DIR/complete" && "$DRY_RUN" != true ]]; then
|
||||
if [[ "${OMARCHY_DEFER_RUNTIME_DEPS:-false}" == true ]]; then
|
||||
print_error "The deferred release pair must succeed together; nothing will be published"
|
||||
notify_error "Release failed: Incomplete release pair" "$RELEASE_CONTEXT"
|
||||
exit 1
|
||||
fi
|
||||
partial=true
|
||||
while IFS= read -r file; do
|
||||
[[ -f "$BUILD_OUTPUT_DIR/$file" ]] || {
|
||||
print_error "Completed build artifact is missing: $file"
|
||||
notify_error "Release failed: Missing completed artifact" "$RELEASE_CONTEXT"
|
||||
exit 1
|
||||
}
|
||||
done < "$BUILD_RESULT_DIR/artifacts"
|
||||
# Exclude partial split outputs or leftovers from failed builds. Moving
|
||||
# them out of the flat publication directory keeps them available for
|
||||
# diagnosis without handing them to sign/promote.
|
||||
unpublished=""
|
||||
for path in "$BUILD_OUTPUT_DIR"/*.pkg.tar.*; do
|
||||
[[ -f "$path" ]] || continue
|
||||
file=${path##*/}
|
||||
if ! grep -Fxq -- "${file%.sig}" "$BUILD_RESULT_DIR/artifacts"; then
|
||||
[[ -n "$unpublished" ]] || unpublished=$(mktemp -d "$BUILD_OUTPUT_DIR/.unpublished.XXXXXX")
|
||||
mv -- "$path" "$unpublished/"
|
||||
fi
|
||||
done
|
||||
print_warning "Some packages failed; publishing the completed packages before retrying the failures"
|
||||
elif [[ "$build_status" != 0 ]]; then
|
||||
print_error "Build failed"
|
||||
notify_error "Release failed: Build step failed" "$RELEASE_CONTEXT"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo ""
|
||||
@@ -155,6 +187,12 @@ BUILT_COUNT=$(grep -c '' <<<"$BUILT_FILES")
|
||||
[[ -z "$BUILT_FILES" ]] && BUILT_COUNT=0
|
||||
print_info "Built $BUILT_COUNT package(s) this run"
|
||||
|
||||
if [[ "$partial" == true && "$BUILT_COUNT" == 0 ]]; then
|
||||
print_error "No completed packages to publish"
|
||||
notify_error "Release failed: No completed packages" "$RELEASE_CONTEXT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 2: Sign
|
||||
echo ""
|
||||
print_info "Step 2/6: Signing packages..."
|
||||
@@ -213,7 +251,16 @@ if ((BUILT_COUNT > 0)); then
|
||||
summary+="<br><br><strong>$BUILT_COUNT package(s) published:</strong>"
|
||||
summary+="$(format_package_list_html "$BUILT_FILES")"
|
||||
summary+="<br><br>Live at https://pkgs.omarchy.org/$MIRROR/$ARCH/"
|
||||
notify_success "Release published: $MIRROR" "$summary"
|
||||
if [[ "$partial" == true ]]; then
|
||||
failed=$(tr '\n' ' ' < "$BUILD_RESULT_DIR/failed" | basecamp_html_escape)
|
||||
blocked=$(tr '\n' ' ' < "$BUILD_RESULT_DIR/blocked" | basecamp_html_escape)
|
||||
[[ -z "$failed" ]] || summary+="<br><br>Failed: $failed"
|
||||
[[ -z "$blocked" ]] || summary+="<br>Blocked by failed dependencies: $blocked"
|
||||
summary+="<br>Published packages will be skipped on retry; failed packages remain queued."
|
||||
notify_info "Partial release published: $MIRROR" "$summary"
|
||||
else
|
||||
notify_success "Release published: $MIRROR" "$summary"
|
||||
fi
|
||||
else
|
||||
# Rare by construction: a release only runs when the version check queued
|
||||
# work, so publishing nothing means the check and the builder disagreed
|
||||
@@ -228,4 +275,10 @@ else
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$partial" == true ]]; then
|
||||
print_warning "Completed packages published; unsuccessful packages remain for retry"
|
||||
# Preserve the scheduled queue and failure backoff. The next version
|
||||
# check/build compares against the updated repository and skips successes.
|
||||
exit 1
|
||||
fi
|
||||
print_success "Release workflow completed successfully!"
|
||||
-434
@@ -1,434 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
source "$BUILD_ROOT/helpers/package-metadata.sh"
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
|
||||
SPECIFIC_PACKAGES=()
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [PACKAGE...]
|
||||
|
||||
Sync AUR-backed packages into pkgbuilds/<package>/.
|
||||
|
||||
Package selection is driven by pkgbuilds/<package>/.omarchy/package.json:
|
||||
{ "source": "aur" } # synced from matching AUR package name
|
||||
{ "source": "aur", "aur": "yaru" } # synced from different AUR package name
|
||||
{ "source": "aur", "sync": false } # AUR-origin, but not auto-synced
|
||||
|
||||
Arguments:
|
||||
PACKAGE One or more package names to sync (optional)
|
||||
|
||||
Examples:
|
||||
$0 # Sync all AUR packages with sync enabled
|
||||
$0 yay cursor-bin # Sync specific packages
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--tier)
|
||||
print_error "--tier is no longer supported; package metadata controls sync behavior"
|
||||
exit 1
|
||||
;;
|
||||
--*)
|
||||
print_error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
SPECIFIC_PACKAGES+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
print_header "AUR Package Sync"
|
||||
mkdir -p "$PKGBUILDS_DIR"
|
||||
|
||||
SYNCED=0
|
||||
SKIPPED=0
|
||||
FAILED=0
|
||||
SYNCED_PACKAGES=()
|
||||
SPECIFIC_MODE=false
|
||||
|
||||
get_pkgbuild_field() {
|
||||
local package_dir="$1"
|
||||
local field="$2"
|
||||
local value=""
|
||||
|
||||
if [[ -f "$package_dir/PKGBUILD" ]]; then
|
||||
value=$(grep -m1 "^${field}=" "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'") || true
|
||||
if [[ -n "$value" ]]; then
|
||||
echo "$value"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f "$package_dir/.SRCINFO" ]]; then
|
||||
awk -F' = ' -v field="$field" '$1 ~ "^[[:space:]]*" field "$" { print $2; exit }' "$package_dir/.SRCINFO"
|
||||
fi
|
||||
}
|
||||
|
||||
set_pkgrel() {
|
||||
local package_dir="$1"
|
||||
local pkgrel="$2"
|
||||
local pkgbuild="$package_dir/PKGBUILD"
|
||||
|
||||
if [[ -f "$pkgbuild" ]]; then
|
||||
sed -i "s/^pkgrel=.*/pkgrel=$pkgrel/" "$pkgbuild"
|
||||
fi
|
||||
}
|
||||
|
||||
display_package_name() {
|
||||
local package_dir="$1"
|
||||
local name
|
||||
name=$(basename "$package_dir")
|
||||
echo "${name%.work}"
|
||||
}
|
||||
|
||||
remove_aur_only_files() {
|
||||
local package_dir="$1"
|
||||
|
||||
rm -f "$package_dir/.SRCINFO" "$package_dir/.gitignore"
|
||||
}
|
||||
|
||||
copy_aur_contents() {
|
||||
local aur_dir="$1"
|
||||
local target_dir="$2"
|
||||
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
shopt -s dotglob nullglob
|
||||
local item base
|
||||
for item in "$aur_dir"/*; do
|
||||
base=$(basename "$item")
|
||||
[[ "$base" == ".git" ]] && continue
|
||||
cp -a "$item" "$target_dir/"
|
||||
done
|
||||
shopt -u dotglob nullglob
|
||||
}
|
||||
|
||||
commit_synced_worktree() {
|
||||
local work_dir="$1"
|
||||
local target_dir="$2"
|
||||
local parent base staged_dir backup_root backup_dir
|
||||
|
||||
parent=$(dirname "$target_dir")
|
||||
base=$(basename "$target_dir")
|
||||
staged_dir=$(mktemp -d "$parent/.${base}.staged.XXXXXX")
|
||||
backup_root=$(mktemp -d "$parent/.${base}.backup.XXXXXX")
|
||||
backup_dir="$backup_root/$base"
|
||||
|
||||
# Copy to the package filesystem before swapping. This keeps the original
|
||||
# package directory intact if copying from /tmp fails or is interrupted.
|
||||
if ! cp -a "$work_dir/." "$staged_dir/"; then
|
||||
print_error "Failed to stage synced package for $base"
|
||||
rm -rf "$staged_dir" "$backup_root"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -e "$target_dir" ]]; then
|
||||
if ! mv "$target_dir" "$backup_dir"; then
|
||||
print_error "Failed to back up existing package directory: $target_dir"
|
||||
rm -rf "$staged_dir" "$backup_root"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! mv "$staged_dir" "$target_dir"; then
|
||||
print_error "Failed to install synced package directory: $target_dir"
|
||||
if [[ -e "$backup_dir" ]]; then
|
||||
mv "$backup_dir" "$target_dir" || true
|
||||
fi
|
||||
rm -rf "$staged_dir" "$backup_root"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -rf "$backup_root" "$work_dir"
|
||||
}
|
||||
|
||||
set_upstream_commit() {
|
||||
local package_dir="$1"
|
||||
local commit="$2"
|
||||
local metadata="$package_dir/.omarchy/package.json"
|
||||
local tmpfile
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
jq --arg commit "$commit" '.upstream_commit = $commit' "$metadata" > "$tmpfile"
|
||||
mv "$tmpfile" "$metadata"
|
||||
}
|
||||
|
||||
apply_omarchy_patches() {
|
||||
local package_dir="$1"
|
||||
local patches_dir="$package_dir/.omarchy/patches"
|
||||
local applied=false
|
||||
|
||||
[[ -d "$patches_dir" ]] || return 1
|
||||
|
||||
shopt -s nullglob
|
||||
local patch_files=("$patches_dir"/*.patch)
|
||||
local patch_dir_files=("$patches_dir"/*)
|
||||
shopt -u nullglob
|
||||
|
||||
if [[ ${#patch_files[@]} -eq 0 ]]; then
|
||||
if [[ ${#patch_dir_files[@]} -gt 0 ]]; then
|
||||
print_warning "No .patch files found in $patches_dir"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Applying Omarchy patches for $(display_package_name "$package_dir")..."
|
||||
local patch_file
|
||||
for patch_file in "${patch_files[@]}"; do
|
||||
print_info " $(basename "$patch_file")"
|
||||
if ! (cd "$package_dir" && patch -p1 --forward --batch --no-backup-if-mismatch < "$patch_file"); then
|
||||
print_error "Failed to apply patch: $patch_file"
|
||||
return 2
|
||||
fi
|
||||
applied=true
|
||||
done
|
||||
|
||||
[[ "$applied" == true ]]
|
||||
}
|
||||
|
||||
run_omarchy_post_sync_hook() {
|
||||
local package_dir="$1"
|
||||
local package="$2"
|
||||
local aur_package="$3"
|
||||
local aur_pkgrel="$4"
|
||||
local hook="$package_dir/.omarchy/post-sync.sh"
|
||||
|
||||
[[ -f "$hook" ]] || return 1
|
||||
|
||||
print_info "Running Omarchy post-sync hook for $(display_package_name "$package_dir")..."
|
||||
if ! (
|
||||
cd "$package_dir"
|
||||
PACKAGE_NAME="$package" \
|
||||
AUR_PACKAGE_NAME="$aur_package" \
|
||||
AUR_PKGREL="$aur_pkgrel" \
|
||||
bash ".omarchy/post-sync.sh"
|
||||
); then
|
||||
print_error "Failed to run post-sync hook: $hook"
|
||||
return 2
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_pkgrel_suffix_if_customized() {
|
||||
local package_dir="$1"
|
||||
local aur_pkgrel="$2"
|
||||
|
||||
[[ -n "$aur_pkgrel" ]] || return 0
|
||||
|
||||
print_info "Applying Omarchy pkgrel suffix for $(display_package_name "$package_dir"): pkgrel=$aur_pkgrel.1"
|
||||
set_pkgrel "$package_dir" "$aur_pkgrel.1"
|
||||
}
|
||||
|
||||
apply_pkgrel_override() {
|
||||
local package_dir="$1"
|
||||
local aur_pkgrel="$2"
|
||||
local previous_pkgver="$3"
|
||||
local metadata="$package_dir/.omarchy/package.json"
|
||||
local pkgbuild="$package_dir/PKGBUILD"
|
||||
|
||||
[[ -f "$metadata" ]] || return 1
|
||||
jq -e 'has("pkgrel")' "$metadata" >/dev/null || return 1
|
||||
|
||||
local current_pkgver suffix offset base rel tmpfile
|
||||
# Read through the same accessor that produced previous_pkgver. Parsing it a
|
||||
# second time here let a quoted pkgver= compare unequal to itself, which threw
|
||||
# away the pkgrel metadata of an unchanged package on every sync.
|
||||
current_pkgver=$(get_pkgbuild_field "$package_dir" pkgver)
|
||||
|
||||
if [[ -n "$previous_pkgver" && "$current_pkgver" != "$previous_pkgver" ]]; then
|
||||
print_info "Removing stale pkgrel metadata for $(display_package_name "$package_dir") (pkgver changed: $previous_pkgver -> $current_pkgver)"
|
||||
tmpfile=$(mktemp)
|
||||
jq 'del(.pkgrel)' "$metadata" > "$tmpfile"
|
||||
mv "$tmpfile" "$metadata"
|
||||
return 1
|
||||
fi
|
||||
|
||||
suffix=$(jq -r '.pkgrel.suffix // 1' "$metadata")
|
||||
offset=$(jq -r '.pkgrel.offset // 0' "$metadata")
|
||||
|
||||
if [[ ! "$offset" =~ ^[0-9]+$ || ! "$aur_pkgrel" =~ ^[0-9]+$ ]]; then
|
||||
print_error "pkgrel offset requires numeric AUR pkgrel for $(display_package_name "$package_dir")"
|
||||
return 2
|
||||
fi
|
||||
|
||||
base=$((aur_pkgrel + offset))
|
||||
rel="$base.$suffix"
|
||||
|
||||
print_info "Applying pkgrel suffix for $(display_package_name "$package_dir"): AUR pkgrel=$aur_pkgrel, offset=$offset, suffix=$suffix -> pkgrel=$rel"
|
||||
set_pkgrel "$package_dir" "$rel"
|
||||
return 0
|
||||
}
|
||||
|
||||
clone_aur_package() {
|
||||
local aur_package="$1"
|
||||
local dest="$2"
|
||||
local clone_log="$TEMP_DIR/clone.log"
|
||||
local attempt
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
rm -rf "$dest"
|
||||
if git clone "https://aur.archlinux.org/${aur_package}.git" "$dest" >"$clone_log" 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
if [[ $attempt -lt 3 ]]; then
|
||||
print_warning "Clone of $aur_package failed (attempt $attempt/3), retrying in 10s..."
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
|
||||
print_warning "Failed to clone $aur_package after 3 attempts: $(tail -n 1 "$clone_log")"
|
||||
return 1
|
||||
}
|
||||
|
||||
sync_package() {
|
||||
local package="$1"
|
||||
local package_dir="$PKGBUILDS_DIR/$package"
|
||||
local metadata="$package_dir/.omarchy/package.json"
|
||||
|
||||
if [[ ! -f "$metadata" ]]; then
|
||||
if [[ "$SPECIFIC_MODE" == true ]]; then
|
||||
print_error "Package $package is missing .omarchy/package.json"
|
||||
((++FAILED))
|
||||
else
|
||||
print_warning "Skipping $package: missing .omarchy/package.json"
|
||||
((++SKIPPED))
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$(jq -r '.source // ""' "$metadata")" != "aur" ]]; then
|
||||
print_info "Skipping $package: source is not AUR"
|
||||
((++SKIPPED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$(jq -r 'if has("sync") then .sync else true end' "$metadata")" == "false" ]]; then
|
||||
print_info "Skipping $package: AUR sync disabled"
|
||||
((++SKIPPED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
local aur_package
|
||||
aur_package=$(jq -r --arg package "$package" '.aur // $package' "$metadata")
|
||||
|
||||
if [[ "$aur_package" == "$package" ]]; then
|
||||
print_info "Syncing $package from AUR..."
|
||||
else
|
||||
print_info "Syncing $package from AUR package $aur_package..."
|
||||
fi
|
||||
|
||||
cd "$TEMP_DIR"
|
||||
|
||||
if ! clone_aur_package "$aur_package" "$TEMP_DIR/$aur_package"; then
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TEMP_DIR/$aur_package/PKGBUILD" ]]; then
|
||||
print_warning "AUR repository for $aur_package is empty (package does not exist in AUR)"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
local aur_dir="$TEMP_DIR/$aur_package"
|
||||
local work_dir="$TEMP_DIR/${package}.work"
|
||||
local aur_pkgrel previous_pkgver upstream_commit
|
||||
aur_pkgrel=$(get_pkgbuild_field "$aur_dir" pkgrel)
|
||||
previous_pkgver=$(get_pkgbuild_field "$package_dir" pkgver || true)
|
||||
upstream_commit=$(git -C "$aur_dir" rev-parse HEAD)
|
||||
|
||||
rm -rf "$work_dir"
|
||||
copy_aur_contents "$aur_dir" "$work_dir"
|
||||
rm -rf "$work_dir/.omarchy"
|
||||
cp -a "$package_dir/.omarchy" "$work_dir/.omarchy"
|
||||
|
||||
local customized=false
|
||||
|
||||
if apply_omarchy_patches "$work_dir"; then
|
||||
customized=true
|
||||
else
|
||||
local patch_status=$?
|
||||
if [[ $patch_status -eq 2 ]]; then
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if run_omarchy_post_sync_hook "$work_dir" "$package" "$aur_package" "$aur_pkgrel"; then
|
||||
customized=true
|
||||
else
|
||||
local hook_status=$?
|
||||
if [[ $hook_status -eq 2 ]]; then
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local pkgrel_overridden=false
|
||||
set +e
|
||||
apply_pkgrel_override "$work_dir" "$aur_pkgrel" "$previous_pkgver"
|
||||
local pkgrel_status=$?
|
||||
set -e
|
||||
case "$pkgrel_status" in
|
||||
0) pkgrel_overridden=true ;;
|
||||
1) ;;
|
||||
*) ((++FAILED)); return 0 ;;
|
||||
esac
|
||||
|
||||
if [[ "$customized" == true && "$pkgrel_overridden" == false ]]; then
|
||||
apply_pkgrel_suffix_if_customized "$work_dir" "$aur_pkgrel"
|
||||
fi
|
||||
|
||||
remove_aur_only_files "$work_dir"
|
||||
|
||||
set_upstream_commit "$work_dir" "$upstream_commit"
|
||||
if ! commit_synced_worktree "$work_dir" "$package_dir"; then
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
SYNCED_PACKAGES+=("$package")
|
||||
((++SYNCED))
|
||||
}
|
||||
|
||||
if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then
|
||||
SPECIFIC_MODE=true
|
||||
for package in "${SPECIFIC_PACKAGES[@]}"; do
|
||||
sync_package "$package"
|
||||
done
|
||||
else
|
||||
while IFS= read -r package; do
|
||||
sync_package "$package"
|
||||
done < <(packages_for_aur_sync)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
print_error "Sync completed with failures"
|
||||
else
|
||||
print_success "Sync complete!"
|
||||
fi
|
||||
echo " Target: $PKGBUILDS_DIR"
|
||||
echo " Synced: $SYNCED"
|
||||
echo " Skipped: $SKIPPED"
|
||||
echo " Failed: $FAILED"
|
||||
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -9,6 +9,7 @@ source "$BUILD_ROOT/helpers/upstream-github.sh"
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
export UPSTREAM_CACHE_DIR="$TEMP_DIR/watch-cache"
|
||||
|
||||
SPECIFIC_PACKAGES=()
|
||||
|
||||
@@ -332,6 +333,12 @@ sync_package() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if jq -e '.sync == false' "$package_dir/.omarchy/package.json" >/dev/null 2>&1; then
|
||||
print_info "Skipping $package: upstream updates held by sync=false"
|
||||
((++SKIPPED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
local provider has_upstream=false
|
||||
provider=$(package_upstream_provider "$package_dir")
|
||||
if package_has_upstream_provider "$package_dir"; then
|
||||
@@ -372,6 +379,21 @@ sync_package() {
|
||||
|
||||
print_info "Checking $package for upstream releases..."
|
||||
|
||||
if [[ "$provider" == watch ]]; then
|
||||
local result
|
||||
if ! result=$(python3 "$BUILD_ROOT/helpers/upstream-watch.py" sync "$package_dir" --min-age "$min_age"); then
|
||||
print_error "Upstream watch failed for $package"
|
||||
((++FAILED))
|
||||
elif [[ $(jq -r .status <<<"$result") == updated ]]; then
|
||||
print_success " $(jq -r '.before + " -> " + .after' <<<"$result")"
|
||||
((++UPDATED))
|
||||
else
|
||||
print_info " $(jq -r '.reason' <<<"$result")"
|
||||
((++SKIPPED))
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
local release release_status=0
|
||||
if [[ -n "$provider" ]]; then
|
||||
case "$provider" in
|
||||
|
||||
+3
-1
@@ -129,6 +129,7 @@ RUN pacman -Syu --noconfirm && \
|
||||
wget \
|
||||
curl \
|
||||
jq \
|
||||
rclone \
|
||||
gnupg && \
|
||||
pacman -Scc --noconfirm && \
|
||||
rm -rf /var/cache/pacman/pkg/*
|
||||
@@ -146,7 +147,8 @@ RUN useradd -m -G wheel -s /bin/bash builder && \
|
||||
# be skipped at signing. Pin the extension so both architectures match.
|
||||
RUN sed -i 's/^#MAKEFLAGS=.*/MAKEFLAGS="-j$(nproc)"/' /etc/makepkg.conf && \
|
||||
sed -i 's/^COMPRESSZST=.*/COMPRESSZST=(zstd -c -z -q --threads=0 -)/' /etc/makepkg.conf && \
|
||||
sed -i "s|^PKGEXT=.*|PKGEXT='.pkg.tar.zst'|" /etc/makepkg.conf
|
||||
sed -i "s|^PKGEXT=.*|PKGEXT='.pkg.tar.zst'|" /etc/makepkg.conf && \
|
||||
sed -i 's|^#\?PACKAGER=.*|PACKAGER="Omarchy <pkgs@omarchy.org>"|' /etc/makepkg.conf
|
||||
|
||||
# Pacman wrapper that auto-resolves package conflicts (e.g. rustup vs rust).
|
||||
# makepkg only passes --noconfirm to pacman, which defaults to 'N' on conflict
|
||||
|
||||
+46
-5
@@ -26,6 +26,29 @@ DEFER_RUNTIME_DEPS=${DEFER_RUNTIME_DEPS:-false}
|
||||
|
||||
source "$HELPERS_DIR/package-metadata.sh"
|
||||
|
||||
# Where the channel's published database is read from for planning. On the
|
||||
# repository host it is the published tree itself. Anywhere else (a CI runner,
|
||||
# a fresh clone) that tree is absent, so the database is fetched from the
|
||||
# public channel and the same URL serves as pacman's dependency repository.
|
||||
# Set OMARCHY_PUBLISHED_REPO_URL= (empty) to disable the remote fallback.
|
||||
PUBLISHED_REPO_URL=${OMARCHY_PUBLISHED_REPO_URL-https://pkgs.omarchy.org}
|
||||
PUBLISHED_DB_DIR="$FINAL_OUTPUT_DIR"
|
||||
PUBLISHED_REPO_SERVER=""
|
||||
if [[ ! -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" && ! -f "$FINAL_OUTPUT_DIR/omarchy.db" && -n "$PUBLISHED_REPO_URL" ]]; then
|
||||
remote_channel="$PUBLISHED_REPO_URL/$MIRROR/$ARCH"
|
||||
remote_db_dir=$(mktemp -d /tmp/omarchy-published.XXXXXX) || exit 1
|
||||
# Cache-bust: the channel sits behind a CDN that serves a stale database
|
||||
# for a while after a sync.
|
||||
if curl -fsSL "$remote_channel/omarchy.db.tar.zst?$(date +%s)" -o "$remote_db_dir/omarchy.db.tar.zst"; then
|
||||
PUBLISHED_DB_DIR="$remote_db_dir"
|
||||
PUBLISHED_REPO_SERVER="$remote_channel"
|
||||
echo "==> No local published tree; planning against $remote_channel"
|
||||
else
|
||||
rm -rf "$remote_db_dir"
|
||||
echo "==> No local published tree and $remote_channel is unavailable; treating the channel as empty"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $DEFER_RUNTIME_DEPS != "false" && $DEFER_RUNTIME_DEPS != "true" ]]; then
|
||||
echo "DEFER_RUNTIME_DEPS must be true or false" >&2
|
||||
exit 1
|
||||
@@ -118,10 +141,15 @@ if [[ "$DRY_RUN" != true ]]; then
|
||||
fi
|
||||
touch "$BUILD_PLAN_DIR/repository-initialized" || exit 1
|
||||
|
||||
# Add omarchy repo if it has a database (stable packages)
|
||||
# Add omarchy repo if it has a database (stable packages). The local tree
|
||||
# is trusted as-is; the public channel is verified against the omarchy
|
||||
# keyring the image already carries.
|
||||
if [[ -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" ]] || [[ -f "$FINAL_OUTPUT_DIR/omarchy.db" ]]; then
|
||||
sudo sed -i "/^\[core\]$/i [omarchy]\nSigLevel = Optional TrustAll\nServer = file://$FINAL_OUTPUT_DIR\n" /etc/pacman.conf
|
||||
echo " -> omarchy (priority 2): $FINAL_OUTPUT_DIR"
|
||||
elif [[ -n "$PUBLISHED_REPO_SERVER" ]]; then
|
||||
sudo sed -i "/^\[core\]$/i [omarchy]\nSigLevel = Required DatabaseOptional\nServer = $PUBLISHED_REPO_SERVER\n" /etc/pacman.conf
|
||||
echo " -> omarchy (priority 2): $PUBLISHED_REPO_SERVER"
|
||||
fi
|
||||
|
||||
# Sync pacman database
|
||||
@@ -159,10 +187,10 @@ LOCAL_VERSION_CACHE_LOADED=false
|
||||
LOCAL_VERSION_CACHE_DB=""
|
||||
|
||||
load_local_versions() {
|
||||
local db="$FINAL_OUTPUT_DIR/omarchy.db.tar.zst"
|
||||
local db="$PUBLISHED_DB_DIR/omarchy.db.tar.zst"
|
||||
|
||||
if [[ ! -f "$db" ]]; then
|
||||
db="$FINAL_OUTPUT_DIR/omarchy.db"
|
||||
db="$PUBLISHED_DB_DIR/omarchy.db"
|
||||
fi
|
||||
|
||||
[[ -f "$db" ]] || return 0
|
||||
@@ -411,6 +439,11 @@ build_package() {
|
||||
ln -sf omarchy-build.db.tar.zst omarchy-build.db || return 1
|
||||
fi
|
||||
|
||||
# A release may publish successful builds even when a peer fails. Record
|
||||
# outputs only after this package's entire split build has completed.
|
||||
mkdir -p "$BUILD_PLAN_DIR/artifacts" || return 1
|
||||
printf '%s\n' "${new_pkgs[@]}" > "$BUILD_PLAN_DIR/artifacts/$pkg" || return 1
|
||||
|
||||
echo " Successfully built $pkg"
|
||||
return 0
|
||||
else
|
||||
@@ -526,9 +559,17 @@ check_needs_build() {
|
||||
|
||||
if [[ "$local_version" == "$pkgbuild_version" ]]; then
|
||||
return 1 # Already up to date
|
||||
else
|
||||
return 0 # Needs building
|
||||
fi
|
||||
|
||||
# Match check-versions: a retained archive is already published even when
|
||||
# the DB now indexes a newer release (for example, 4.0.4rc1 vs 4.0.3).
|
||||
# Rebuilding it would produce different bytes under an immutable filename.
|
||||
if package_version_is_published "$FINAL_OUTPUT_DIR" "$pkg" "$pkgbuild_version" "$ARCH"; then
|
||||
echo " + $pkg $pkgbuild_version - archive already published; skipping rebuild"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0 # Needs building
|
||||
}
|
||||
|
||||
# Collect packages that should be built for the selected mirror
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# CI spike: build PRs on ephemeral DigitalOcean droplets
|
||||
|
||||
Status: spike. Nothing here publishes. The repository host keeps building and
|
||||
signing on merge exactly as before.
|
||||
|
||||
## Pieces
|
||||
|
||||
- `.github/workflows/build-pr.yml` — on a PR touching `pkgbuilds/**`, one job
|
||||
per changed package on runners labelled `omarchy-builder`. 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 (ric1) per job up
|
||||
to `MAX_DROPLETS`, deletes droplets that are powered off or older than
|
||||
`MAX_AGE_MINUTES`. 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.
|
||||
|
||||
## Standing up the controller box
|
||||
|
||||
DIGITALOCEAN_TOKEN=<omarchy account> GITHUB_TOKEN=<fine-grained PAT> \
|
||||
REPO=omacom/omarchy-pkgs ci/controller-box/create.sh <branch>
|
||||
|
||||
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.
|
||||
|
||||
## What the spike proved (2026-09-17, fork ryanrhughes/omarchy-pkgs)
|
||||
|
||||
- `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.
|
||||
|
||||
## Not done (required before this touches the real repo)
|
||||
|
||||
- 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.
|
||||
|
||||
## Done since the spike README was first written
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Cleanup
|
||||
|
||||
doctl compute droplet list --tag-name omarchy-builder
|
||||
doctl compute droplet delete -f <id>
|
||||
@@ -0,0 +1,45 @@
|
||||
#cloud-config
|
||||
# The always-on controller droplet (smallest size is fine). Clones the repo
|
||||
# for ci/controller.sh, installs the unit and timer, and starts polling.
|
||||
#
|
||||
# Substitute before use:
|
||||
# __REPO_URL__ https://github.com/omacom/omarchy-pkgs.git
|
||||
# __BRANCH__ branch carrying ci/ (master once merged)
|
||||
# __ENV_B64__ base64 of a filled-in controller.env.example
|
||||
# __SSH_KEYS_JSON__ JSON array of public keys authorized for root
|
||||
package_update: true
|
||||
packages: [curl, jq, git]
|
||||
|
||||
# Root stays reachable by key so the journal can be read. Two things stand
|
||||
# in the way on DO images: disable_root rewrites root's keys into a stub, and
|
||||
# with no account ssh key attached DO expires root's password, which makes
|
||||
# sshd refuse every non-interactive session with "password change required".
|
||||
disable_root: false
|
||||
chpasswd:
|
||||
expire: false
|
||||
ssh_authorized_keys: __SSH_KEYS_JSON__
|
||||
|
||||
users:
|
||||
- name: controller
|
||||
shell: /bin/bash
|
||||
|
||||
write_files:
|
||||
# defer: write after the users module has created the controller group,
|
||||
# otherwise chown to root:controller fails and the unit cannot read this.
|
||||
- path: /etc/omarchy-controller.env
|
||||
permissions: "0640"
|
||||
owner: root:controller
|
||||
encoding: b64
|
||||
defer: true
|
||||
content: __ENV_B64__
|
||||
|
||||
runcmd:
|
||||
- chage -d "$(date +%F)" -M -1 root
|
||||
- chown root:controller /etc/omarchy-controller.env && chmod 0640 /etc/omarchy-controller.env
|
||||
- git clone --depth 1 --branch __BRANCH__ __REPO_URL__ /opt/omarchy-pkgs
|
||||
- mkdir -p /run/omarchy-controller && chown controller:controller /run/omarchy-controller
|
||||
- echo "d /run/omarchy-controller 0755 controller controller -" > /etc/tmpfiles.d/omarchy-controller.conf
|
||||
# runcmd is executed by /bin/sh: no brace expansion.
|
||||
- cp /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.service /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.timer /etc/systemd/system/
|
||||
- systemctl daemon-reload
|
||||
- systemctl enable --now omarchy-controller.timer
|
||||
@@ -0,0 +1,15 @@
|
||||
# /etc/omarchy-controller.env — mode 0600, owned by root, read by systemd.
|
||||
DIGITALOCEAN_TOKEN=dop_v1_...
|
||||
# Fine-grained PAT scoped to the repo: Actions: read, Administration: read+write
|
||||
GITHUB_TOKEN=github_pat_...
|
||||
REPO=omacom/omarchy-pkgs
|
||||
LABEL=omarchy-builder
|
||||
TAG=omarchy-builder
|
||||
REGION=ric1
|
||||
SIZE=g5-32vcpu-64gb-50gb
|
||||
MAX_DROPLETS=6
|
||||
MAX_AGE_MINUTES=200
|
||||
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=[]
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Create the controller droplet with plain curl. Run from a laptop, once.
|
||||
#
|
||||
# DIGITALOCEAN_TOKEN=... GITHUB_TOKEN=... ci/controller-box/create.sh [branch]
|
||||
#
|
||||
# The DO token given here is baked into the box's env file, so it must be the
|
||||
# token for the account that should pay for builder droplets.
|
||||
set -euo pipefail
|
||||
here=$(dirname "$0")
|
||||
: "${DIGITALOCEAN_TOKEN:?}" "${GITHUB_TOKEN:?}"
|
||||
REPO=${REPO:-omacom/omarchy-pkgs}
|
||||
BRANCH=${1:-master}
|
||||
REGION=${REGION:-ric1}
|
||||
NAME=${NAME:-omarchy-controller}
|
||||
# Optional DO ssh key ids as a JSON array, e.g. SSH_KEYS='[123]', for reading
|
||||
# the journal while bringing the box up. Not needed once it works.
|
||||
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}
|
||||
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; }
|
||||
|
||||
env_file=$(sed -e "s|^DIGITALOCEAN_TOKEN=.*|DIGITALOCEAN_TOKEN=$DIGITALOCEAN_TOKEN|" \
|
||||
-e "s|^GITHUB_TOKEN=.*|GITHUB_TOKEN=$GITHUB_TOKEN|" \
|
||||
-e "s|^REPO=.*|REPO=$REPO|" \
|
||||
-e "s|^SSH_KEYS_JSON=.*|SSH_KEYS_JSON=$ssh_keys_json|" "$here/controller.env.example")
|
||||
userdata=$(sed -e "s|__REPO_URL__|https://github.com/$REPO.git|" -e "s|__BRANCH__|$BRANCH|" \
|
||||
-e "s|__ENV_B64__|$(printf '%s\n' "$env_file" | base64 -w0)|" \
|
||||
-e "s|__SSH_KEYS_JSON__|$ssh_keys_json|" "$here/cloud-init.yaml")
|
||||
body=$(jq -n --arg name "$NAME" --arg region "$REGION" --arg ud "$userdata" --argjson keys "$SSH_KEYS" \
|
||||
'{name:$name, region:$region, size:"s-1vcpu-1gb", image:"ubuntu-24-04-x64", tags:["omarchy-controller"], user_data:$ud, ssh_keys:$keys}')
|
||||
|
||||
# Refuse to create a second one.
|
||||
existing=$(curl -fsS -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
|
||||
"https://api.digitalocean.com/v2/droplets?tag_name=omarchy-controller" | jq '.droplets | length')
|
||||
if (( existing > 0 )); then echo "a controller droplet already exists" >&2; exit 1; fi
|
||||
|
||||
curl -fsS -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" -H "Content-Type: application/json" \
|
||||
-X POST -d "$body" https://api.digitalocean.com/v2/droplets | jq -r '"created \(.droplet.name) id=\(.droplet.id)"'
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Provision ephemeral omarchy-builder runner droplets for queued jobs
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=controller
|
||||
EnvironmentFile=/etc/omarchy-controller.env
|
||||
ExecStart=/opt/omarchy-pkgs/ci/controller.sh
|
||||
# The reaper's safety net is time, not state; a hung tick must not hold the lock.
|
||||
TimeoutStartSec=240
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Run the omarchy-builder controller every minute
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnUnitActiveSec=1min
|
||||
AccuracySec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
# Droplet-per-job controller for the omarchy-builder runner pool.
|
||||
#
|
||||
# Run from a systemd timer every minute on a small always-on droplet. No
|
||||
# inbound endpoint: it polls GitHub for queued jobs wanting our label, creates
|
||||
# one ephemeral droplet per job (up to MAX_DROPLETS), and deletes droplets
|
||||
# that have powered off or exceeded MAX_AGE_MINUTES. The reaper does not
|
||||
# trust its own bookkeeping: it lists by tag and acts on what DigitalOcean
|
||||
# reports.
|
||||
#
|
||||
# Talks to both APIs with curl. No doctl: its saved contexts silently choose
|
||||
# an account; a token in the environment cannot. Needs curl and jq.
|
||||
#
|
||||
# Environment:
|
||||
# DIGITALOCEAN_TOKEN DO API token for the account that pays for droplets
|
||||
# GITHUB_TOKEN fine-grained PAT: Actions read, Administration write
|
||||
# REPO owner/name
|
||||
set -euo pipefail
|
||||
|
||||
REPO=${REPO:?owner/name}
|
||||
: "${DIGITALOCEAN_TOKEN:?}" "${GITHUB_TOKEN:?}"
|
||||
LABEL=${LABEL:-omarchy-builder}
|
||||
TAG=${TAG:-omarchy-builder}
|
||||
REGION=${REGION:-ric1}
|
||||
SIZE=${SIZE:-g5-32vcpu-64gb-50gb}
|
||||
IMAGE=${IMAGE:-ubuntu-24-04-x64}
|
||||
MAX_DROPLETS=${MAX_DROPLETS:-4}
|
||||
MAX_AGE_MINUTES=${MAX_AGE_MINUTES:-200}
|
||||
RUNNER_VERSION=${RUNNER_VERSION:-2.337.0}
|
||||
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:-[]}
|
||||
LOCK=${LOCK:-/tmp/omarchy-controller.lock}
|
||||
|
||||
log() { echo "$(date '+%F %T') $*"; }
|
||||
|
||||
# The only two places the outside world is touched. The self-test overrides
|
||||
# both, so every decision below is exercised against canned responses.
|
||||
do_api() { # do_api <path> [curl args...]
|
||||
local path=$1; shift
|
||||
curl -fsS -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
|
||||
-H "Content-Type: application/json" "https://api.digitalocean.com/v2/$path" "$@"
|
||||
}
|
||||
gh_api() { # gh_api <path> [curl args...]
|
||||
local path=$1; shift
|
||||
curl -fsS -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" "https://api.github.com/$path" "$@"
|
||||
}
|
||||
|
||||
# --- reap ------------------------------------------------------------------
|
||||
reap() {
|
||||
local now id status created age
|
||||
now=$(date +%s)
|
||||
while read -r id status created; do
|
||||
[[ -n "$id" ]] || continue
|
||||
age=$(( (now - $(date -d "$created" +%s)) / 60 ))
|
||||
if [[ $status == off ]] || (( age > MAX_AGE_MINUTES )); then
|
||||
log "deleting droplet $id (status=$status age=${age}m)"
|
||||
do_api "droplets/$id" -X DELETE
|
||||
fi
|
||||
done < <(do_api "droplets?tag_name=$TAG&per_page=200" |
|
||||
jq -r '.droplets[] | "\(.id) \(.status) \(.created_at)"')
|
||||
}
|
||||
|
||||
# --- demand ----------------------------------------------------------------
|
||||
queued_jobs() {
|
||||
local run
|
||||
gh_api "repos/$REPO/actions/runs?status=queued&per_page=50" --get \
|
||||
| jq -r '.workflow_runs[].id' |
|
||||
while read -r run; do
|
||||
gh_api "repos/$REPO/actions/runs/$run/jobs" \
|
||||
| jq -r --arg l "$LABEL" '.jobs[] | select(.status=="queued") | select(.labels | index($l)) | .id'
|
||||
done | wc -l
|
||||
}
|
||||
|
||||
live_droplets() {
|
||||
do_api "droplets?tag_name=$TAG&per_page=200" | jq '[.droplets[] | select(.status != "off")] | length'
|
||||
}
|
||||
|
||||
busy_runners() {
|
||||
gh_api "repos/$REPO/actions/runners?per_page=100" \
|
||||
| jq --arg l "$LABEL" '[.runners[] | select(.busy) | select(any(.labels[]; .name == $l))] | length'
|
||||
}
|
||||
|
||||
# --- create ----------------------------------------------------------------
|
||||
create_droplet() {
|
||||
local token userdata name body
|
||||
token=$(gh_api "repos/$REPO/actions/runners/registration-token" -X POST | jq -r .token)
|
||||
userdata=$(sed -e "s|__REPO__|$REPO|g" -e "s|__RUNNER_TOKEN__|$token|g" \
|
||||
-e "s|__RUNNER_LABELS__|$LABEL|g" -e "s|__RUNNER_VERSION__|$RUNNER_VERSION|g" \
|
||||
-e "s|__SSH_KEYS_JSON__|$SSH_KEYS_JSON|" "$CLOUD_INIT")
|
||||
name="$TAG-$(date +%s)-$RANDOM"
|
||||
body=$(jq -n --arg name "$name" --arg region "$REGION" --arg size "$SIZE" --arg image "$IMAGE" \
|
||||
--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)"
|
||||
do_api droplets -X POST -d "$body" | jq -r '"created droplet \(.droplet.id)"'
|
||||
}
|
||||
|
||||
controller_tick() {
|
||||
reap
|
||||
local queued live busy available need room
|
||||
queued=$(queued_jobs)
|
||||
live=$(live_droplets)
|
||||
busy=$(busy_runners)
|
||||
# A live droplet whose runner is busy is spoken for. Only droplets still
|
||||
# booting or listening can absorb a queued job.
|
||||
available=$(( live - busy )); (( available < 0 )) && available=0
|
||||
need=$(( queued - available ))
|
||||
(( need > 0 )) || return 0
|
||||
room=$(( MAX_DROPLETS - live ))
|
||||
(( need > room )) && need=$room
|
||||
if (( need <= 0 )); then
|
||||
log "at cap ($live/$MAX_DROPLETS, $busy busy) with $queued queued"
|
||||
return 0
|
||||
fi
|
||||
local i
|
||||
for (( i = 0; i < need; i++ )); do create_droplet; done
|
||||
}
|
||||
|
||||
if [[ "${CONTROLLER_LIBRARY_ONLY:-}" != 1 ]]; then
|
||||
exec 9>"$LOCK"; flock -n 9 || exit 0
|
||||
controller_tick
|
||||
fi
|
||||
@@ -0,0 +1,81 @@
|
||||
#cloud-config
|
||||
# Ephemeral GitHub Actions runner for omarchy-pkgs package builds.
|
||||
#
|
||||
# Boots an Ubuntu droplet, installs docker and the runner, registers ONCE with
|
||||
# --ephemeral, runs exactly one job, then powers off. The controller (or the
|
||||
# reaper) deletes the powered-off droplet. Nothing here holds a long-lived
|
||||
# credential: the registration token is single-use and expires in an hour.
|
||||
#
|
||||
# Substitute before use:
|
||||
# __REPO__ owner/name
|
||||
# __RUNNER_TOKEN__ registration token (gh api -X POST repos/O/R/actions/runners/registration-token)
|
||||
# __RUNNER_LABELS__ e.g. omarchy-builder
|
||||
# __RUNNER_VERSION__ e.g. 2.329.0
|
||||
|
||||
# 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__).
|
||||
disable_root: false
|
||||
chpasswd:
|
||||
expire: false
|
||||
ssh_authorized_keys: __SSH_KEYS_JSON__
|
||||
|
||||
package_update: true
|
||||
packages:
|
||||
- docker.io
|
||||
- docker-buildx
|
||||
- unzip
|
||||
- git
|
||||
- curl
|
||||
- jq
|
||||
- rsync
|
||||
|
||||
users:
|
||||
- name: runner
|
||||
groups: [docker]
|
||||
shell: /bin/bash
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
|
||||
write_files:
|
||||
# defer: write after users/groups exist, so /home/runner is created by
|
||||
# useradd (owned by runner) rather than by this module as root.
|
||||
- path: /home/runner/start.sh
|
||||
permissions: "0755"
|
||||
owner: runner:runner
|
||||
defer: true
|
||||
content: |
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/runner
|
||||
mkdir -p actions-runner && cd actions-runner
|
||||
arch=$(uname -m); [[ $arch == x86_64 ]] && arch=x64
|
||||
curl -fsSL -o runner.tgz \
|
||||
"https://github.com/actions/runner/releases/download/v__RUNNER_VERSION__/actions-runner-linux-${arch}-__RUNNER_VERSION__.tar.gz"
|
||||
tar xzf runner.tgz && rm runner.tgz
|
||||
./config.sh --unattended --ephemeral \
|
||||
--url "https://github.com/__REPO__" \
|
||||
--token "__RUNNER_TOKEN__" \
|
||||
--name "do-$(hostname)" \
|
||||
--labels "__RUNNER_LABELS__" \
|
||||
--replace
|
||||
./run.sh
|
||||
# One job done. Power off; the controller deletes powered-off droplets.
|
||||
sudo poweroff
|
||||
|
||||
runcmd:
|
||||
# With no account ssh key attached, DO expires root's password, and sshd
|
||||
# then refuses every non-interactive session. Clear it first so operators
|
||||
# can read the logs of a builder that never registers.
|
||||
- chage -d "$(date +%F)" -M -1 root
|
||||
- systemctl enable --now docker
|
||||
# aarch64 builds run under user-mode emulation (DO has no arm droplets).
|
||||
# Register QEMU with the F and C flags via tonistiigi/binfmt, exactly as
|
||||
# helpers/docker-helpers.sh setup_qemu does. Ubuntu's qemu-user-static
|
||||
# registers without C, so sudo inside the emulated container fails with
|
||||
# "effective uid is not 0"; multiarch/qemu-user-static is abandoned at QEMU
|
||||
# 7.2, under which qmake's compiler probe returns nothing on current gcc
|
||||
# ("failed to parse default include paths", PR #517). Pin the emulator
|
||||
# version: the tag is the only thing that decides what every aarch64 build
|
||||
# runs under. Best-effort: an x86-only job never needs it.
|
||||
- docker run --rm --privileged docker.io/tonistiigi/binfmt:qemu-v10.2.3-68 --uninstall qemu-aarch64 --install arm64 || true
|
||||
- chown -R runner:runner /home/runner
|
||||
- sudo -u runner /home/runner/start.sh > /home/runner/runner.log 2>&1
|
||||
@@ -0,0 +1,182 @@
|
||||
# Direct upstream watches
|
||||
|
||||
Omarchy owns the recipes in `pkgbuilds/`. `bin/sync-upstream` discovers new
|
||||
releases directly from project/vendor feeds and updates versions, declared
|
||||
release variables, and the source checksums already used by the recipe. It never
|
||||
imports upstream PKGBUILDs or runs downloaded build scripts. Architecture support,
|
||||
root install hooks, dependencies, and build functions remain ours to maintain.
|
||||
|
||||
`upstream.watch` complements the existing declarative providers and custom hooks:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/walker",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Watch fields
|
||||
|
||||
Choose exactly one provider: `github` (owner/repository), `git_tags` (repository
|
||||
HTTPS URL), `git_branch` (repository URL plus explicit `branch`), `npm` or `pypi`
|
||||
(package name), `debian` (Packages index plus exact `package`), `json` (URL plus
|
||||
version `path`), `regex` (text URL), `redirect` (final HTTPS download URL), or
|
||||
`archive` (inspect archive metadata without extracting/executing code).
|
||||
|
||||
Tag, text, redirect and archive watches use an explicit `pattern` with a named
|
||||
`version` capture. Tag patterns match the entire tag. `version` optionally formats
|
||||
those captures into an Arch pkgver; e.g. Sublime uses `4.{version}`. JSON feeds can
|
||||
expose additional capture values through `fields`, a name-to-JSON-path map.
|
||||
|
||||
`variables` maps recipe scalars such as `_commit` or `_build` to capture templates.
|
||||
Only explicitly declared underscore-prefixed variables can change. GitHub
|
||||
`{commit}` resolves the selected tag, not a moving target_commitish branch.
|
||||
`submodules` can map a recipe variable to a gitlink in the selected GitHub tag;
|
||||
RustDesk uses this for hbb_common. Downloaded repository code is never evaluated.
|
||||
|
||||
For upstreams that rebuild a release, declare a numeric `revision` template and
|
||||
its `revision_variable`. With unchanged pkgver, only an increasing revision can
|
||||
advance that variable, and the downstream pkgrel increments instead of resetting.
|
||||
Cursor CLI uses `sequence` to preserve its date/counter/hash version convention
|
||||
when the vendor publishes a second hash on the same day. A new pkgver resets
|
||||
pkgrel to 1, but the complete epoch:pkgver-pkgrel must still increase.
|
||||
|
||||
GitHub releases exclude drafts and prereleases unless `allow_prerelease` is true.
|
||||
Existing `min_release_age` policies apply: a feed without a verifiable publication
|
||||
time cannot bypass a configured hold. Git branch watches derive a commit count
|
||||
and date from the actual branch history and write an immutable source pin.
|
||||
|
||||
Checksums retain their algorithms (SHA256, SHA512, BLAKE2, etc.) and source order.
|
||||
Changed git sources are hashed with makepkg's git-archive convention. Unchanged
|
||||
sources retain their hashes; `mutable_sources` explicitly names entries such as
|
||||
`source:0` that must be fetched again for a new version despite a stable URL.
|
||||
Existing `SKIP` entries remain unchanged (including signed metadata verified by
|
||||
the recipe); new skips are never introduced. Changed URLs are still fetched.
|
||||
A matching GitHub release asset SHA256 digest avoids downloading large assets.
|
||||
Missing architecture artifacts or malformed metadata fail the package atomically.
|
||||
Every declared architecture must read back the same release and checksum values.
|
||||
|
||||
Archive watches use `member` to select a text member, or `filenames: true` to read
|
||||
versions from archive member names. Debian archives are read through their control
|
||||
metadata. `unescape_json` handles JSON strings embedded in a vendor's HTML page.
|
||||
|
||||
## Maintenance and validation
|
||||
|
||||
Running watches locally requires Python 3.11+, Bash, curl, git, jq, Arch's
|
||||
`vercmp`, and `bsdtar`. CI installs these in its Arch container.
|
||||
|
||||
- Edit packaging and architecture changes directly in PKGBUILD. The old AUR
|
||||
overlays have been folded into these recipes and removed.
|
||||
- Keep source-code patches and install hooks checked in as ordinary package files.
|
||||
- Bump pkgrel when changing a recipe at the same version. Removing a dotted AUR
|
||||
suffix must never lower the complete version.
|
||||
- Add a watch with each new package. `bin/add-package --source aur` is a one-time
|
||||
import; it records historical `origin` metadata and leaves an owned recipe.
|
||||
- `python helpers/upstream-watch.py check pkgbuilds/NAME` checks release discovery
|
||||
without rewriting the recipe. `bin/sync-upstream NAME` performs the update.
|
||||
- `python tests/upstream-watch.py` tests update atomicity, architecture coverage,
|
||||
version ordering, source hashes and hostile metadata using offline fixtures.
|
||||
|
||||
The scheduled workflow continues reviewing completed updates if another package
|
||||
fails. The failing recipe stays unchanged and the run still reports failure.
|
||||
|
||||
## Migrated package watches
|
||||
|
||||
68 active AUR packages now use direct watches. The nine previously disabled
|
||||
packages retain manual maintenance holds. Historical AUR provenance is recorded
|
||||
in `origin` and has no effect on release selection.
|
||||
|
||||
| Package | Provider | Upstream |
|
||||
|---|---|---|
|
||||
| `1password-beta` | debian | [https://downloads.1password.com/linux/debian/amd64/dists/beta/main/binary-amd64/Packages](https://downloads.1password.com/linux/debian/amd64/dists/beta/main/binary-amd64/Packages) |
|
||||
| `1password-cli` | json | [https://app-updates.agilebits.com/check/1/0/CLI2/en/0](https://app-updates.agilebits.com/check/1/0/CLI2/en/0) |
|
||||
| `aether` | github | [omacom/aether](https://github.com/omacom/aether) |
|
||||
| `asusctl` | git_tags | [https://github.com/OpenGamingCollective/asusctl.git](https://github.com/OpenGamingCollective/asusctl.git) |
|
||||
| `basecamp-cli` | github | [basecamp/basecamp-cli](https://github.com/basecamp/basecamp-cli) |
|
||||
| `bun-bin` | github | [oven-sh/bun](https://github.com/oven-sh/bun) |
|
||||
| `claude-code` | regex | [https://downloads.claude.ai/claude-code-releases/latest](https://downloads.claude.ai/claude-code-releases/latest) |
|
||||
| `cliamp` | github | [bjarneo/cliamp](https://github.com/bjarneo/cliamp) |
|
||||
| `crush-bin` | github | [charmbracelet/crush](https://github.com/charmbracelet/crush) |
|
||||
| `cursor-bin` | json | [https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable](https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable) |
|
||||
| `cursor-cli` | regex | [https://cursor.com/install](https://cursor.com/install) |
|
||||
| `dbxcli-bin` | github | [dropbox/dbxcli](https://github.com/dropbox/dbxcli) |
|
||||
| `dropbox` | redirect | [https://www.dropbox.com/download?plat=lnx.x86_64](https://www.dropbox.com/download?plat=lnx.x86_64) |
|
||||
| `dropbox-cli` | regex | [https://linux.dropbox.com/packages/](https://linux.dropbox.com/packages/) |
|
||||
| `elephant` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-all` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-archlinuxpkgs` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-bluetooth` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-calc` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-clipboard` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-desktopapplications` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-files` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-menus` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-providerlist` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-runner` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-symbols` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-todo` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-unicode` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `elephant-websearch` | github | [abenz1267/elephant](https://github.com/abenz1267/elephant) |
|
||||
| `heroic-games-launcher-bin` | github | [Heroic-Games-Launcher/HeroicGamesLauncher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) |
|
||||
| `hyprshade` | pypi | [hyprshade](https://pypi.org/project/hyprshade/) |
|
||||
| `lib32-nvidia-580xx-utils` | regex | [https://download.nvidia.com/XFree86/Linux-x86_64/](https://download.nvidia.com/XFree86/Linux-x86_64/) |
|
||||
| `limine-mkinitcpio-hook` | git_tags | [https://gitlab.com/Zesko/limine-entry-tool.git](https://gitlab.com/Zesko/limine-entry-tool.git) |
|
||||
| `limine-snapper-sync` | git_tags | [https://gitlab.com/Zesko/limine-snapper-sync.git](https://gitlab.com/Zesko/limine-snapper-sync.git) |
|
||||
| `lmstudio-bin` | regex | [https://lmstudio.ai/download](https://lmstudio.ai/download) |
|
||||
| `localsend` | github | [localsend/localsend](https://github.com/localsend/localsend) |
|
||||
| `localsend-bin` | github | [localsend/localsend](https://github.com/localsend/localsend) |
|
||||
| `macbook12-spi-driver-dkms` | git_branch | [https://github.com/marc-git/macbook12-spi-driver.git](https://github.com/marc-git/macbook12-spi-driver.git) |
|
||||
| `makima-bin` | github | [cyber-sushi/makima](https://github.com/cyber-sushi/makima) |
|
||||
| `minecraft-launcher` | archive | [https://launcher.mojang.com/download/Minecraft.deb](https://launcher.mojang.com/download/Minecraft.deb) |
|
||||
| `nautilus-dropbox` | github | [dropbox/nautilus-dropbox](https://github.com/dropbox/nautilus-dropbox) |
|
||||
| `nautilus-open-any-terminal` | git_tags | [https://github.com/Stunkymonkey/nautilus-open-any-terminal.git](https://github.com/Stunkymonkey/nautilus-open-any-terminal.git) |
|
||||
| `nordvpn-bin` | debian | [https://repo.nordvpn.com/deb/nordvpn/debian/dists/stable/main/binary-amd64/Packages](https://repo.nordvpn.com/deb/nordvpn/debian/dists/stable/main/binary-amd64/Packages) |
|
||||
| `nvidia-580xx-utils` | regex | [https://download.nvidia.com/XFree86/Linux-x86_64/](https://download.nvidia.com/XFree86/Linux-x86_64/) |
|
||||
| `omarchy-chromium-bin` | github | [omacom/omarchy-chromium](https://github.com/omacom/omarchy-chromium) |
|
||||
| `omarchy-emacs` | git_tags | [https://github.com/scottjones/omarchy-emacs.git](https://github.com/scottjones/omarchy-emacs.git) |
|
||||
| `omazed` | git_tags | [https://github.com/aps6/omazed.git](https://github.com/aps6/omazed.git) |
|
||||
| `once-bin` | github | [basecamp/once](https://github.com/basecamp/once) |
|
||||
| `openai-codex-bin` | github | [openai/codex](https://github.com/openai/codex) |
|
||||
| `python-mediapipe` | github | [google-ai-edge/mediapipe](https://github.com/google-ai-edge/mediapipe) |
|
||||
| `python-sounddevice` | pypi | [sounddevice](https://pypi.org/project/sounddevice/) |
|
||||
| `python-terminaltexteffects` | pypi | [terminaltexteffects](https://pypi.org/project/terminaltexteffects/) |
|
||||
| `rustdesk` | github | [rustdesk/rustdesk](https://github.com/rustdesk/rustdesk) |
|
||||
| `spotify` | debian | [https://repository.spotify.com/dists/testing/non-free/binary-amd64/Packages](https://repository.spotify.com/dists/testing/non-free/binary-amd64/Packages) |
|
||||
| `sublime-text-4` | json | [https://www.sublimetext.com/updates/4/stable_update_check](https://www.sublimetext.com/updates/4/stable_update_check) |
|
||||
| `sunshine` | github | [LizardByte/Sunshine](https://github.com/LizardByte/Sunshine) |
|
||||
| `ttf-ia-writer` | git_branch | [https://github.com/iaolo/iA-Fonts.git](https://github.com/iaolo/iA-Fonts.git) |
|
||||
| `tuxedo-drivers-nocompatcheck-dkms` | git_tags | [https://gitlab.com/kronerm/tuxedo-drivers-nocompatcheck.git](https://gitlab.com/kronerm/tuxedo-drivers-nocompatcheck.git) |
|
||||
| `typora` | debian | [https://downloads.typora.io/linux/Packages](https://downloads.typora.io/linux/Packages) |
|
||||
| `ufw-docker` | git_tags | [https://github.com/chaifeng/ufw-docker.git](https://github.com/chaifeng/ufw-docker.git) |
|
||||
| `vi` | regex | [https://sources.archlinux.org/other/vi/](https://sources.archlinux.org/other/vi/) |
|
||||
| `visual-studio-code-bin` | json | [https://update.code.visualstudio.com/api/update/linux-deb-x64/stable/latest](https://update.code.visualstudio.com/api/update/linux-deb-x64/stable/latest) |
|
||||
| `walker` | github | [abenz1267/walker](https://github.com/abenz1267/walker) |
|
||||
| `xdg-terminal-exec` | git_tags | [https://gitlab.freedesktop.org/Vladimir-csp/xdg-terminal-exec.git](https://gitlab.freedesktop.org/Vladimir-csp/xdg-terminal-exec.git) |
|
||||
| `xpadneo-dkms` | github | [atar-axis/xpadneo](https://github.com/atar-axis/xpadneo) |
|
||||
| `yaru-icon-theme` | git_tags | [https://github.com/ubuntu/yaru.git](https://github.com/ubuntu/yaru.git) |
|
||||
| `yay` | github | [Jguer/yay](https://github.com/Jguer/yay) |
|
||||
| `yt6801-dkms` | archive | [https://www.motor-comm.com/Cn/Skippower/downloadFile.html?id=1817](https://www.motor-comm.com/Cn/Skippower/downloadFile.html?id=1817) |
|
||||
|
||||
## Existing manual holds
|
||||
|
||||
`grok-bot`, `libfprint-git`, `libretro-cap32-git`, `libretro-database-git`, `libretro-fbneo-git`, `libretro-uae-git`, `libretro-vice-git`, `quickshell-git`, `supergfxctl`.
|
||||
|
||||
These packages were already excluded from automatic AUR updates. The migration preserves that policy.
|
||||
|
||||
`linux-firmware-cirrus` is a deliberate hold: a self-retiring shim that ships Arch's linux-firmware-cirrus 20260910-2 payload to stable while stable's Arch snapshot is on 20260810-2 (Dell XPS 13 DX13260 / 1028:0e54 speaker firmware). It is versioned 20260810-3 so the genuine Arch package supersedes it as soon as the snapshot advances; bumping it to the Arch version would defeat that. Delete the recipe once stable's snapshot carries linux-firmware >= 20260910.
|
||||
|
||||
`m1n1-aurora` and `uboot-asahi` are deliberate holds: Apple Silicon boot code, pinned by hand like `linux-aurora`, and bumped only after a cold boot on the qualification Macs. `m1n1-aurora` pins an aurora-silicon/m1n1 commit plus a local patch. `uboot-asahi` follows asahi-alarm's recipe and patch set (asahi-alarm/PKGBUILDs), which a tag watch on AsahiLinux/u-boot cannot carry.
|
||||
|
||||
## Package-specific boundaries
|
||||
|
||||
- NVIDIA watches remain on the 580 driver branch.
|
||||
- Hardware-specific packages keep their declared architectures; this migration does not invent ARM binaries for x86-only upstreams.
|
||||
- iA Duospace was deleted upstream. Its four legacy font files retain their original immutable pin while the other families track the current repository.
|
||||
- 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.
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Package files cross from a PR build to publish.yml as one GitHub Actions
|
||||
# artifact. actions/upload-artifact rejects any path containing ':', and a
|
||||
# package with an epoch is named `name-1:ver-rel-arch.pkg.tar.zst` by
|
||||
# makepkg. So the files ride inside a tar with a plain name and keep their
|
||||
# own names untouched: pacman clients and bin/publish-artifact both rely on
|
||||
# the filename matching PKGINFO.
|
||||
#
|
||||
# Both functions run under the workflow's `bash -e`: nothing in them may
|
||||
# return non-zero except the final failure.
|
||||
|
||||
# package_files <dir>: the *.pkg.tar.zst directly in <dir>, one per line.
|
||||
# Signatures and the scratch database next to them are not packages.
|
||||
package_files() {
|
||||
local f
|
||||
for f in "$1"/*.pkg.tar.zst; do
|
||||
[[ -e "$f" ]] && printf '%s\n' "$f"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# pack_packages <dir> <tar>: every package in <dir> into <tar>.
|
||||
pack_packages() {
|
||||
local dir=$1 out=$2 files=()
|
||||
mapfile -t files < <(package_files "$dir")
|
||||
(( ${#files[@]} )) || { echo "pack_packages: no *.pkg.tar.zst in $dir" >&2; return 1; }
|
||||
tar -cf "$out" -C "$dir" -- "${files[@]##*/}"
|
||||
}
|
||||
|
||||
# unpack_packages <artifact dir> <dest>: the packages an unzipped artifact
|
||||
# carried, into <dest>. Packed artifacts hold packages.tar; artifacts from
|
||||
# builds before packing hold the bare files. The bare form can go once
|
||||
# those artifacts have expired (7-day retention).
|
||||
unpack_packages() {
|
||||
local src=$1 dest=$2 files=()
|
||||
mkdir -p "$dest"
|
||||
if [[ -f "$src/packages.tar" ]]; then
|
||||
tar -xf "$src/packages.tar" -C "$dest"
|
||||
return 0
|
||||
fi
|
||||
mapfile -t files < <(package_files "$src")
|
||||
(( ${#files[@]} )) || { echo "unpack_packages: nothing to unpack in $src" >&2; return 1; }
|
||||
cp -- "${files[@]}" "$dest/"
|
||||
}
|
||||
@@ -91,8 +91,19 @@ setup_qemu() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Register emulators for builds whose target differs from the host.
|
||||
if ! "$CONTAINER_ENGINE" run --rm --privileged docker.io/multiarch/qemu-user-static --reset -p yes --credential yes >/dev/null 2>&1; then
|
||||
# Register emulators for builds whose target differs from the host, with
|
||||
# the F and C flags (tonistiigi/binfmt always sets both). The image tag pins
|
||||
# the QEMU version every emulated build runs under; multiarch/qemu-user-static
|
||||
# stopped at QEMU 7.2, which breaks qmake's compiler probe on current gcc.
|
||||
# Keep ci/runner-cloud-init.yaml on the same tag. Uninstall first: install
|
||||
# leaves an existing registration (an older emulator) in place and exits 0.
|
||||
local platform_arch
|
||||
case "$target_arch" in
|
||||
aarch64) platform_arch=arm64 ;;
|
||||
x86_64) platform_arch=amd64 ;;
|
||||
*) platform_arch="$target_arch" ;;
|
||||
esac
|
||||
if ! "$CONTAINER_ENGINE" run --rm --privileged docker.io/tonistiigi/binfmt:qemu-v10.2.3-68 --uninstall "qemu-$target_arch" --install "$platform_arch" >/dev/null 2>&1; then
|
||||
print_error "Failed to set up QEMU emulation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
+27
-12
@@ -3,15 +3,13 @@
|
||||
# Expects package directories in $PKGBUILDS_DIR, each with:
|
||||
# .omarchy/package.json
|
||||
#
|
||||
# Minimal schema:
|
||||
# { "source": "aur" }
|
||||
# { "source": "aur", "sync": false }
|
||||
# { "source": "aur", "aur": "different-aur-name" }
|
||||
# { "source": "aur", "release_ring": "fast" }
|
||||
# { "source": "aur", "skip_build": true }
|
||||
# { "source": "aur", "pkgrel": { "suffix": 1, "offset": 1 } }
|
||||
# { "source": "aur", "rebuild_on": ["qt6-base"] }
|
||||
# Minimal schema (legacy source:aur remains readable for initial imports):
|
||||
# { "source": "local" }
|
||||
# { "source": "local", "sync": false }
|
||||
# { "source": "local", "release_ring": "fast" }
|
||||
# { "source": "local", "skip_build": true }
|
||||
# { "source": "local", "rebuild_on": ["qt6-base"] }
|
||||
# { "source": "local", "upstream": { "watch": { "github": "owner/repo", "pattern": "v(?P<version>[0-9.]+)" } } }
|
||||
# { "source": "local", "channels": ["edge"] }
|
||||
# { "source": "local", "channels": ["edge", "rc", "stable"] }
|
||||
# { "source": "local", "min_release_age": "24h" }
|
||||
@@ -21,7 +19,7 @@
|
||||
# { "source": "local", "upstream": { "npm": "@scope/package", "sources": { "any": ["{npm_tarball}"] } } }
|
||||
# { "source": "local", "upstream": { "debian": "https://example/debian/dists/stable/main/binary-amd64/Packages", "package": "example", "sources": { "any": ["https://example/releases/{pkgver}.tar.gz"] } } }
|
||||
#
|
||||
# bin/sync-aur also writes upstream_commit for AUR-backed packages, and
|
||||
# bin/import-aur records historical origin.aur and origin.commit;
|
||||
# bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on.
|
||||
|
||||
if [[ -z "${PKGBUILDS_DIR:-}" ]]; then
|
||||
@@ -193,6 +191,18 @@ package_supports_arch() {
|
||||
esac
|
||||
}
|
||||
|
||||
# The channel DB indexes only its newest version, but older published archives
|
||||
# remain immutable. Both the scheduler and build planner must skip an existing
|
||||
# filename even when the checkout differs from the version currently indexed.
|
||||
package_version_is_published() {
|
||||
local repo_dir="$1" package="$2" version="$3" target="$4" path
|
||||
for path in "$repo_dir/$package-$version-$target.pkg.tar."* \
|
||||
"$repo_dir/$package-$version-any.pkg.tar."*; do
|
||||
[[ -f "$path" && "$path" != *.sig ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Channel membership: where a package may be published. Packages without a
|
||||
# `channels` key are members of every channel (they flow edge -> rc -> stable).
|
||||
package_has_channels() {
|
||||
@@ -523,8 +533,9 @@ validate_package_metadata() {
|
||||
if has("upstream") | not then true
|
||||
elif (.upstream | type) != "object" then false
|
||||
else .upstream |
|
||||
([has("github"), has("git_tags"), has("npm"), has("debian")] | map(select(.)) | length) == 1
|
||||
and if has("github") then
|
||||
([has("github"), has("git_tags"), has("npm"), has("debian"), has("watch")] | map(select(.)) | length) == 1
|
||||
and if has("watch") then (.watch | type == "object")
|
||||
elif has("github") then
|
||||
(.github | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z"))
|
||||
and (if has("checksums") then (.checksums | type == "string" and length > 0) else true end)
|
||||
and (if has("digests") then (.digests | type == "boolean") else true end)
|
||||
@@ -550,10 +561,14 @@ validate_package_metadata() {
|
||||
end
|
||||
end
|
||||
' "$metadata" >/dev/null; then
|
||||
echo "invalid upstream for $(basename "$pkgdir"): configure exactly one valid github, git_tags, npm, or debian provider"
|
||||
echo "invalid upstream for $(basename "$pkgdir"): configure exactly one valid github, git_tags, npm, debian, or watch provider"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if jq -e '.upstream? | objects | has("watch")' "$metadata" >/dev/null; then
|
||||
python3 "${BASH_SOURCE[0]%/*}/upstream-watch.py" validate "$pkgdir" || return 1
|
||||
fi
|
||||
|
||||
pkgrel_type=$(jq -r 'if has("pkgrel") then .pkgrel | type else "missing" end' "$metadata")
|
||||
case "$pkgrel_type" in
|
||||
object|missing) ;;
|
||||
|
||||
@@ -31,7 +31,7 @@ package_upstream_provider() {
|
||||
metadata=$(metadata_file_for_dir "$pkgdir")
|
||||
jq -r '
|
||||
(.upstream? | objects) as $u
|
||||
| [$u | keys[] | select(. == "github" or . == "git_tags" or . == "npm" or . == "debian")]
|
||||
| [$u | keys[] | select(. == "github" or . == "git_tags" or . == "npm" or . == "debian" or . == "watch")]
|
||||
| if length == 1 then .[0] else "" end
|
||||
' "$metadata"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Discover releases and update our own recipes; never import upstream build code.
|
||||
|
||||
The watch selects release metadata. Sources, supported architectures, integrity
|
||||
algorithms and packaging behavior stay in the checked-in PKGBUILD. Only release
|
||||
scalars and checksum arrays are replaced, atomically, after every source passes.
|
||||
"""
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
from urllib.parse import quote, urlsplit
|
||||
import zipfile
|
||||
|
||||
PROVIDERS = {"github", "git_tags", "git_branch", "npm", "pypi", "debian", "json", "regex", "archive", "redirect"}
|
||||
VERSION = re.compile(r"[A-Za-z0-9][A-Za-z0-9._+]*\Z")
|
||||
SCALAR = re.compile(r"[A-Za-z0-9._+/-]+\Z")
|
||||
SUM = re.compile(r"(md5|sha1|sha224|sha256|sha384|sha512|b2)sums(_[a-z0-9_]+)?\Z")
|
||||
HASHES = {"b2": "blake2b"}
|
||||
|
||||
|
||||
def run(args, **kwargs):
|
||||
return subprocess.check_output(args, **kwargs)
|
||||
|
||||
|
||||
def vercmp(a, b):
|
||||
return int(run(["vercmp", a, b], text=True).strip())
|
||||
|
||||
|
||||
def https(url):
|
||||
parts = urlsplit(url)
|
||||
if parts.scheme != "https" or not parts.hostname or parts.username or parts.password or re.search(r"[\s\x00-\x1f]", url):
|
||||
raise ValueError(f"expected an HTTPS upstream URL: {url!r}")
|
||||
return url
|
||||
|
||||
|
||||
class Fetcher:
|
||||
def __init__(self, cache):
|
||||
self.cache = Path(cache)
|
||||
self.cache.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def file(self, url):
|
||||
https(url)
|
||||
dest = self.cache / hashlib.sha256(url.encode()).hexdigest()
|
||||
if not dest.exists():
|
||||
scratch = dest.with_suffix(f".{os.getpid()}.tmp")
|
||||
command = ["curl", "--proto", "=https", "--proto-redir", "=https", "-fsSL",
|
||||
"--connect-timeout", "20", "--max-time", "300", "--retry", "2", "-o", str(scratch), url]
|
||||
# Credentials only go to GitHub's API, never to release assets or vendors.
|
||||
token = os.environ.get("UPSTREAM_GITHUB_TOKEN")
|
||||
if token and urlsplit(url).hostname == "api.github.com":
|
||||
command[1:1] = ["--config", "-"]
|
||||
subprocess.run(command, input=f'header = "Authorization: Bearer {token}"\n', text=True, check=True)
|
||||
else:
|
||||
subprocess.run(command, check=True)
|
||||
scratch.replace(dest)
|
||||
return dest
|
||||
|
||||
def text(self, url):
|
||||
data = self.file(url).read_bytes()
|
||||
if data.startswith(b"\x1f\x8b"):
|
||||
data = gzip.decompress(data)
|
||||
return data.decode()
|
||||
|
||||
def json(self, url):
|
||||
return json.loads(self.text(url))
|
||||
|
||||
|
||||
def validate(watch):
|
||||
if not isinstance(watch, dict) or len(PROVIDERS & watch.keys()) != 1:
|
||||
raise ValueError("watch must select exactly one release provider")
|
||||
provider = next(iter(PROVIDERS & watch.keys()))
|
||||
allowed = PROVIDERS | {"pattern", "path", "package", "branch", "variables", "fields",
|
||||
"submodules", "allow_prerelease", "unescape_json", "filenames",
|
||||
"sequence", "version", "revision", "revision_variable",
|
||||
"mutable_sources", "member", "dist_tag"}
|
||||
if watch.keys() - allowed:
|
||||
raise ValueError(f"unknown watch fields: {sorted(watch.keys() - allowed)}")
|
||||
value = watch[provider]
|
||||
if not isinstance(value, str) or not value:
|
||||
raise ValueError(f"invalid watch.{provider}")
|
||||
if provider == "github":
|
||||
if not re.fullmatch(r"[\w.-]+/[\w.-]+", value):
|
||||
raise ValueError("invalid GitHub repository")
|
||||
elif provider in {"npm", "pypi"}:
|
||||
if not re.fullmatch(r"(?:@[\w.-]+/)?[\w.-]+", value):
|
||||
raise ValueError("invalid registry package")
|
||||
else:
|
||||
https(value)
|
||||
if "pattern" in watch or provider in {"github", "git_tags", "regex", "archive", "redirect"}:
|
||||
if not isinstance(watch.get("pattern"), str):
|
||||
raise ValueError("watch needs an explicit release pattern")
|
||||
pattern = re.compile(watch["pattern"])
|
||||
if "version" not in pattern.groupindex:
|
||||
raise ValueError("release pattern needs a named version group")
|
||||
if provider == "json" and (not isinstance(watch.get("path"), str) or not watch["path"]):
|
||||
raise ValueError("JSON watch needs a version path")
|
||||
if provider == "debian":
|
||||
name = watch.get("package", "")
|
||||
if not isinstance(name, str) or not re.fullmatch(r"[a-z0-9][a-z0-9+.-]*", name):
|
||||
raise ValueError("Debian watch needs an exact package name")
|
||||
if provider == "git_branch":
|
||||
branch = watch.get("branch", "")
|
||||
if not isinstance(branch, str) or not branch or branch.startswith("-"):
|
||||
raise ValueError("git branch watch needs an explicit branch")
|
||||
run(["git", "check-ref-format", "refs/heads/" + branch])
|
||||
for field in ("variables", "submodules", "fields"):
|
||||
mapping = watch.get(field, {})
|
||||
if not isinstance(mapping, dict):
|
||||
raise ValueError(f"watch.{field} must be a string mapping")
|
||||
for name, value in mapping.items():
|
||||
pattern = r"[a-z][a-z0-9_]*" if field == "fields" else r"_[a-z][a-z0-9_]*"
|
||||
if not re.fullmatch(pattern, name) or not isinstance(value, str) or not value:
|
||||
raise ValueError(f"invalid watch.{field} mapping")
|
||||
if "submodules" in watch and provider != "github":
|
||||
raise ValueError("submodules require a GitHub watch")
|
||||
if watch.get("variables", {}).keys() & watch.get("submodules", {}).keys():
|
||||
raise ValueError("a release variable cannot also be a submodule")
|
||||
for path in watch.get("submodules", {}).values():
|
||||
if path.startswith("/") or any(part in {"", ".", ".."} for part in path.split("/")):
|
||||
raise ValueError("submodule path must be relative to the release repository")
|
||||
for field in ("allow_prerelease", "unescape_json", "filenames", "sequence"):
|
||||
if field in watch and not isinstance(watch[field], bool):
|
||||
raise ValueError(f"watch.{field} must be boolean")
|
||||
for field in ("version", "revision", "member", "dist_tag"):
|
||||
if field in watch and (not isinstance(watch[field], str) or not watch[field]):
|
||||
raise ValueError(f"watch.{field} must be a string template")
|
||||
if "revision_variable" in watch:
|
||||
name = watch["revision_variable"]
|
||||
if not isinstance(name, str) or name not in watch.get("variables", {}) or not watch.get("revision"):
|
||||
raise ValueError("revision_variable requires a declared variable and revision template")
|
||||
for field in ("mutable_sources",):
|
||||
entries = watch.get(field, [])
|
||||
if not isinstance(entries, list) or any(not isinstance(v, str) or not re.fullmatch(r"source(?:_[a-z0-9_]+)?:[0-9]+", v) for v in entries):
|
||||
raise ValueError(f"watch.{field} must name source-array:index entries")
|
||||
return provider
|
||||
|
||||
|
||||
def json_path(data, path):
|
||||
for key in path.split("."):
|
||||
data = data[int(key)] if isinstance(data, list) else data[key]
|
||||
return data
|
||||
|
||||
|
||||
def candidate(watch, values):
|
||||
values = {k: str(v) for k, v in values.items() if v is not None}
|
||||
version = watch.get("version", "{version}").format_map(values)
|
||||
if not VERSION.fullmatch(version):
|
||||
raise ValueError(f"unusable upstream version: {version!r}")
|
||||
revision = watch.get("revision", "").format_map(values)
|
||||
if revision and not re.fullmatch(r"[0-9]+", revision):
|
||||
raise ValueError("upstream release revision must be numeric")
|
||||
return {"pkgver": version, "values": values,
|
||||
"published_at": values.get("published_at"),
|
||||
"revision": revision}
|
||||
|
||||
|
||||
def matches(watch, text, extra=None, full=False):
|
||||
pattern = re.compile(watch["pattern"])
|
||||
found = [pattern.fullmatch(text)] if full else pattern.finditer(text)
|
||||
for match in found:
|
||||
if match:
|
||||
yield candidate(watch, {**(extra or {}), **match.groupdict()})
|
||||
|
||||
|
||||
def discover(watch, fetch):
|
||||
provider = validate(watch)
|
||||
feed = watch[provider]
|
||||
results = []
|
||||
if provider == "github":
|
||||
releases = fetch.json(f"https://api.github.com/repos/{feed}/releases?per_page=100")
|
||||
if not isinstance(releases, list):
|
||||
raise ValueError("GitHub did not return a release list")
|
||||
for release in releases:
|
||||
if release.get("draft") or (release.get("prerelease") and not watch.get("allow_prerelease")):
|
||||
continue
|
||||
for item in matches(watch, release["tag_name"], {"tag": release["tag_name"], "published_at": release["published_at"]}, full=True):
|
||||
item["assets"] = release.get("assets", [])
|
||||
results.append(item)
|
||||
elif provider == "git_tags":
|
||||
refs = run(["git", "ls-remote", "--tags", feed], text=True)
|
||||
tags = {}
|
||||
for line in refs.splitlines():
|
||||
commit, ref = line.split()
|
||||
tag = ref.removeprefix("refs/tags/")
|
||||
if tag.endswith("^{}"):
|
||||
tags[tag[:-3]] = commit
|
||||
else:
|
||||
tags.setdefault(tag, commit)
|
||||
for tag, commit in tags.items():
|
||||
results.extend(matches(watch, tag, {"tag": tag, "commit": commit}, full=True))
|
||||
elif provider == "git_branch":
|
||||
with tempfile.TemporaryDirectory(prefix="upstream-git-") as work:
|
||||
subprocess.run(["git", "clone", "--quiet", "--bare", "--filter=blob:none", "--single-branch", "--branch", watch["branch"], feed, work], check=True)
|
||||
commit = run(["git", "-C", work, "rev-parse", "HEAD"], text=True).strip()
|
||||
count = run(["git", "-C", work, "rev-list", "--count", "HEAD"], text=True).strip()
|
||||
date = run(["git", "-C", work, "show", "-s", "--format=%cs", "HEAD"], text=True).strip().replace("-", "")
|
||||
timestamp = run(["git", "-C", work, "show", "-s", "--format=%cI", "HEAD"], text=True).strip()
|
||||
results.append(candidate(watch, {"version": date, "date": date, "count": count, "commit": commit, "published_at": timestamp}))
|
||||
elif provider == "npm":
|
||||
data = fetch.json("https://registry.npmjs.org/" + quote(feed, safe=""))
|
||||
version = data["dist-tags"][watch.get("dist_tag", "latest")]
|
||||
results.append(candidate(watch, {"version": version, "published_at": data.get("time", {}).get(version)}))
|
||||
elif provider == "pypi":
|
||||
data = fetch.json(f"https://pypi.org/pypi/{feed}/json")
|
||||
version = data["info"]["version"]
|
||||
dates = [r["upload_time_iso_8601"] for r in data["releases"].get(version, []) if not r.get("yanked")]
|
||||
if not dates:
|
||||
raise ValueError("PyPI release has no unyanked files")
|
||||
results.append(candidate(watch, {"version": version, "published_at": max(dates)}))
|
||||
elif provider == "debian":
|
||||
for stanza in re.split(r"\n\s*\n", fetch.text(feed).replace("\r", "")):
|
||||
fields = dict(re.findall(r"^([A-Za-z0-9-]+): (.*)$", stanza, re.M))
|
||||
if fields.get("Package") != watch["package"]:
|
||||
continue
|
||||
if "pattern" in watch:
|
||||
results.extend(matches(watch, fields["Version"], full=True))
|
||||
else:
|
||||
results.append(candidate(watch, {"version": fields["Version"]}))
|
||||
elif provider == "json":
|
||||
data = fetch.json(feed)
|
||||
values = {"version": json_path(data, watch["path"])}
|
||||
values.update({name: json_path(data, path) for name, path in watch.get("fields", {}).items()})
|
||||
results.append(candidate(watch, values))
|
||||
elif provider == "redirect":
|
||||
final_url = run(["curl", "--proto", "=https", "--proto-redir", "=https", "-fsSLI", "--max-time", "60", "-o", "/dev/null", "-w", "%{url_effective}", feed], text=True)
|
||||
results.extend(matches(watch, final_url))
|
||||
elif provider == "regex":
|
||||
text = fetch.text(feed)
|
||||
if watch.get("unescape_json"):
|
||||
text = text.replace('\\"', '"')
|
||||
results.extend(matches(watch, text))
|
||||
elif provider == "archive":
|
||||
file = fetch.file(feed)
|
||||
if zipfile.is_zipfile(file):
|
||||
with zipfile.ZipFile(file) as archive:
|
||||
names = archive.namelist()
|
||||
if watch.get("filenames"):
|
||||
results.extend(matches(watch, "\n".join(names)))
|
||||
for name in ([] if watch.get("filenames") else names):
|
||||
if re.fullmatch(watch.get("member", ".*"), name):
|
||||
results.extend(matches(watch, archive.read(name).decode()))
|
||||
elif file.read_bytes()[:8] == b"!<arch>\n":
|
||||
names = run(["bsdtar", "-tf", str(file)], text=True).splitlines()
|
||||
controls = [name for name in names if name.startswith("control.tar")]
|
||||
if len(controls) != 1:
|
||||
raise ValueError("deb does not contain exactly one control archive")
|
||||
data = run(["bsdtar", "-xOf", str(file), controls[0]])
|
||||
with tarfile.open(fileobj=io.BytesIO(data)) as archive:
|
||||
members = [m for m in archive if m.name.removeprefix("./") == "control"]
|
||||
if len(members) != 1:
|
||||
raise ValueError("deb control file is missing or ambiguous")
|
||||
results.extend(matches(watch, archive.extractfile(members[0]).read().decode()))
|
||||
else:
|
||||
with tarfile.open(file) as archive:
|
||||
for member in archive:
|
||||
if member.isfile() and re.fullmatch(watch.get("member", ".*"), member.name):
|
||||
results.extend(matches(watch, archive.extractfile(member).read().decode()))
|
||||
if not results:
|
||||
raise ValueError(f"no matching releases in {feed}")
|
||||
return results
|
||||
|
||||
|
||||
def select_release(releases, min_age=0, now=None, bypass=False):
|
||||
now = now or dt.datetime.now(dt.timezone.utc)
|
||||
best = None
|
||||
for release in releases:
|
||||
if min_age and not bypass:
|
||||
value = release.get("published_at")
|
||||
if not value or not re.fullmatch(r"\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:?\d\d)", value):
|
||||
raise ValueError("release age cannot be established")
|
||||
if (now - dt.datetime.fromisoformat(value.replace("Z", "+00:00"))).total_seconds() < min_age:
|
||||
continue
|
||||
order = vercmp(release["pkgver"], best["pkgver"]) if best else 1
|
||||
if best and order == 0:
|
||||
order = vercmp(release["revision"] or "0", best["revision"] or "0")
|
||||
if order > 0:
|
||||
best = release
|
||||
return best
|
||||
|
||||
|
||||
DUMP = r'''
|
||||
source "$1" >/dev/null || exit 1
|
||||
set +u
|
||||
for __watch_name in pkgver pkgrel epoch arch $(compgen -A variable | LC_ALL=C sort); do
|
||||
case "$__watch_name" in
|
||||
pkgver|pkgrel|epoch|arch|source|source_*|md5sums*|sha1sums*|sha224sums*|sha256sums*|sha384sums*|sha512sums*|b2sums*|_*)
|
||||
[[ $__watch_name == __watch_* ]] && continue
|
||||
declare -n __watch_value="$__watch_name"
|
||||
printf '%s\0' "$__watch_name" "${#__watch_value[@]}" "${__watch_value[@]}"
|
||||
unset -n __watch_value
|
||||
;;
|
||||
esac
|
||||
done
|
||||
'''
|
||||
|
||||
|
||||
def read_recipe(path, arch="x86_64"):
|
||||
with tempfile.TemporaryDirectory(prefix="recipe-read-") as work:
|
||||
env = {**os.environ, "CARCH": arch, "SRCDEST": work, "srcdir": work, "pkgdir": work}
|
||||
data = run(["bash", "-c", DUMP, "_", str(path.resolve())], cwd=path.parent, env=env).decode().split("\0")
|
||||
result = {}
|
||||
index = 0
|
||||
while index < len(data) - 1:
|
||||
name, size = data[index:index + 2]
|
||||
index += 2
|
||||
size = int(size)
|
||||
result[name] = data[index:index + size]
|
||||
index += size
|
||||
return result
|
||||
|
||||
|
||||
def scalar(recipe, name, default=""):
|
||||
return recipe.get(name, [default])[0] if recipe.get(name) else default
|
||||
|
||||
|
||||
def replace_scalar(text, name, value):
|
||||
if not SCALAR.fullmatch(value):
|
||||
raise ValueError(f"unsafe {name} value")
|
||||
pattern = re.compile(r"^" + re.escape(name) + r"=.*$", re.M)
|
||||
if len(pattern.findall(text)) != 1:
|
||||
raise ValueError(f"expected one top-level {name}= assignment")
|
||||
return pattern.sub(lambda _: f"{name}={value}", text)
|
||||
|
||||
|
||||
def replace_array(text, name, values):
|
||||
starts = list(re.finditer(r"^" + re.escape(name) + r"=\(", text, re.M))
|
||||
if len(starts) != 1:
|
||||
raise ValueError(f"expected one top-level {name}= array")
|
||||
start = starts[0]
|
||||
depth, quote_char, escaped, comment = 1, None, False, False
|
||||
for index in range(start.end(), len(text)):
|
||||
char = text[index]
|
||||
if comment:
|
||||
if char == "\n": comment = False
|
||||
elif escaped:
|
||||
escaped = False
|
||||
elif char == "\\" and quote_char != "'":
|
||||
escaped = True
|
||||
elif quote_char:
|
||||
if char == quote_char: quote_char = None
|
||||
elif char in "\"'": quote_char = char
|
||||
elif char == "#" and (index == 0 or text[index - 1].isspace()): comment = True
|
||||
elif char == "(": depth += 1
|
||||
elif char == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
replacement = name + "=(" + " ".join("'" + value + "'" for value in values) + ")"
|
||||
return text[:start.start()] + replacement + text[index + 1:]
|
||||
raise ValueError(f"unclosed {name} array")
|
||||
|
||||
|
||||
def bump_pkgrel(value):
|
||||
if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", value):
|
||||
raise ValueError(f"invalid pkgrel: {value}")
|
||||
components = value.split(".")
|
||||
components[-1] = str(int(components[-1]) + 1)
|
||||
return ".".join(components)
|
||||
|
||||
|
||||
def complete_version(recipe):
|
||||
return f"{scalar(recipe, 'epoch', '0')}:{scalar(recipe, 'pkgver')}-{scalar(recipe, 'pkgrel')}"
|
||||
|
||||
|
||||
def hash_file(path, algorithm):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, HASHES.get(algorithm, algorithm)).hexdigest()
|
||||
|
||||
|
||||
def source_url(source):
|
||||
return source.split("::", 1)[-1]
|
||||
|
||||
|
||||
def git_source_file(url, cache):
|
||||
base, fragment = url.removeprefix("git+").split("#", 1)
|
||||
kind, ref = fragment.split("=", 1)
|
||||
https(base)
|
||||
if kind not in {"tag", "commit"} or (kind == "commit" and not re.fullmatch(r"[0-9a-f]{40}", ref)):
|
||||
raise ValueError("VCS sources must name an immutable commit or a checksummed tag")
|
||||
if kind == "tag":
|
||||
run(["git", "check-ref-format", "refs/tags/" + ref])
|
||||
dest = cache / (hashlib.sha256(url.encode()).hexdigest() + ".git.tar")
|
||||
if not dest.exists():
|
||||
with tempfile.TemporaryDirectory(prefix="upstream-source-", dir=cache) as work:
|
||||
subprocess.run(["git", "init", "--quiet", "--bare", work], check=True)
|
||||
subprocess.run(["git", "-C", work, "fetch", "--quiet", "--depth=1", base, "refs/tags/" + ref if kind == "tag" else ref], check=True)
|
||||
scratch = Path(work) / "source.tar"
|
||||
with scratch.open("wb") as output:
|
||||
subprocess.run(["git", "-c", "core.abbrev=no", "-C", work, "archive", "--format", "tar", "FETCH_HEAD"], stdout=output, check=True)
|
||||
# The cache must never retain partial archives after a git failure.
|
||||
scratch.replace(dest)
|
||||
return dest
|
||||
|
||||
|
||||
def updated_checksums(before, after, package, fetch, release, watch):
|
||||
arrays = {}
|
||||
source_names = {key for key in before if key == "source" or key.startswith("source_")}
|
||||
if source_names != {key for key in after if key == "source" or key.startswith("source_")}:
|
||||
raise ValueError("release changed the set of source architectures")
|
||||
for source_name in sorted(source_names):
|
||||
old_sources, sources = before[source_name], after[source_name]
|
||||
suffix = source_name.removeprefix("source")
|
||||
names = [name for name in before if SUM.fullmatch(name) and (SUM.fullmatch(name)[2] or "") == suffix]
|
||||
if not sources:
|
||||
continue
|
||||
if len(sources) != len(old_sources) or not names:
|
||||
raise ValueError(f"{source_name}: sources changed shape or have no checksums")
|
||||
for name in names:
|
||||
if len(before[name]) != len(sources):
|
||||
raise ValueError(f"{name}: source/checksum count mismatch")
|
||||
values = []
|
||||
algorithm = SUM.fullmatch(name)[1]
|
||||
for index, source in enumerate(sources):
|
||||
old = before[name][index]
|
||||
if source == old_sources[index] and f"{source_name}:{index}" not in watch.get("mutable_sources", []):
|
||||
values.append(old)
|
||||
continue
|
||||
url = source_url(source)
|
||||
# A release API digest can supply SHA256 without downloading a
|
||||
# large asset, but only when its exact declared URL matches.
|
||||
assets = [a for a in release.get("assets", []) if a.get("browser_download_url") == url]
|
||||
if algorithm == "sha256" and len(assets) == 1 and re.fullmatch(r"sha256:[0-9a-f]{64}", assets[0].get("digest") or ""):
|
||||
values.append("SKIP" if old == "SKIP" else assets[0]["digest"][7:])
|
||||
continue
|
||||
if url.startswith("git+https://"):
|
||||
file = git_source_file(url, fetch.cache)
|
||||
elif url.startswith("https://"):
|
||||
file = fetch.file(url)
|
||||
elif "://" not in url:
|
||||
file = (package / url).resolve()
|
||||
if not file.is_relative_to(package.resolve()) or not file.is_file():
|
||||
raise ValueError(f"unsafe local source: {url}")
|
||||
else:
|
||||
raise ValueError(f"unsupported source transport: {url}")
|
||||
# Preserve existing signature/prepare()-verified sources. Never
|
||||
# introduce SKIP; still fetch changed URLs to verify availability.
|
||||
values.append("SKIP" if old == "SKIP" else hash_file(file, algorithm))
|
||||
if values != before[name]:
|
||||
arrays[name] = values
|
||||
return arrays
|
||||
|
||||
|
||||
def resolve_release_fields(watch, release, fetch):
|
||||
values = release["values"].copy()
|
||||
if "github" in watch and any("{commit}" in value for value in watch.get("variables", {}).values()):
|
||||
ref = fetch.json(f"https://api.github.com/repos/{watch['github']}/git/ref/tags/{quote(values['tag'], safe='')}")['object']
|
||||
if ref['type'] == 'tag':
|
||||
ref = fetch.json(f"https://api.github.com/repos/{watch['github']}/git/tags/{ref['sha']}")['object']
|
||||
if ref['type'] != 'commit' or not re.fullmatch(r"[0-9a-f]{40}", ref['sha']):
|
||||
raise ValueError("release tag does not resolve to a commit")
|
||||
values['commit'] = ref['sha']
|
||||
variables = {k: template.format_map(values) for k, template in watch.get('variables', {}).items()}
|
||||
for name, path in watch.get('submodules', {}).items():
|
||||
entry = fetch.json(f"https://api.github.com/repos/{watch['github']}/contents/{quote(path, safe='/')}?ref={quote(values['tag'], safe='')}")
|
||||
if not entry.get('submodule_git_url') or not re.fullmatch(r"[0-9a-f]{40}", entry.get('sha', '')):
|
||||
raise ValueError(f"release does not contain submodule {path}")
|
||||
variables[name] = entry['sha']
|
||||
if any(not SCALAR.fullmatch(value) for value in variables.values()):
|
||||
raise ValueError("unsafe release variable value")
|
||||
release['variables'] = variables
|
||||
return release
|
||||
|
||||
|
||||
def sync(package, fetch, min_age=0, check=False):
|
||||
metadata = json.loads((package / ".omarchy/package.json").read_text())
|
||||
if metadata.get("sync") is False:
|
||||
return {"status": "skipped", "reason": "upstream updates held by sync=false"}
|
||||
watch = metadata["upstream"]["watch"]
|
||||
validate(watch)
|
||||
path = package / "PKGBUILD"
|
||||
original = path.read_text()
|
||||
before = read_recipe(path)
|
||||
release = select_release(discover(watch, fetch), min_age, bypass=os.environ.get("BYPASS_MIN_RELEASE_AGE") == "1")
|
||||
if release is None:
|
||||
return {"status": "skipped", "reason": "minimum release age"}
|
||||
current = scalar(before, "pkgver")
|
||||
if watch.get('sequence'):
|
||||
prefix, counter, identity = current.rsplit('.', 2)
|
||||
new_prefix, new_identity = release['values']['version'], release['values']['hash']
|
||||
if new_prefix == prefix:
|
||||
release['pkgver'] = current if new_identity == identity else f"{prefix}.{int(counter) + 1}.{new_identity}"
|
||||
order = vercmp(release["pkgver"], current)
|
||||
if order < 0:
|
||||
return {"status": "skipped", "current": current, "available": release["pkgver"], "reason": "upstream is older"}
|
||||
if order == 0 and not watch.get("revision_variable"):
|
||||
return {"status": "skipped", "current": current, "reason": "already current"}
|
||||
release = resolve_release_fields(watch, release, fetch)
|
||||
changed_variables = {k: v for k, v in release["variables"].items() if scalar(before, k) != v}
|
||||
if order == 0 and not changed_variables:
|
||||
return {"status": "skipped", "current": current, "reason": "already current"}
|
||||
if order == 0 and changed_variables:
|
||||
# Only a declared, forward-moving release revision can rebuild the same
|
||||
# version. A changed hash/commit alone is an immutable-release violation.
|
||||
revision_field = watch.get("revision_variable")
|
||||
if revision_field not in changed_variables or vercmp(changed_variables[revision_field], scalar(before, revision_field, "0")) <= 0:
|
||||
raise ValueError("release metadata changed without a newer version/revision")
|
||||
new_pkgrel = "1" if order > 0 else bump_pkgrel(scalar(before, "pkgrel"))
|
||||
text = replace_scalar(original, "pkgver", release["pkgver"])
|
||||
text = replace_scalar(text, "pkgrel", new_pkgrel)
|
||||
for name, value in release["variables"].items():
|
||||
text = replace_scalar(text, name, value)
|
||||
if check:
|
||||
return {"status": "available", "current": current, "release": release}
|
||||
scratch = path.with_name("PKGBUILD.sync-upstream")
|
||||
try:
|
||||
scratch.write_text(text)
|
||||
after = read_recipe(scratch)
|
||||
if scalar(after, "pkgver") != release["pkgver"] or scalar(after, "pkgrel") != new_pkgrel:
|
||||
raise ValueError("recipe did not retain the release version")
|
||||
if vercmp(complete_version(after), complete_version(before)) <= 0:
|
||||
raise ValueError("complete package version must increase")
|
||||
if before["arch"] != after["arch"]:
|
||||
raise ValueError("release changed supported architectures")
|
||||
arrays = updated_checksums(before, after, package, fetch, release, watch)
|
||||
for name, values in arrays.items():
|
||||
text = replace_array(text, name, values)
|
||||
scratch.write_text(text)
|
||||
subprocess.run(["bash", "-n", str(scratch)], check=True)
|
||||
for arch in before["arch"]:
|
||||
result = read_recipe(scratch, "x86_64" if arch == "any" else arch)
|
||||
if complete_version(result) != complete_version(after):
|
||||
raise ValueError(f"{arch}: inconsistent release version")
|
||||
for name, values in arrays.items():
|
||||
if result.get(name) != values:
|
||||
raise ValueError(f"{arch}: rewritten {name} differs from the checked source hashes")
|
||||
for name in after:
|
||||
if name == "source" or name.startswith("source_"):
|
||||
if result.get(name) != after[name]:
|
||||
raise ValueError(f"{arch}: conditional {name} differs from the checked sources; use source_<arch> arrays")
|
||||
scratch.chmod(path.stat().st_mode)
|
||||
scratch.replace(path)
|
||||
return {"status": "updated", "before": complete_version(before), "after": complete_version(after)}
|
||||
finally:
|
||||
scratch.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("command", choices=["sync", "check", "validate"])
|
||||
parser.add_argument("package", type=Path)
|
||||
parser.add_argument("--min-age", type=int, default=0)
|
||||
args = parser.parse_args()
|
||||
package = args.package.resolve()
|
||||
if args.command == "validate":
|
||||
validate(json.loads((package / ".omarchy/package.json").read_text())["upstream"]["watch"])
|
||||
return
|
||||
with tempfile.TemporaryDirectory(prefix="upstream-watch-") as cache:
|
||||
fetch = Fetcher(os.environ.get("UPSTREAM_CACHE_DIR", cache))
|
||||
print(json.dumps(sync(package, fetch, args.min_age, check=args.command == "check")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except (ValueError, KeyError, TypeError, IndexError, re.error, OSError, subprocess.CalledProcessError) as error:
|
||||
print(f"upstream watch failed: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -1,5 +1,19 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "a0f4262f94eb8a5b604146e71d42a3bb522feedf"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"debian": "https://downloads.1password.com/linux/debian/amd64/dists/beta/main/binary-amd64/Packages",
|
||||
"package": "1password",
|
||||
"pattern": "(?P<version>[0-9]+(?:\\.[0-9]+)*)~(?P<build>[0-9]+)\\.BETA",
|
||||
"version": "{version}_{build}.BETA",
|
||||
"variables": {
|
||||
"_tarver": "{version}-{build}.BETA"
|
||||
}
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "1password-beta",
|
||||
"commit": "18d2de51dc01c9a58c1e8e96262f7afadcbf0648"
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Keep the AUR x86_64 checksums and add aarch64 sources. The aarch64
|
||||
# artifacts are signed by 1Password's validpgpkeys, so we skip checksums there
|
||||
# instead of baking version-specific hashes into Omarchy metadata.
|
||||
set +u
|
||||
CARCH=x86_64 source PKGBUILD
|
||||
set -u
|
||||
|
||||
if declare -p sha256sums >/dev/null 2>&1 && [[ ${#sha256sums[@]} -ge 2 ]]; then
|
||||
x86_sums=("${sha256sums[@]}")
|
||||
elif declare -p sha256sums_x86_64 >/dev/null 2>&1 && [[ ${#sha256sums_x86_64[@]} -ge 2 ]]; then
|
||||
x86_sums=("${sha256sums_x86_64[@]}")
|
||||
else
|
||||
echo "Unable to read x86_64 checksums from PKGBUILD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sha256_from_url() {
|
||||
curl -fsSL "$1" | sha256sum | awk '{ print $1 }'
|
||||
}
|
||||
|
||||
arm_url="https://downloads.1password.com/linux/tar/beta/aarch64/1password-${_tarver}.arm64.tar.gz"
|
||||
arm_tar_sum=$(sha256_from_url "$arm_url")
|
||||
arm_sig_sum=$(sha256_from_url "$arm_url.sig")
|
||||
|
||||
emit_archdir() {
|
||||
cat <<'EOF'
|
||||
case "${CARCH}" in
|
||||
x86_64)
|
||||
_archdir="x64"
|
||||
;;
|
||||
aarch64)
|
||||
_archdir="arm64"
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
}
|
||||
|
||||
emit_sources() {
|
||||
cat <<EOF
|
||||
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=('${x86_sums[0]}'
|
||||
'${x86_sums[1]}')
|
||||
sha256sums_aarch64=('$arm_tar_sum'
|
||||
'$arm_sig_sum')
|
||||
EOF
|
||||
}
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
skip_checksums=false
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$skip_checksums" == true ]]; then
|
||||
[[ "$line" == ")" ]] && skip_checksums=false
|
||||
continue
|
||||
fi
|
||||
|
||||
case "$line" in
|
||||
'_tar="1password-${_tarver}.x64.tar.gz"')
|
||||
emit_archdir >> "$tmpfile"
|
||||
;;
|
||||
"arch=('x86_64')")
|
||||
echo "arch=('x86_64' 'aarch64')" >> "$tmpfile"
|
||||
;;
|
||||
source=\(*)
|
||||
emit_sources >> "$tmpfile"
|
||||
;;
|
||||
sha256sums=\(*)
|
||||
[[ "$line" == *")" ]] || skip_checksums=true
|
||||
;;
|
||||
*)
|
||||
line=${line//1password-\$\{_tarver\}.x64/1password-\$\{_tarver\}.\$\{_archdir\}}
|
||||
printf '%s\n' "$line" >> "$tmpfile"
|
||||
;;
|
||||
esac
|
||||
done < PKGBUILD
|
||||
|
||||
mv "$tmpfile" PKGBUILD
|
||||
@@ -1,6 +1,6 @@
|
||||
pkgname=1password-beta
|
||||
|
||||
_tarver=8.12.34-29.BETA
|
||||
_tarver=8.12.38-25.BETA
|
||||
case "${CARCH}" in
|
||||
x86_64)
|
||||
_archdir="x64"
|
||||
@@ -9,8 +9,8 @@ case "${CARCH}" in
|
||||
_archdir="arm64"
|
||||
;;
|
||||
esac
|
||||
pkgver=${_tarver//-/_}
|
||||
pkgrel=29.1
|
||||
pkgver=8.12.38_25.BETA
|
||||
pkgrel=25.2
|
||||
conflicts=('1password' '1password-beta-bin')
|
||||
pkgdesc="Password manager and secure wallet"
|
||||
arch=('x86_64' 'aarch64')
|
||||
@@ -22,10 +22,10 @@ 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=('6894b283a534cf94b07903fb38966a0aab2e37f75ee3848ce340308819aadddb'
|
||||
'8d4df4d0a80d2be7aad7d91ad964a750c8f32db5007261045003c191690c8246')
|
||||
sha256sums_aarch64=('c77ce6ddf36dbd6054c64d91b7f644274d3218f465fd60e211d0296f6443124a'
|
||||
'91c7249a1cf5e7924ef2810cb0cb1b893d8a9a424b373b433f45d7aaa5a8369b')
|
||||
sha256sums_x86_64=('c6d302a2c7404a7ded34a3c4f1c401a43eafeed8b147d128dcb416284c2c2b71'
|
||||
'cc0f00054749c32d77fba31a12a8dece812e409f4b9d81850d2f9b50fab55dca')
|
||||
sha256sums_aarch64=('1fd62cd0df90098dd5e50d22e9a6c0a5221f9db6355b7c848db7af7076b395fd'
|
||||
'4d273b71ab987dcadad9e4fa7cfac7e0173dc4dbe7908c9a3e76af274313dd76')
|
||||
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
|
||||
|
||||
package() {
|
||||
@@ -42,7 +42,7 @@ package() {
|
||||
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
|
||||
done
|
||||
# Install desktop file
|
||||
install -Dm0644 resources/1password.desktop -t "${pkgdir}"/usr/share/applications/
|
||||
install -Dm0644 resources/com.onepassword.OnePassword.desktop -t "${pkgdir}"/usr/share/applications/
|
||||
|
||||
# Fill in policy kit file with a list of (the first 10) human users of the system.
|
||||
export POLICY_OWNERS
|
||||
@@ -65,8 +65,7 @@ EOF" > ./com.1password.1Password.policy
|
||||
# Cleanup un-needed files
|
||||
rm "${pkgdir}"/opt/1Password/com.1password.1Password.policy "${pkgdir}"/opt/1Password/com.1password.1Password.policy.tpl "${pkgdir}"/opt/1Password/install_biometrics_policy.sh
|
||||
rm -r "${pkgdir}"/opt/1Password/resources/icons/
|
||||
rm "${pkgdir}"/opt/1Password/resources/1password.desktop "${pkgdir}"/opt/1Password/resources/custom_allowed_browsers
|
||||
|
||||
rm "${pkgdir}"/opt/1Password/resources/com.onepassword.OnePassword.desktop "${pkgdir}"/opt/1Password/resources/custom_allowed_browsers
|
||||
# Symlink /usr/bin executable to opt
|
||||
install -dm0755 "${pkgdir}"/usr/bin
|
||||
ln -s /opt/1Password/1password "${pkgdir}"/usr/bin/1password
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "b0d208821677a5dbb883a8b92f06a5c92b9e861a"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"json": "https://app-updates.agilebits.com/check/1/0/CLI2/en/0",
|
||||
"path": "version"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "1password-cli",
|
||||
"commit": "b0d208821677a5dbb883a8b92f06a5c92b9e861a"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "0f047f40a200121075ca3cedce59ddf226f2a0f1"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "omacom/aether",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "aether",
|
||||
"commit": "0f047f40a200121075ca3cedce59ddf226f2a0f1"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Bjarne Øverli <bjarne@oever.li>
|
||||
pkgname=aether
|
||||
pkgver=4.29.8
|
||||
pkgver=4.30.0
|
||||
pkgrel=1
|
||||
pkgdesc='Desktop theming application - extract colors from wallpapers and apply cohesive themes'
|
||||
arch=('x86_64' 'aarch64')
|
||||
@@ -10,9 +10,9 @@ depends=('webkit2gtk-4.1' 'gtk3')
|
||||
source=("aether-${pkgver}.tar.gz::https://github.com/omacom/aether/archive/refs/tags/v${pkgver}.tar.gz")
|
||||
source_x86_64=("aether-linux-amd64-${pkgver}::https://github.com/omacom/aether/releases/download/v${pkgver}/aether-linux-amd64")
|
||||
source_aarch64=("aether-linux-arm64-${pkgver}::https://github.com/omacom/aether/releases/download/v${pkgver}/aether-linux-arm64")
|
||||
sha256sums=('b83e0eeb1332b4ed655389a051d5b9b14a3e109968b7f278005e52c5e69a1e9c')
|
||||
sha256sums_x86_64=('d3d2d07b32da7a495221ed271ee66f4a1e344c91dcec9b267ec0a74ab6e36462')
|
||||
sha256sums_aarch64=('ffcfd23d0375a3f0c0ce014821cc5e1670f2e061c35e991340e75352dac9b731')
|
||||
sha256sums=('f67c8d2c6f27f67a755bc279ece5ddb194f1bb165648280b9a7be86904d36ff5')
|
||||
sha256sums_x86_64=('75bda600ddd3ecab3338de5c0c5d5e2c9f08cfc0c465b63f8e6cb9c5cb60d68e')
|
||||
sha256sums_aarch64=('a91d800736def74d86e19d8acbecc4bda3d7c3e64fb95273f809707104c3a5bc')
|
||||
noextract=("aether-linux-amd64-${pkgver}" "aether-linux-arm64-${pkgver}")
|
||||
|
||||
package() {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "local",
|
||||
"channels": [
|
||||
"edge"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
From 375e80ec3826b54618fdd5f2db708c0f2bb936ae Mon Sep 17 00:00:00 2001
|
||||
From: Marcelo Alcantara <maralc@gmail.com>
|
||||
Date: Wed, 16 Sep 2026 00:39:04 +1000
|
||||
Subject: [PATCH] drm: re-read possible CRTCs when rescanning connectors
|
||||
|
||||
A driver can change a connector's possible CRTCs at runtime. Apple's DCP
|
||||
driver narrows a Type-C port's encoder to the display pipeline the fabric
|
||||
routed it to and relies on the following hotplug for userspace to re-read
|
||||
it. possibleCrtcs was only read when the connector was first created, so a
|
||||
rerouted port kept a stale mask and was never assigned its now-free CRTC
|
||||
until the compositor restarted.
|
||||
---
|
||||
src/backend/drm/DRM.cpp | 7 +++++++
|
||||
1 file changed, 7 insertions(+)
|
||||
|
||||
diff --git a/src/backend/drm/DRM.cpp b/src/backend/drm/DRM.cpp
|
||||
index 6618a26..b492dac 100644
|
||||
--- a/src/backend/drm/DRM.cpp
|
||||
+++ b/src/backend/drm/DRM.cpp
|
||||
@@ -1287,6 +1287,13 @@ void Aquamarine::CDRMBackend::scanConnectors() {
|
||||
} else {
|
||||
backend->log(AQ_LOG_DEBUG, std::format("drm: Connector id {} already initialized", connectorID));
|
||||
conn = *it;
|
||||
+
|
||||
+ // drivers may narrow or widen these on hotplug, e.g. when a Type-C port is rerouted to another pipeline
|
||||
+ const auto possibleCrtcs = drmModeConnectorGetPossibleCrtcs(gpu->fd, drmConn);
|
||||
+ if (possibleCrtcs && possibleCrtcs != conn->possibleCrtcs) {
|
||||
+ backend->log(AQ_LOG_DEBUG, std::format("drm: Connector {} possible CRTCs changed {:#x} -> {:#x}", conn->szName, conn->possibleCrtcs, possibleCrtcs));
|
||||
+ conn->possibleCrtcs = possibleCrtcs;
|
||||
+ }
|
||||
}
|
||||
|
||||
conn->status = drmConn->connection;
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Maintainer: Caleb Maclennan <caleb@alerque.com>
|
||||
# Contributor: Aaron Blasko <blaskoazzolaaaron [at] gmail.com>
|
||||
#
|
||||
# Arch's aquamarine 0.15.1-1 plus one patch, carried on edge for Apple Silicon:
|
||||
# re-read a connector's possible CRTCs when rescanning, so a Type-C port the
|
||||
# DCP fabric reroutes to another display pipeline gets a CRTC without
|
||||
# restarting the compositor. Drop the carry once hyprwm releases the fix.
|
||||
|
||||
pkgname=aquamarine
|
||||
pkgver=0.15.1
|
||||
pkgrel=1.1
|
||||
pkgdesc='a very light linux rendering backend library'
|
||||
arch=(aarch64)
|
||||
url="https://github.com/hyprwm/$pkgname"
|
||||
license=(BSD-3-Clause)
|
||||
depends=(libgcc
|
||||
libstdc++
|
||||
glibc # libc.so libm.so
|
||||
hyprutils libhyprutils.so
|
||||
libdisplay-info libdisplay-info.so
|
||||
libdrm # libdrm.so
|
||||
libglvnd libEGL.so
|
||||
libinput # libinput.so
|
||||
mesa # libgbm.so
|
||||
opengl-driver
|
||||
pixman
|
||||
seatd libseat.so
|
||||
systemd-libs libudev.so
|
||||
wayland libwayland-client.so
|
||||
wayland-protocols)
|
||||
makedepends=(cmake
|
||||
hyprwayland-scanner)
|
||||
provides=("lib$pkgname.so")
|
||||
_archive="$pkgname-$pkgver"
|
||||
source=("$url/archive/v$pkgver/$_archive.tar.gz"
|
||||
0001-drm-re-read-possible-CRTCs-when-rescanning-connectors.patch)
|
||||
sha256sums=('2f9de98c0bd1b7b1b09c576e390a2fef436449762fb334163c414f0c300296f2'
|
||||
'50e9e38ff915b18074dc9b5dd1d07d7f24e340e99f254476d6abe578eece7864')
|
||||
|
||||
prepare() {
|
||||
cd "$_archive"
|
||||
patch -Np1 -i ../0001-drm-re-read-possible-CRTCs-when-rescanning-connectors.patch
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$_archive"
|
||||
# cc1plus needs more than 8 MiB of stack for DRM.cpp. GCC raises its own
|
||||
# limit natively, but under QEMU user-mode emulation (the aarch64 builder)
|
||||
# the guest stack is fixed at exec and setrlimit is ignored, so the compiler
|
||||
# segfaults. QEMU reads this at exec; native builds ignore it. The object
|
||||
# code is identical either way.
|
||||
export QEMU_STACK_SIZE=64M
|
||||
cmake -B build \
|
||||
-D CMAKE_INSTALL_PREFIX=/usr \
|
||||
-D CMAKE_BUILD_TYPE=Release
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$_archive"
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname" LICENSE
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "b0ec6ca495eb331a684db91b0fe12085a868b632"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"git_tags": "https://github.com/OpenGamingCollective/asusctl.git",
|
||||
"pattern": "(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "asusctl",
|
||||
"commit": "b0ec6ca495eb331a684db91b0fe12085a868b632"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
pkgbase=asusctl
|
||||
pkgname=(asusctl rog-control-center)
|
||||
pkgver=6.4.0
|
||||
pkgver=6.5.0
|
||||
pkgrel=1
|
||||
pkgdesc="Daemon and tools to control your ASUS ROG laptop"
|
||||
arch=('x86_64')
|
||||
@@ -12,7 +12,7 @@ url="https://asus-linux.org"
|
||||
license=('MPL-2.0')
|
||||
makedepends=('cargo' 'fontconfig')
|
||||
source=("${pkgbase}-${pkgver}.tar.gz::https://github.com/OpenGamingCollective/asusctl/archive/${pkgver}.tar.gz")
|
||||
b2sums=('e90074e904f364386ad661784bd9fc2e929e83ea06ac62fd1d34eb03490cefe8aae3bed2722162f1e8ea5d69248138cb5fd9f90c3a18443d1d8c04cf732b928a')
|
||||
b2sums=('4179e08a60f9480b62e41d84faade1f46407140a213ca4d60c8699837a2486bda8e66b4ca43fa06149667d02e3d060c3efd792348f9a93a675f762d6ea2d05f8')
|
||||
|
||||
prepare() {
|
||||
cd "${pkgbase}-${pkgver}"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)",
|
||||
"git_tags": "https://github.com/AsahiLinux/avd-fw.git"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
# Open replacement firmware for the Apple Video Decoder.
|
||||
# Built from AsahiLinux sources, without extracting proprietary firmware.
|
||||
|
||||
pkgname=avd-fw
|
||||
pkgver=0.1
|
||||
pkgrel=1
|
||||
pkgdesc='Open replacement firmware for the Apple Video Decoder (AVD) on Apple Silicon'
|
||||
# This recipe uses the native ARM linker and serves Apple Silicon systems.
|
||||
# Restrict both builds and publication to aarch64.
|
||||
arch=('aarch64')
|
||||
url='https://github.com/AsahiLinux/avd-fw'
|
||||
license=('MIT')
|
||||
# clang cross-compiles to arm-none-eabi out of the box, so no arm-none-eabi
|
||||
# toolchain is required; llvm supplies llvm-objcopy, which meson.build looks
|
||||
# up by name to turn each linked ELF into a padded raw image.
|
||||
makedepends=('meson' 'clang' 'llvm')
|
||||
# !buildflags is load-bearing: makepkg's CFLAGS/LDFLAGS target the aarch64 host
|
||||
# and would be injected into a bare-metal Cortex-M3 build. !strip keeps makepkg
|
||||
# from running the host strip over raw firmware images.
|
||||
options=('!strip' '!debug' '!buildflags' '!lto')
|
||||
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
|
||||
sha256sums=('2e131244275cb15c94243e41eec8a2a665ec895cfe3a818181549db991e62c67')
|
||||
|
||||
build() {
|
||||
# The cross file selects clang, pins the host machine to cortex-m3 and sets
|
||||
# libdir=lib, which lands the images in /usr/lib/firmware/apple.
|
||||
#
|
||||
# Optimization is pinned to match upstream's Makefile (-O2). Meson's default
|
||||
# buildtype is "debug" (-O0 -g), which would ship unoptimized firmware; the
|
||||
# MMIO accessors in src/util.c go through a volatile typedef, so -O2 cannot
|
||||
# elide register reads or writes.
|
||||
meson setup \
|
||||
--cross-file "$pkgname-$pkgver/llvm.ini" \
|
||||
--prefix=/usr \
|
||||
-Doptimization=2 \
|
||||
-Ddebug=false \
|
||||
"$pkgname-$pkgver" build
|
||||
|
||||
meson compile -C build
|
||||
}
|
||||
|
||||
package() {
|
||||
meson install -C build --destdir "$pkgdir"
|
||||
|
||||
# meson installs custom_target outputs 0755; these are firmware blobs, not
|
||||
# programs, and every other firmware file on the system is 0644.
|
||||
chmod 644 "$pkgdir"/usr/lib/firmware/apple/*.bin
|
||||
|
||||
install -Dm644 "$pkgname-$pkgver/LICENSE" \
|
||||
"$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"origin": {
|
||||
"aur": "bambustudio-bin",
|
||||
"commit": "b962c12d14873f94669e0e256a444f957b1843cd"
|
||||
},
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"regex": "https://api.github.com/repos/bambulab/BambuStudio/releases/latest",
|
||||
"pattern": "\"name\"\\s*:\\s*\"BambuStudio_ubuntu24\\.04-v(?P<version>[0-9]+(?:\\.[0-9]+)*)-(?P<build>[0-9]+)\\.AppImage\"",
|
||||
"variables": {
|
||||
"_build": "{build}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[Desktop Entry]
|
||||
Name=Bambu Studio
|
||||
GenericName=3D Printing Software
|
||||
Comment=Slicer for Bambu Lab and other 3D printers
|
||||
Exec=/usr/bin/bambu-studio %U
|
||||
Icon=BambuStudio
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Graphics;3DGraphics;Engineering;
|
||||
MimeType=x-scheme-handler/bambustudio;x-scheme-handler/bambustudioopen;model/stl;model/3mf;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;application/x-amf;
|
||||
Keywords=3D;Printing;Slicer;gcode;stl;3mf;
|
||||
StartupWMClass=bambu-studio
|
||||
@@ -0,0 +1,48 @@
|
||||
# Maintainer: goll <adrian.goll+aur[at]gmail>
|
||||
# Contributor: George Woodall <georgewoodall82@gmail.com>
|
||||
pkgname=bambustudio-bin
|
||||
pkgver=02.08.02.61
|
||||
pkgrel=1
|
||||
pkgdesc="PC Software for BambuLab's 3D printers"
|
||||
arch=("x86_64")
|
||||
url="https://github.com/bambulab/BambuStudio"
|
||||
license=('AGPL-3.0-only')
|
||||
conflicts=('bambustudio' 'bambustudio-git')
|
||||
depends=('cairo' 'dbus' 'fontconfig' 'gcc-libs' 'glib2' 'glibc'
|
||||
'gst-libav' 'gst-plugins-base-libs' 'gstreamer' 'gtk3' 'libglvnd'
|
||||
'libx11' 'mesa' 'pango' 'wayland' 'webkit2gtk-4.1')
|
||||
makedepends=('7zip')
|
||||
options=('!strip' '!debug')
|
||||
# The upstream watch updates the timestamp together with pkgver.
|
||||
_build=20260820225108
|
||||
source=("bambustudio-${pkgver}.AppImage::https://github.com/bambulab/BambuStudio/releases/download/v${pkgver}/BambuStudio_ubuntu24.04-v${pkgver}-${_build}.AppImage"
|
||||
"BambuStudio.desktop"
|
||||
"bambu-studio")
|
||||
noextract=("bambustudio-${pkgver}.AppImage")
|
||||
sha256sums=(
|
||||
'd501b103fac5424513ec0e8d6bc145fb30719de2c7d94d7320d723740c81a7fd'
|
||||
'f10718a8b201cad64800746fe8167ccc032c545d05f7ad8caa99eb5fb975f2a1'
|
||||
'a3a5c8f6a8b287e42b93957e9602621923c766e0b6a3f10c14eca10b023b15f2'
|
||||
)
|
||||
|
||||
prepare() {
|
||||
# Read the embedded SquashFS without executing or modifying the AppImage.
|
||||
rm -rf "$srcdir/squashfs-root"
|
||||
7z x "$srcdir/bambustudio-${pkgver}.AppImage" -o"$srcdir/squashfs-root" >/dev/null
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir/squashfs-root"
|
||||
install -Dm755 AppRun "$pkgdir/opt/$pkgname/AppRun"
|
||||
cp -a bin resources "$pkgdir/opt/$pkgname/"
|
||||
|
||||
local icon size
|
||||
for icon in usr/share/icons/hicolor/*/apps/BambuStudio.png; do
|
||||
size="${icon#usr/share/icons/hicolor/}"
|
||||
install -Dm644 "$icon" "$pkgdir/usr/share/icons/hicolor/$size"
|
||||
done
|
||||
|
||||
install -Dm755 "$srcdir/bambu-studio" "$pkgdir/usr/bin/bambu-studio"
|
||||
install -Dm644 "$srcdir/BambuStudio.desktop" \
|
||||
"$pkgdir/usr/share/applications/BambuStudio.desktop"
|
||||
}
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
exec "/opt/bambustudio-bin/AppRun" "$@"
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "89c4eb5a0ac9ffe98aecd4ea8b789cf1fee42bf1"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "basecamp/basecamp-cli",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "basecamp-cli",
|
||||
"commit": "89c4eb5a0ac9ffe98aecd4ea8b789cf1fee42bf1"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Basecamp <support@basecamp.com>
|
||||
pkgname=basecamp-cli
|
||||
pkgver=0.11.0
|
||||
pkgrel=1
|
||||
pkgrel=2
|
||||
pkgdesc="CLI for Basecamp project management"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://github.com/basecamp/basecamp-cli"
|
||||
@@ -13,8 +13,8 @@ optdepends=(
|
||||
'zsh: for zsh shell completions'
|
||||
'fish: for fish shell completions'
|
||||
)
|
||||
source_x86_64=("https://github.com/basecamp/basecamp-cli/releases/download/v0.11.0/basecamp_${pkgver}_linux_amd64.tar.gz")
|
||||
source_aarch64=("https://github.com/basecamp/basecamp-cli/releases/download/v0.11.0/basecamp_${pkgver}_linux_arm64.tar.gz")
|
||||
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=('425ffab1251c4315c5f731f8367c3c8c37b54b6b1050eafdbe369e11e1a1ce51')
|
||||
sha256sums_aarch64=('9c433b12a704402a98b238abb3128a0844a0bc682064adb260b11bc228b3595e')
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "195b828d52ce9a181215f753927123e7ef2af252"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "oven-sh/bun",
|
||||
"pattern": "bun-v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "bun-bin",
|
||||
"commit": "195b828d52ce9a181215f753927123e7ef2af252"
|
||||
}
|
||||
}
|
||||
@@ -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.3.11
|
||||
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,11 +12,8 @@ license=('MIT')
|
||||
provides=('bun')
|
||||
conflicts=('bun')
|
||||
options=('!debug')
|
||||
sha256sums_x86_64=('8611ba935af886f05a6f38740a15160326c15e5d5d07adef966130b4493607ed'
|
||||
'abe346f63414547cdf6b35b7a649a490c728b93d006226156923918a84c0e59b'
|
||||
'9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
|
||||
sha256sums_aarch64=('d13944da12a53ecc74bf6a720bd1d04c4555c038dfe422365356a7be47691fdf'
|
||||
'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"
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "f7dd445621ab53f9e51b70b351ff6917ff814175"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"regex": "https://downloads.claude.ai/claude-code-releases/latest",
|
||||
"pattern": "^(?P<version>[0-9]+(?:\\.[0-9]+)*)\\s*$"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "claude-code",
|
||||
"commit": "27f61daa48ebdca87c9b2c7c83c162100d233ccc"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
# Automation repository: https://github.com/fabifont/claude-code-aur
|
||||
|
||||
pkgname=claude-code
|
||||
pkgver=2.1.268
|
||||
pkgver=2.1.278
|
||||
pkgrel=1
|
||||
pkgdesc="An agentic coding tool that lives in your terminal"
|
||||
arch=('x86_64' 'aarch64')
|
||||
@@ -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=('9691a2b7bd796712ca8cffb8e32e54ff7fc45b662540233171a16a94a0425653')
|
||||
sha256sums_aarch64=('116fd031f939ef1e09edf170d62c489e1cc28ed6bfbda49f948773ba168c8f62')
|
||||
sha256sums_x86_64=('5c4735937844e84f8a93306e841a5b0e12252909b07870f789b190468da147ab')
|
||||
sha256sums_aarch64=('7de6cab134e48321148e30182c98614118e8f4666819412bead45865190b34ed')
|
||||
|
||||
package() {
|
||||
install -Dm755 "${srcdir}/claude-${pkgver}-${CARCH}" "${pkgdir}/opt/claude-code/bin/claude"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# repository's package index.
|
||||
|
||||
pkgname=claude-desktop
|
||||
pkgver=1.52386.0
|
||||
pkgver=2.2553.1
|
||||
pkgrel=1
|
||||
pkgdesc="Official Claude desktop app with Claude Code"
|
||||
arch=('x86_64' 'aarch64')
|
||||
@@ -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=('9c5d113ea2c31c0d4f6075c02e6180bf4ab53d35736b9a497ce0e84f62e9654b')
|
||||
sha256sums_aarch64=('71c7d32a8edee57870db54f59a455bd05f92b2e18b45a1510b6ca9099ebd77d7')
|
||||
sha256sums_x86_64=('6700fdd84e77a6b8c93912c2f69eb5d1e40fa99bcd9d37f438f809ef2a6fe6f8')
|
||||
sha256sums_aarch64=('0003a6f9605a210f03c38670d62cd59c71153c2702aa4427e4cabe2e2e5f3390')
|
||||
|
||||
package() {
|
||||
cd "${srcdir}"
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "92fb021a1c78f75043cbbd402aca8b515af069ec"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "bjarneo/cliamp",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "cliamp",
|
||||
"commit": "92fb021a1c78f75043cbbd402aca8b515af069ec"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "da836cc4d2b9373143236030cb0dff18a31b5b97"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "charmbracelet/crush",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "crush-bin",
|
||||
"commit": "9c63847ccd3650646141f84384545c8fe2d68bb7"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
# Maintainer: caarlos0 <carlos@charm.sh>
|
||||
|
||||
pkgname='crush-bin'
|
||||
pkgver=0.93.1
|
||||
pkgver=0.96.0
|
||||
pkgrel=1
|
||||
pkgdesc='A powerful terminal-based AI assistant for developers, providing intelligent coding assistance directly in your terminal.'
|
||||
url='https://charm.sh/crush'
|
||||
@@ -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=('200b9162815187771d99c813f2d71f4b2efe5a94a7830e7ed34fc25764e670ae')
|
||||
sha256sums_aarch64=('667062a39d499506b0fe151148f8d7a1c5cb5722902080d44bb3dd2ddafbf5c1')
|
||||
|
||||
source_armv7h=("${pkgname}_${pkgver}_armv7h.tar.gz::https://github.com/charmbracelet/crush/releases/download/v${pkgver}/crush_${pkgver}_Linux_armv7.tar.gz")
|
||||
sha256sums_armv7h=('368bdba39bcf7cbfb2f0522a2a0560f63f8cd11b18e6bff0b886cd18018a89c2')
|
||||
sha256sums_armv7h=('1de4c1ccb237743e4debb8c302df612fcef5c9b3b5378f5b65c8c6f8bc15cbfa')
|
||||
|
||||
source_i686=("${pkgname}_${pkgver}_i686.tar.gz::https://github.com/charmbracelet/crush/releases/download/v${pkgver}/crush_${pkgver}_Linux_i386.tar.gz")
|
||||
sha256sums_i686=('0ecda04da536cd42b24c8620140720f4fe0af5f5109ca32cee6d53960628313c')
|
||||
sha256sums_i686=('4ec66431565de5721afb7afdd99e45ff6bc9c7e667bd18d9696ea7c5622e158d')
|
||||
|
||||
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=('3086719c3e4ff592b567c22691157457158b87245469009e530447481c975102')
|
||||
sha256sums_x86_64=('5b33303a404acacf761c027e9fa9e69d4d2dd050c2690abe40877c49574b7475')
|
||||
|
||||
package() {
|
||||
case "$CARCH" in
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# binary to point at pm.sh, a stand-in that declines and names pacman instead.
|
||||
|
||||
pkgname=cua-driver-bin
|
||||
pkgver=0.27.0
|
||||
pkgver=0.28.2
|
||||
pkgrel=1
|
||||
pkgdesc="Computer-use driver for native GUI apps: accessibility-tree snapshots and input injection"
|
||||
arch=('x86_64' 'aarch64')
|
||||
@@ -46,8 +46,8 @@ source_x86_64=("https://github.com/trycua/cua/releases/download/cua-driver-rs-v$
|
||||
source_aarch64=("https://github.com/trycua/cua/releases/download/cua-driver-rs-v${pkgver}/cua-driver-rs-${pkgver}-linux-arm64.tar.gz")
|
||||
sha256sums=('c0779290c1d4783169aa3dbfb55feb505e563ef8a004bbf55298ceffcfbda8d9'
|
||||
'c76e251c3ed424200eac52bec35ba534336307fabd83a175ab0b47e2084ab0d8')
|
||||
sha256sums_x86_64=('331af6773b44ee781e8d6ca0e47a35aee741355bb423dfe2bd4f4acd5b697677')
|
||||
sha256sums_aarch64=('2d973a8d82714cc7a80b99d107bfc68abb6bf0a81e98ff225cc85b0acfebefb3')
|
||||
sha256sums_x86_64=('8f3e5b669e2bcd98d0eecc64f40640aac77f358b6332a06abc6ee79991620f7d')
|
||||
sha256sums_aarch64=('cadd7e6b757c3ce50f2b5f6e273c154ea48450fb5fcaff744209b382915eddf5')
|
||||
|
||||
case "${CARCH}" in
|
||||
x86_64) _platform="linux-x86_64" ;;
|
||||
@@ -56,7 +56,7 @@ esac
|
||||
_vendor_tree="cua-driver-rs-${pkgver}-${_platform}"
|
||||
|
||||
# Rust strings carry their length out of band, so the replacement has to be
|
||||
# exactly as long as the original: 32 bytes, which is what fixes the
|
||||
# exactly as long as the original, which is what fixes the
|
||||
# stand-in's short name and location.
|
||||
_vendor_installer='https://cua.ai/driver/install.sh'
|
||||
_pacman_installer='file:///usr/lib/cua-driver/pm.sh'
|
||||
@@ -69,14 +69,14 @@ prepare() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The URL appears twice: once in the updater and once in the printed
|
||||
# reinstall one-liner. Any other count means upstream moved the updater
|
||||
# and this rewrite needs another look, so the build stops rather than
|
||||
# shipping a live self-updater.
|
||||
local found
|
||||
# In 0.28.1 the URL appears in the updater, the printed reinstall command,
|
||||
# and two embedded copies of Skills/cua-driver/README.md. Rewrite all four
|
||||
# so the embedded instructions also defer to pacman. Any other count means
|
||||
# the release layout changed and needs review before packaging.
|
||||
local expected=4 found
|
||||
found=$(grep -obUaF "${_vendor_installer}" cua-driver | wc -l)
|
||||
if (( found != 2 )); then
|
||||
echo "expected the vendor installer URL twice in cua-driver, found ${found}" >&2
|
||||
if (( found != expected )); then
|
||||
echo "expected the vendor installer URL ${expected} times in cua-driver, found ${found}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -87,7 +87,7 @@ prepare() {
|
||||
|
||||
if (( size_before != size_after )) \
|
||||
|| grep -qUaF "${_vendor_installer}" cua-driver \
|
||||
|| (( $(grep -obUaF "${_pacman_installer}" cua-driver | wc -l) != 2 )); then
|
||||
|| (( $(grep -obUaF "${_pacman_installer}" cua-driver | wc -l) != expected )); then
|
||||
echo "installer URL rewrite did not land cleanly in cua-driver" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"source": "local"
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"files": {
|
||||
"CMakeLists.txt": "7e874a595e1abd708cb0626bd9f0f58d79b4b4bbc6d99b45a0cbe1c5c53b43bc",
|
||||
"LICENSE.md": "c0779290c1d4783169aa3dbfb55feb505e563ef8a004bbf55298ceffcfbda8d9",
|
||||
"SOURCE-PROVENANCE.json": "54f514664c84e1358a435f29cd6befd5661b0b133d76997191c000b10f021a75",
|
||||
"cmake/DetectHyprlandAPI.cmake": "216133ec0eb141c3696bf3770a23e63e46521c9e91a0245a7cb75c20a75ba2c5",
|
||||
"cmake/VerifyRuntime.cmake": "5869f79a418e7aa6178d2b9166c36cd01c3093c2579b647624968244db57b761",
|
||||
"include/cua_hyprland/protocol.hpp": "7051463b3c61a2dc93c388e66b6136b7bb524f8694350f249f9d2b5c55826493",
|
||||
"include/cua_hyprland/session.hpp": "e6a968e4f2ac28122cb7413a0e318f6222d2a0a1b7b0c45f4ab419a78639ebea",
|
||||
"include/cua_hyprland/status.hpp": "56a9656647c0f4eeb0c588cd4b98a77df198d1f90421f973e9a80e6802f61495",
|
||||
"src/drag_geometry.hpp": "c5b783d15ff197f22938f08f8d176bab5d45544fe989d150f15cb99295acedb8",
|
||||
"src/foreground_route.hpp": "4aa016c237b33c15e352a9f5f64bbbca7e1a9c1671ffd593a95a0d87994fb519",
|
||||
"src/inject_server.cpp": "0935283580c50fcf0e4ad956f858885700536002b1d86d2f078da9c4404ee9e8",
|
||||
"src/inject_server.hpp": "67de008b4d6983371207bb22a57bab154b1dedda9b38d1207d3ac8cb379382eb",
|
||||
"src/input_client_deadline.hpp": "00a91a789ff698820607449ff7152e2fb50d0f315e0fa9c5c3855752bfffe2ef",
|
||||
"src/input_experiment.cpp": "7017748c782b64b0bc1d257f4ade1a8d46fd19ce940ecc21f22d628614fcef37",
|
||||
"src/input_experiment.hpp": "9da2ddab7f0de6e9cf02aea53e9119acef17ef8513af849bb5aa3d137ebd12c3",
|
||||
"src/input_grant.hpp": "90b544b2f559bacd920b85ab5915f09ff201de3c05a052ce1b71ff71f767e6a6",
|
||||
"src/keyboard_layout.hpp": "bc2ec039ac1974caebbb66fe4acb7a2a81832c9054b3aa644a9adcc8954a2467",
|
||||
"src/owned_socket_path.hpp": "8e784656d944c700f3ded383c93a8cadf846dabdaa4cc12673bcb637c26d9fd1",
|
||||
"src/passive_pointer_target.hpp": "ede36fd9fd6e95ae5923c751f12591084392be9d2270eb06ad64eb4f245169fa",
|
||||
"src/plugin.cpp": "712fd73ef8e9046e0fd91531b7bf5da50ce37ccca9df174160137e1924a74b09",
|
||||
"src/primary_trace.cpp": "e9468d1a3f3be2a90d47bf8c4a638ad8a60fe10b6297582ab7825751cb707aa9",
|
||||
"src/primary_trace.hpp": "8d62535fb0b24a02bb80d9a8dcd540b39204afb2f3b4bcb5cabd5275c3b5eaa7",
|
||||
"src/protocol.cpp": "bd083d65efb05e80946566dc535b1a6fadaa581c66ec327eff41796feba795b1",
|
||||
"src/seat_lifetime.hpp": "386cf5c72c178f8eec0824f2a7d46fa755b0bb000861f9a6e00802f6b81fb779",
|
||||
"src/session.cpp": "0105c7ba5f9e2dbdd9a21f48be0bc1f2bde930f6aa19f77e2216403d7790e4df",
|
||||
"src/status.cpp": "e46e81e5e8ae3b1f50af5dcaa6c1776e236321c788fe61b402c9923c797b1270",
|
||||
"tests/agent_keymap_test.py": "9b112f520a97a77a0d55f1009cd2594988d3c1e8e7bcaa39f213ffad2644dc56",
|
||||
"tests/cmake-api/CMakeLists.txt": "6a66c8f98023f029998af917fcd3ec6b1388607bdf7acaf5bdfaef9141962685",
|
||||
"tests/cmake-api/include/src/plugins/PluginAPI.hpp": "86c8ad51e668908d18928cef1b04e8e6d32a33894525640b52f0b31769c870a0",
|
||||
"tests/desktop_fault_policy_fixture.cpp": "ac24d675ebc64cc98148e852eb5aba5858bffc06332678d14276d04b317aaf15",
|
||||
"tests/desktop_fault_policy_test.py": "c3f624c0239036babd23eaaf1bb6b722f3a0c3321a5d3d6668ae7ab4910ca95c",
|
||||
"tests/drag_geometry_test.cpp": "d32ea649d008fc051fe18555d6fbc54ba5d057b61880b648c5df8aa076a53fbc",
|
||||
"tests/foreground_modifiers_test.py": "abf0ddde2d51c6639c8bdca8fdda51cdc8f922b57575a00fc8925c683ccc3bb2",
|
||||
"tests/foreground_route_test.cpp": "1024168828b13ee6abd8242941e73c042e9381b39108e3ada74823039c7e7932",
|
||||
"tests/input_client_deadline_test.cpp": "d62373a7815d531f1269c9a838773595f43e8bcef6482fa140edb162e59a6cac",
|
||||
"tests/input_grant_test.cpp": "1f327b7ee678189ebad6a50bb1b9bd06767521cebc9cfb478d92de4a8bf7e7fe",
|
||||
"tests/keyboard_layout_test.cpp": "3bb0fade4675d7ad92a81eb4a1c5201dd1d01bcc418b7ea59a4284dc235e5fb2",
|
||||
"tests/mock-hyprland/mock.hpp": "3aeb1a4b9d6b83506b66c129d3fa812330fad4509fe218a0dbb99dd2bb5b6319",
|
||||
"tests/mock-hyprland/src/config/values/types/BoolValue.hpp": "47cf2cca89f71a273573968cb9b8ba46a1496841d6c892756ebf123500a7ecb3",
|
||||
"tests/mock-hyprland/src/plugins/PluginAPI.hpp": "5654d90ec9090a88bea3d31f8a79617d4c79742b09068648e64448319d395110",
|
||||
"tests/owned_socket_path_test.cpp": "eaff6b5c6f148eca6c8650ee3dc212a5e892f002ee4f68b2a1290c7205ee7e42",
|
||||
"tests/passive_pointer_target_test.cpp": "2aeef1de1dc8932b26ab8c41b83fb16a4289ff96c177a5816088f07b9a168948",
|
||||
"tests/plugin_api_test.cpp": "1e7e200c309996ee945c88e172273ae942be2837e24564d422dee79d8b77d8a2",
|
||||
"tests/plugin_input_lifetime_test.cpp": "82e57b335ea1216ea24cca07fe4feebafebdebb779785fde20b8dab6ee222e1b",
|
||||
"tests/protocol_test.cpp": "119cfe0df81c0c00036a2d181764eda7601d6ee72459c2275a96226d4f670447",
|
||||
"tests/seat_lifetime_test.cpp": "b07570edbe0a142f97c54560eeb93e8327c435ab3b8cbc7496d55175e387b78a",
|
||||
"tests/status_test.cpp": "b8990efc53ec3820cfe498c920b9220c4b70615ad585468558e032e7619e32f3",
|
||||
"tests/transport_test.cpp": "deef114a950a27eaff0a530165ddf7db0bfc0fe7e8fb55bdbb66135c8e0c04c9",
|
||||
"verify.py": "fb35d62313ff4661f892f88666919b33b160f8b6d4fb2d5d52610708bf7f4a54"
|
||||
},
|
||||
"patch_sha256": "e74d9021111a5c217b12f3e13c1bbdd260e829c60db5fc2f7293ba4846e1a7d7",
|
||||
"schema": 1,
|
||||
"upstream_manifest_sha256": "54f514664c84e1358a435f29cd6befd5661b0b133d76997191c000b10f021a75",
|
||||
"upstream_revision": "cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7"
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
# Verified upstream kit plus a separately pinned Omarchy keyboard-remap patch.
|
||||
# Normal reruns need a fresh build directory; makepkg -e reuses verified extracted trees.
|
||||
# Profile kit: original source bytes and separately committed packaging tooling.
|
||||
# shellcheck shell=bash disable=SC2034,SC2154
|
||||
pkgname=cua-hyprland-plugin
|
||||
pkgver=0.26.1
|
||||
pkgrel=6
|
||||
pkgdesc='Cua input candidate for reviewed profile omarchy-hypr0562r3-aq0151-remaps'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/trycua/cua'
|
||||
license=('MIT')
|
||||
depends=('hyprland=0.56.2-3' 'aquamarine=0.15.1-1' 'glibc=2.44+r24+g16be1518495f-1' 'hyprcursor=0.1.13-7' 'hyprgraphics=0.5.1-4' 'hyprlang=0.6.8-5' 'hyprutils=0.14.2-1' 'libgcc=16.2.1+r23+gd564253eb6c8-1' 'libstdc++=16.2.1+r23+gd564253eb6c8-1' 'libxkbcommon=1.13.2-1' 'wayland=1.26.0-1' 'python>=3.11' 'binutils')
|
||||
makedepends=('cmake>=3.30' 'ninja' 'pkgconf' 'gcc' 'patch')
|
||||
options=('!strip' '!debug' '!lto')
|
||||
_stem='cua-hyprland-plugin-0.26.1-cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7'
|
||||
_archive_sha256='47bca9e018f32f4fcfe683f91c7475c60368f3b65d318cc35c1f2de88a4ee9ab'
|
||||
_kit_sha256='819779b93655d603d9ebb0d33ea052326c3374674a1d473886106af25e0fffdd'
|
||||
_profile_sha256='a2eade8b8195036be2c666788a1d28ac722403285a35987d92b3344e095f9a5e'
|
||||
_verifier_sha256='480f649cbd35a0ddc7f49dc0a3a44785402c9e0ec9653b0f8ea4965d52d7f900'
|
||||
_cxx="${CUA_RELEASE_CXX:-/usr/bin/g++}"
|
||||
_download_name='cua-hyprland-plugin-0.26.1-cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7-profile-omarchy-stable-20260910-kit-1.1.0-5dacea79a6a5927e59fca7b51e6c04e94fa45133ac1b8fb7f4e1f442d113eb9a-67f89ceb47edd973aa748820eadd02ddbfb3d3e4.tar.gz'
|
||||
_download_sha256='a89bfa7f6490f598719dfabdd2a5badc137e27b0764fd25e6a05e1f969433520'
|
||||
source=('https://github.com/trycua/cua/releases/download/cua-hyprland-kit-v1.1.0-omarchy-stable-20260910/cua-hyprland-plugin-0.26.1-cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7-profile-omarchy-stable-20260910-kit-1.1.0-5dacea79a6a5927e59fca7b51e6c04e94fa45133ac1b8fb7f4e1f442d113eb9a-67f89ceb47edd973aa748820eadd02ddbfb3d3e4.tar.gz'
|
||||
'PROFILE.json')
|
||||
noextract=("$_download_name")
|
||||
sha256sums=('a89bfa7f6490f598719dfabdd2a5badc137e27b0764fd25e6a05e1f969433520'
|
||||
'a2eade8b8195036be2c666788a1d28ac722403285a35987d92b3344e095f9a5e')
|
||||
|
||||
# Downstream inputs are also checked explicitly when makepkg integrity is skipped.
|
||||
declare -gA _downstream_sha256=(
|
||||
['independent-keymaps.patch']='e74d9021111a5c217b12f3e13c1bbdd260e829c60db5fc2f7293ba4846e1a7d7'
|
||||
['DOWNSTREAM-PROVENANCE.json']='e0c95350ec3ff2dd54a05e9377d79ddffffdb1cf494d05553a90207a3a919f7e'
|
||||
['downstream.py']='7c9725805ad038737bd4d346c85f27991ce9ddfff436a32ebf515cad1bf98ca1'
|
||||
['downstream_test.py']='7b9a992979b22a5e061fdf011ac6be59a3f1ae6e0d7c0a8ff631f74e71984c5a'
|
||||
)
|
||||
source+=('independent-keymaps.patch' 'DOWNSTREAM-PROVENANCE.json' 'downstream.py' 'downstream_test.py')
|
||||
sha256sums+=('e74d9021111a5c217b12f3e13c1bbdd260e829c60db5fc2f7293ba4846e1a7d7' 'e0c95350ec3ff2dd54a05e9377d79ddffffdb1cf494d05553a90207a3a919f7e' '7c9725805ad038737bd4d346c85f27991ce9ddfff436a32ebf515cad1bf98ca1' '7b9a992979b22a5e061fdf011ac6be59a3f1ae6e0d7c0a8ff631f74e71984c5a')
|
||||
|
||||
_verify_download() {
|
||||
python3 -I - "$SRCDEST/$_download_name" "$_download_sha256" "$srcdir" "$1" "$SRCDEST/PROFILE.json" <<'CUA_DOWNLOAD_PY'
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
expected = {'KIT-PROVENANCE.json': '7beb736adfd334eed52e84070177634269e3a09f8bb25971b38606933ff4c997', 'PKGBUILD': 'b945a6a6eda13d0e382770edd5419485e3196041956663eb38f5183b05d30db3', 'PROFILE-PKGBUILD.in': 'c350d1b2375946cb0166893d67a1fc01344ee5e3215bbe801b4d54bb361d6bce', 'PROFILE-USAGE.md': 'c14d8e8103fccab59e558758f4249f86bce700a8723cd4ba1b97b1102e02bbd2', 'PROFILE.json': '5dacea79a6a5927e59fca7b51e6c04e94fa45133ac1b8fb7f4e1f442d113eb9a', 'SHA256SUMS': '34a2126bcfab983f171382aafac3ef218475b652f4583c58f4a04088044cb935', 'SOURCE-PROVENANCE.json': '54f514664c84e1358a435f29cd6befd5661b0b133d76997191c000b10f021a75', 'cua-hyprland-plugin-0.26.1-cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7.tar.gz': '47bca9e018f32f4fcfe683f91c7475c60368f3b65d318cc35c1f2de88a4ee9ab', 'lifecycle.py': 'b18dceb8b8e05b93586ddd3a2f1d90d70ae1c36088e54fc05c89e08585290990', 'profile_bundle.py': 'ac883883814787da477c037f017939ee92c9fb5f46f373af7de1331b24f1f6da', 'profile_verify.py': '480f649cbd35a0ddc7f49dc0a3a44785402c9e0ec9653b0f8ea4965d52d7f900'}
|
||||
stem = 'cua-hyprland-plugin-0.26.1-cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7'
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise SystemExit(message)
|
||||
|
||||
def digest(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
archive, checksum, srcdir, mode, profile_path = sys.argv[1:]
|
||||
archive, srcdir = Path(archive), Path(srcdir)
|
||||
require(mode in {'check', 'extract'}, 'invalid kit verification mode')
|
||||
require(archive.is_file() and not archive.is_symlink(), 'outer archive must be a regular file')
|
||||
data = archive.read_bytes()
|
||||
require(digest(data) == checksum, 'outer archive checksum mismatch')
|
||||
payload = {}
|
||||
with tarfile.open(fileobj=io.BytesIO(data), mode='r:gz') as contents:
|
||||
for member in contents:
|
||||
require(member.isfile() and not member.issparse() and not member.pax_headers,
|
||||
'nonregular outer kit member')
|
||||
require(member.name in expected and member.name not in payload, 'outer kit inventory mismatch')
|
||||
content = contents.extractfile(member).read()
|
||||
require(digest(content) == expected[member.name], 'outer kit member checksum mismatch')
|
||||
payload[member.name] = content
|
||||
require(payload.keys() == expected.keys(), 'outer kit inventory mismatch')
|
||||
# Derive the reviewed Hyprland -3/Aquamarine 0.15.1 profile while preserving
|
||||
# upstream source/tooling and compiler, compositor, header and runtime hashes.
|
||||
profile_data = Path(profile_path).read_bytes()
|
||||
require(digest(profile_data) == 'a2eade8b8195036be2c666788a1d28ac722403285a35987d92b3344e095f9a5e',
|
||||
'local profile checksum mismatch')
|
||||
profile = json.loads(payload['PROFILE.json'])
|
||||
profile.update(profile_id='omarchy-hypr0562r3-aq0151-remaps', package_release=6)
|
||||
profile['hyprland']['package_version'] = '0.56.2-3'
|
||||
profile['runtime']['packages']['aquamarine'] = '0.15.1-1'
|
||||
require(json.loads(profile_data) == profile, 'local profile changes more than the reviewed package versions')
|
||||
payload['PROFILE.json'] = profile_data
|
||||
provenance = json.loads(payload['KIT-PROVENANCE.json'])
|
||||
provenance['profile_sha256'] = digest(profile_data)
|
||||
payload['KIT-PROVENANCE.json'] = (json.dumps(provenance, sort_keys=True, indent=2) + '\n').encode()
|
||||
recipe = payload['PKGBUILD'].decode()
|
||||
for old, new in [('pkgrel=2\n', 'pkgrel=6\n'), ('omarchy-stable-20260910', profile['profile_id']),
|
||||
('hyprland=0.56.2-2', 'hyprland=0.56.2-3'),
|
||||
('aquamarine=0.15.0-2', 'aquamarine=0.15.1-1'),
|
||||
('5dacea79a6a5927e59fca7b51e6c04e94fa45133ac1b8fb7f4e1f442d113eb9a', digest(profile_data)),
|
||||
('7beb736adfd334eed52e84070177634269e3a09f8bb25971b38606933ff4c997', digest(payload['KIT-PROVENANCE.json']))]:
|
||||
recipe = recipe.replace(old, new)
|
||||
payload['PKGBUILD'] = recipe.encode()
|
||||
payload['SHA256SUMS'] = ''.join(f'{digest(body)} {name}\n' for name, body in sorted(payload.items())
|
||||
if name != 'SHA256SUMS').encode()
|
||||
expected.update({'PROFILE.json': 'a2eade8b8195036be2c666788a1d28ac722403285a35987d92b3344e095f9a5e',
|
||||
'KIT-PROVENANCE.json': '819779b93655d603d9ebb0d33ea052326c3374674a1d473886106af25e0fffdd',
|
||||
'PKGBUILD': 'c3e4149eba3f7def10ef700b30b9d0abcea994e3dd32fb169014874a0b75687c',
|
||||
'SHA256SUMS': 'd9057a9534f7a820ee04cbf5d60db567c5072fa96d1f71030a6a85b4bb7ce881'})
|
||||
for name, content in payload.items():
|
||||
require(digest(content) == expected[name], 'derived kit checksum mismatch: ' + name)
|
||||
require(srcdir.is_dir() and not srcdir.is_symlink(), 'srcdir must be a real directory')
|
||||
kit, source = srcdir / 'cua-profile-kit', srcdir / stem
|
||||
if mode == 'extract':
|
||||
require(not kit.exists() and not kit.is_symlink() and not source.exists() and not source.is_symlink(),
|
||||
'prepare requires fresh kit and source destinations; use a clean srcdir')
|
||||
source_payload = {}
|
||||
with tarfile.open(fileobj=io.BytesIO(payload[stem + '.tar.gz']), mode='r:gz') as contents:
|
||||
for member in contents:
|
||||
require(member.isfile() and not member.issparse() and not member.pax_headers and
|
||||
member.name.startswith(stem + '/'), 'invalid source member')
|
||||
name = member.name[len(stem) + 1:]
|
||||
path = PurePosixPath(name)
|
||||
require(name and path.as_posix() == name and not path.is_absolute() and
|
||||
'..' not in path.parts and '\\' not in name and name not in source_payload,
|
||||
'unsafe or duplicate source path')
|
||||
source_payload[name] = contents.extractfile(member).read()
|
||||
kit.mkdir()
|
||||
source.mkdir()
|
||||
for name, content in payload.items():
|
||||
(kit / name).write_bytes(content)
|
||||
for name, content in source_payload.items():
|
||||
destination = source / name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_bytes(content)
|
||||
require(kit.is_dir() and not kit.is_symlink(), 'kit must be a real directory')
|
||||
require({path.name for path in kit.iterdir()} == expected.keys(), 'extracted kit inventory mismatch')
|
||||
for name, checksum in expected.items():
|
||||
path = kit / name
|
||||
require(path.is_file() and not path.is_symlink() and digest(path.read_bytes()) == checksum,
|
||||
'extracted kit checksum mismatch: ' + name)
|
||||
CUA_DOWNLOAD_PY
|
||||
}
|
||||
|
||||
_verify() {
|
||||
printf '%s %s\n' "$_download_sha256" "$SRCDEST/$_download_name" | sha256sum -c - || return 1
|
||||
_verify_download check || return 1
|
||||
# Explicit checks still apply to --skipinteg, --noextract and --repackage.
|
||||
printf '%s %s\n' "$_archive_sha256" "$srcdir/cua-profile-kit/${_stem}.tar.gz" | sha256sum -c - || return 1
|
||||
printf '%s %s\n' "$_kit_sha256" "$srcdir/cua-profile-kit/KIT-PROVENANCE.json" | sha256sum -c - || return 1
|
||||
printf '%s %s\n' "$_profile_sha256" "$srcdir/cua-profile-kit/PROFILE.json" | sha256sum -c - || return 1
|
||||
printf '%s %s\n' "$_verifier_sha256" "$srcdir/cua-profile-kit/profile_verify.py" | sha256sum -c - || return 1
|
||||
python3 "$srcdir/cua-profile-kit/profile_verify.py" --kit "$srcdir/cua-profile-kit" --kit-sha256 "$_kit_sha256" \
|
||||
--archive "$srcdir/cua-profile-kit/${_stem}.tar.gz" --source "$srcdir/$_stem" --cxx "$_cxx"
|
||||
}
|
||||
|
||||
_downstream() {
|
||||
local name
|
||||
for name in independent-keymaps.patch DOWNSTREAM-PROVENANCE.json downstream.py downstream_test.py; do
|
||||
printf '%s %s\n' "${_downstream_sha256[$name]}" "$SRCDEST/$name" | sha256sum -c - || return 1
|
||||
done
|
||||
python3 -B "$SRCDEST/downstream.py" "$1" \
|
||||
--pristine "$srcdir/$_stem" --source "$srcdir/omarchy-source" \
|
||||
--patch "$SRCDEST/independent-keymaps.patch" --manifest "$SRCDEST/DOWNSTREAM-PROVENANCE.json" \
|
||||
--kit "$srcdir/cua-profile-kit" --kit-sha256 "$_kit_sha256" \
|
||||
--archive "$srcdir/cua-profile-kit/${_stem}.tar.gz" --cxx "$_cxx" "${@:2}"
|
||||
}
|
||||
|
||||
prepare() {
|
||||
_verify_download extract || return 1
|
||||
_verify || return 1
|
||||
_downstream prepare
|
||||
}
|
||||
|
||||
build() {
|
||||
_verify || return 1
|
||||
_downstream check || return 1
|
||||
cmake -S "$srcdir/omarchy-source" -B "$srcdir/build" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER="$_cxx" \
|
||||
-DPKG_CONFIG_EXECUTABLE=/usr/bin/pkgconf -DPKG_CONFIG_ARGN= \
|
||||
-DPKG_CONFIG_USE_CMAKE_PREFIX_PATH=OFF -DCMAKE_PREFIX_PATH= \
|
||||
-DBUILD_TESTING=ON -DCUA_HYPRLAND_BUILD_PLUGIN=ON \
|
||||
-DCUA_HYPRLAND_EXPECTED_VERSION=0.56.2 \
|
||||
-DCUA_HYPRLAND_INPUT=ON -DCUA_HYPRLAND_TEST_INPUT=OFF \
|
||||
-DCUA_HYPRLAND_INPUT_TRACE=OFF -DCUA_HYPRLAND_TEST_OPERATOR_KEY= || return 1
|
||||
cmake --build "$srcdir/build"
|
||||
}
|
||||
|
||||
check() {
|
||||
_verify || return 1
|
||||
_downstream check || return 1
|
||||
python3 -B "$SRCDEST/downstream_test.py" || return 1
|
||||
(
|
||||
unset LD_PRELOAD FAKEROOTKEY FAKED_MODE
|
||||
ctest --test-dir "$srcdir/build" --output-on-failure --no-tests=error
|
||||
)
|
||||
}
|
||||
|
||||
package() {
|
||||
check || return 1
|
||||
_downstream build --build "$srcdir/build" --output "$srcdir/BUILD-PROVENANCE.json" || return 1
|
||||
install -Dm755 "$srcdir/build/cua-hyprland-plugin.so" \
|
||||
"$pkgdir/usr/lib/cua/hyprland/cua-hyprland-plugin.so" || return 1
|
||||
install -Dm644 "$srcdir/$_stem/LICENSE.md" \
|
||||
"$pkgdir/usr/share/licenses/$pkgname/LICENSE" || return 1
|
||||
install -Dm644 "$srcdir/$_stem/SOURCE-PROVENANCE.json" \
|
||||
"$pkgdir/usr/share/$pkgname/SOURCE-PROVENANCE.json" || return 1
|
||||
local name
|
||||
install -Dm644 "$srcdir/BUILD-PROVENANCE.json" "$pkgdir/usr/share/$pkgname/BUILD-PROVENANCE.json" || return 1
|
||||
for name in KIT-PROVENANCE.json PROFILE.json profile_verify.py; do
|
||||
install -Dm644 "$srcdir/cua-profile-kit/$name" "$pkgdir/usr/share/$pkgname/$name" || return 1
|
||||
done
|
||||
for name in DOWNSTREAM-PROVENANCE.json independent-keymaps.patch; do
|
||||
install -Dm644 "$SRCDEST/$name" "$pkgdir/usr/share/$pkgname/$name" || return 1
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"architecture": "x86_64",
|
||||
"compiler": {
|
||||
"comment": "GCC: (GNU) 16.2.1 20260810",
|
||||
"sha256": "f04191f6a7b2cd7d9a62e1745872b8a6088791e5af6955488c69c9b2c4668bc9",
|
||||
"version": "16.2.1 20260810"
|
||||
},
|
||||
"hyprland": {
|
||||
"header_version": "0.56.2",
|
||||
"headers_sha256": "88a6875af00203627b264a5c1f9908781be4ad8d9cee4e577fef174e72dd0e28",
|
||||
"package_version": "0.56.2-3",
|
||||
"sha256": "da8fcacf347bcbed83edc40108c6e2298da095e22246bd764e9bb382786cebb2"
|
||||
},
|
||||
"kit_version": "1.1.0",
|
||||
"package_release": 6,
|
||||
"profile_id": "omarchy-hypr0562r3-aq0151-remaps",
|
||||
"runtime": {
|
||||
"basename": "libstdc++.so.6.0.36",
|
||||
"packages": {
|
||||
"aquamarine": "0.15.1-1",
|
||||
"glibc": "2.44+r24+g16be1518495f-1",
|
||||
"hyprcursor": "0.1.13-7",
|
||||
"hyprgraphics": "0.5.1-4",
|
||||
"hyprlang": "0.6.8-5",
|
||||
"hyprutils": "0.14.2-1",
|
||||
"libgcc": "16.2.1+r23+gd564253eb6c8-1",
|
||||
"libstdc++": "16.2.1+r23+gd564253eb6c8-1",
|
||||
"libxkbcommon": "1.13.2-1",
|
||||
"wayland": "1.26.0-1"
|
||||
},
|
||||
"sha256": "f5fc7380f2ae46fa4053a64be04e7b98109f1066a4bbfff3c37042488aa0be0e"
|
||||
},
|
||||
"schema": 2,
|
||||
"source": {
|
||||
"archive_sha256": "47bca9e018f32f4fcfe683f91c7475c60368f3b65d318cc35c1f2de88a4ee9ab",
|
||||
"driver_version": "0.26.1",
|
||||
"manifest_sha256": "54f514664c84e1358a435f29cd6befd5661b0b133d76997191c000b10f021a75",
|
||||
"revision": "cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
# Optional Cua Hyprland plugin
|
||||
|
||||
This package targets **Omarchy x86_64**, with Inkscape `1.4.4-6` and two independent background-input lanes. Release `6` is an edge candidate for Aquamarine `0.15.1-1`; it retains the keyboard-remap patch introduced in release `5`, which includes the Omarchy patch for independent agent keymaps, operation-specific foreground checks, and compatible Num Lock state; the upstream native qualification below covers the unpatched source, not this change. Cua's native qualification is recorded in [the kit's qualification record](https://github.com/trycua/cua/releases/download/cua-hyprland-kit-v1.1.0-omarchy-stable-20260910/QUALIFICATION.md) and [Cua #3698](https://github.com/trycua/cua/pull/3698). Omabot replay and Omarchy's merge decision are recorded in [omarchy-pkgs #346](https://github.com/omacom/omarchy-pkgs/pull/346). Scheduling the recipe does not expand the qualified stable target.
|
||||
|
||||
The plugin is optional. Cua Driver works independently, and installation does not load the plugin or enable input. The package follows the normal edge-to-RC-to-stable promotion path instead of the fast release ring. Its PKGBUILD limits builds to x86_64. The upstream qualification covers the original stable profile; the updated Aquamarine profile needs its own Omabot validation before promotion.
|
||||
|
||||
## Source and build profile
|
||||
|
||||
The package uses the [Driver 0.26.1 plugin source](https://github.com/trycua/cua/releases/tag/cua-driver-rs-v0.26.1),
|
||||
including the [desktop-fault cleanup repair](https://github.com/trycua/cua/pull/3702).
|
||||
It is not a repackaging of the unmodified 0.24.0 plugin.
|
||||
|
||||
The qualified upstream Driver pairing is `cua-driver-bin 0.27.0-1`, with input protocol v3. Driver 0.27.0 contains the bounded stale-geometry retry validated with the upstream module; its production plugin source is the base for the downstream patch used here. Discovery protocol v2 is separate. A newer Driver release is a changed pairing and requires affected replay before promotion.
|
||||
|
||||
Profile `omarchy-hypr0562r3-aq0151-remaps`, kit tooling `1.1.0`, and package release `6` pin:
|
||||
|
||||
- Hyprland `0.56.2-3`, headers `0.56.2`, and measured executable/header hashes.
|
||||
- GCC `16.2.1 20260810`, including compiler bytes and emitted ELF identity.
|
||||
- Shared runtime `libstdc++.so.6.0.36`, its bytes, and exact ABI package versions, including Aquamarine `0.15.1-1`.
|
||||
|
||||
This profile derives from Cua's `omarchy-stable-20260910` profile. Arch's
|
||||
Hyprland `-3` package splits out `hyprpm` and changes package dependencies;
|
||||
its compositor executable and all 498 header/pkg-config files are byte-identical
|
||||
to `-2`. Both executables have SHA-256
|
||||
`da8fcacf347bcbed83edc40108c6e2298da095e22246bd764e9bb382786cebb2`.
|
||||
The checked-in `PROFILE.json` changes only the profile name, package release, and exact Hyprland and Aquamarine package versions. Compiler, libstdc++ runtime, upstream source, compositor executable, and header identities remain unchanged; the separately recorded patch changes the build source. The download wrapper verifies the
|
||||
original kit before deriving the updated profile, recipe, and provenance,
|
||||
then verifies every derived member against its recorded digest.
|
||||
|
||||
The native qualification below was recorded with package release `2` and
|
||||
Hyprland `-2`. The downstream keymap change and Aquamarine update need their own application and Driver replay before promotion. Hyprland `0.56.2-3` and Aquamarine `0.15.1-1` must both reach a destination channel before this artifact can be installed there; publication still follows edge → RC → stable.
|
||||
|
||||
The generated `PKGBUILD` identifies the immutable kit download, outer checksum,
|
||||
and member checksums. The kit records the full source and tooling revisions,
|
||||
profile digest, and source archive/manifest digests. Do not infer compatibility
|
||||
from a matching version label or substitute an unreviewed profile.
|
||||
|
||||
The download wrapper verifies its complete inventory before executing downloaded
|
||||
tooling. It preserves the source archive and its historical embedded verifier,
|
||||
but explicitly uses the new kit's `profile_verify.py`. Source integrity,
|
||||
package-owned headers, pkg-config selection, compiler probes, runtime equality,
|
||||
and production flags remain mandatory. Packaging runs all bundled CTests even
|
||||
with `--nocheck` or `--repackage`; `--skipinteg` does not bypass recipe checks.
|
||||
Production input is built in; experimental signed input and tracing are off.
|
||||
|
||||
The pristine upstream archive, manifest, and verifier remain unchanged. `independent-keymaps.patch` is applied to a separate source tree, and `DOWNSTREAM-PROVENANCE.json` pins the patch and every resulting source file. Build, check, and package revalidate both trees, including when makepkg integrity checks are skipped. `BUILD-PROVENANCE.json` records the upstream base under `source`, the applied change under `downstream`, and the final module digest; the downstream manifest and patch are installed beside it. This preserves the existing compiler, headers, runtime, and consumer checks without representing the modified module as an unmodified upstream build.
|
||||
|
||||
## Aquamarine dependency refresh
|
||||
|
||||
Release `5` required Aquamarine `0.15.0-2`. When the edge mirror moved to `0.15.1-1`, pacman could no longer resolve that dependency, even after a full database refresh. Release `6` derives a new profile from the same verified upstream kit and pins `0.15.1-1` in both the package dependencies and the installed compatibility verifier. Source, patch, compiler, compositor, headers, and libstdc++ hashes remain pinned; the original upstream qualification does not establish compatibility with the changed Aquamarine package.
|
||||
|
||||
A package release bump alone cannot repair future dependency drift: the checked-in profile and derived kit checksums must agree with the new environment, and affected native checks must pass before publication. Do not remove exact dependencies or selectively downgrade a library to bypass a mismatch.
|
||||
|
||||
## Keyboard behavior
|
||||
|
||||
Each background lane owns a canonical US keymap and independent modifier state. The physical keyboard keeps its layout, Compose key, and remaps. No installation or activation step edits `input:kb_*`. Existing Driver keycodes are interpreted by the agent keyboard, so this does not add Unicode, IME, or new Driver text routes.
|
||||
|
||||
Plain click, scroll, drag, and foreground activation do not require a canonical keyboard layout. Foreground keys still use the primary seat: the plugin checks the requested key and modifier sequence against its actual XKB map before activation or input. Unrelated remaps are accepted; a sequence whose symbols or modifier/lock transitions differ from the canonical meaning is refused with `unsupported_layout`. Arbitrary foreground layout translation remains outside protocol v3.
|
||||
|
||||
Foreground typing preserves Num Lock and admits a requested key sequence only when its symbols and shortcut semantics still match the canonical meaning. Num Lock does not block unaffected letters, top-row digits, Enter, or compatible shortcuts; a keypad sequence whose meaning changes is refused. Caps Lock, other unsupported lock states, held or latched modifiers, and nonzero layout groups remain guarded.
|
||||
|
||||
Both routes retain target/conflict checks and cancellation on desktop/keymap changes. `hyprctl -j cua:status` exposes `keyboard_layout_independent: true` and `foreground_numlock_compatible: true` for installers to distinguish this implementation from an older mapped module. The marker does not identify every future package revision; plugin updates still require a fresh desktop session.
|
||||
|
||||
## Historical upstream qualification
|
||||
|
||||
The initial app scope is native Wayland Inkscape `1.4.4-6` with the canonical
|
||||
US keymap. Two lanes require independent Driver processes and distinct native
|
||||
application clients, not merely two windows. This is concurrency inside one
|
||||
desktop account, not multi-user or mutually untrusted-agent isolation.
|
||||
|
||||
Cua's retained evidence covers background application effects, two-lane overlap,
|
||||
third-owner refusal, primary-input preservation, conflicts, stale targets and
|
||||
geometry, cancellation, desktop faults, recovery, and cold package transitions.
|
||||
Production-package app checks and independent primary observers are separate
|
||||
from trace-enabled diagnostics. Cua's retained canonical run
|
||||
`433ce968ee164d5e8e3226e800db93a6` recorded all 128 required cells: 87
|
||||
deliveries and 41 expected refusals, with no failures or skips; its completion
|
||||
report has SHA-256
|
||||
`1eda4cc008ea6b27d96c21d58f8041834398a43409642376bafe84ad38f0e112`.
|
||||
That historical run used source-built released Driver 0.24.0; earlier real-app
|
||||
checks separately used the then-published Omarchy `cua-driver-bin 0.24.0-1`
|
||||
executable. A separate later Omabot replay reported 124 passes and four
|
||||
failures, plus seven incomplete native cases. Cua subsequently passed those
|
||||
four cells with the repair candidate shipped in Driver 0.27.0 and passed all
|
||||
seven lifecycle cases. A final exact-release Fleet replay then passed all four
|
||||
cells with Driver and harness source `082de4344b731ae4738ddc6a6f13f21bb3c49a85`,
|
||||
released Driver binary SHA-256
|
||||
`bb1b65394e912246220f9f758c9efbbf6260cec16e3562e62a361fa95329377f`,
|
||||
and evidence SHA-256
|
||||
`b6c47278a3db398ecbb4d7aed2bce44a467e2af37e6d4ccfc236d1e07aa70af7`.
|
||||
See the linked qualification record and PR description for exact artifacts and observation limits. Omarchy replay of the 0.27.0 package pairing is recorded in #346; it does not qualify later Driver releases.
|
||||
|
||||
Duplicate motion notifications are retained and counted. They are acceptable
|
||||
only when pointer identity, coordinates, focus, held input, and foreground
|
||||
interaction remain unchanged. Actual motion—including moving away and back—
|
||||
fails isolation. After cancellation, an inert agent pointer may remain parked
|
||||
if held input is released and authority is revoked.
|
||||
|
||||
Current LibreOffice Calc `26.8`, Chromium/Electron raw background input,
|
||||
XWayland, Unicode/IME, non-US layouts, and modified pointer gestures are outside
|
||||
this profile. The plugin does not widen Driver's application admission.
|
||||
Foreground input, capture, and accessibility have separate contracts; a
|
||||
background refusal never authorizes a hidden foreground fallback or unlock.
|
||||
|
||||
## Omabot replay before promotion
|
||||
|
||||
Build the unsigned candidate in edge:
|
||||
|
||||
```sh
|
||||
./bin/build --package cua-hyprland-plugin --arch x86_64 --mirror edge
|
||||
```
|
||||
|
||||
In a fresh worker matching the reviewed profile:
|
||||
|
||||
1. Verify the downloaded kit and source identities against the reviewed recipe.
|
||||
Record the actual channel snapshot, Driver, compiler, compositor, runtime,
|
||||
applications, keymap, and resulting package/module hashes.
|
||||
2. Require all bundled tests and native compatibility checks. Do not weaken
|
||||
exact dependencies or replace the compositor to make the build pass.
|
||||
3. Install through pacman and activate in a fresh session. Replay the declared
|
||||
app, two-lane, refusal, primary-input, cancellation, and fault/recovery checks
|
||||
against the actual packaged Driver and module. A Cua Fleet result is not an
|
||||
Omabot result; matching source alone does not certify different binaries.
|
||||
4. Verify restart-based upgrade, rollback, removal, and reinstallation. Retain
|
||||
evidence that binds each result to the package and mapped module bytes.
|
||||
|
||||
After the exact package and Driver pairing passes, advance the signed artifact with `bin/repo advance --from edge --to rc --package cua-hyprland-plugin`, validate RC, and then use `bin/repo advance --from rc --to stable --package cua-hyprland-plugin`. Do not rebuild independently in RC or stable.
|
||||
|
||||
Portable tests, screenshots, health reports, and a successful build do not
|
||||
replace native qualification. Recheck the published Driver package before
|
||||
rollout and qualify any changed pairing explicitly.
|
||||
|
||||
## Activation, updates, and removal
|
||||
|
||||
The package installs the module at
|
||||
`/usr/lib/cua/hyprland/cua-hyprland-plugin.so` and provenance plus the consumer
|
||||
verifier under `/usr/share/cua-hyprland-plugin/`. There are no hooks, autoloading,
|
||||
configuration edits, or hot replacement.
|
||||
|
||||
Save your work and exit Hyprland before installing, replacing, or removing the
|
||||
package. Install the exact reviewed package from a text console, then start a
|
||||
fresh session. Before loading, run the consumer check with this package's derived
|
||||
kit-provenance digest:
|
||||
|
||||
```sh
|
||||
python3 /usr/share/cua-hyprland-plugin/profile_verify.py \
|
||||
--kit /usr/share/cua-hyprland-plugin \
|
||||
--kit-sha256 819779b93655d603d9ebb0d33ea052326c3374674a1d473886106af25e0fffdd \
|
||||
--consumer /usr/lib/cua/hyprland/cua-hyprland-plugin.so
|
||||
```
|
||||
|
||||
This check requires Python 3.11+, binutils `readelf`, and system `ldd`/`pacman`,
|
||||
not a compiler or headers. If it fails, leave the plugin unloaded. It verifies
|
||||
installed compatibility, not runtime mapping or input effects.
|
||||
|
||||
After that check passes in the fresh session, load the module explicitly:
|
||||
|
||||
```sh
|
||||
hyprctl plugin load /usr/lib/cua/hyprland/cua-hyprland-plugin.so
|
||||
hyprctl -j cua:status
|
||||
```
|
||||
|
||||
Loading alone does not enable input. Use Omarchy's explicit Cua Input toggle when available; it verifies the installed profile and loaded capability and removes the legacy copied toggle's keyboard override. If it reports an older mapped plugin, disable Cua Input and log out and back in before enabling it again. Never hot-unload and reload the module.
|
||||
|
||||
For manual activation, add only this plugin setting to a sourced Hyprland Lua configuration file, preserving all existing input settings:
|
||||
|
||||
```lua
|
||||
hl.config({
|
||||
plugin = { cua = { enabled = true } },
|
||||
})
|
||||
```
|
||||
|
||||
Then reload and inspect status:
|
||||
|
||||
```sh
|
||||
hyprctl reload
|
||||
hyprctl -j cua:status
|
||||
```
|
||||
|
||||
Continue only when status reports `keyboard_layout_independent: true`, `foreground_numlock_compatible: true`, input protocol v3, input capability, socket paths, and the expected compositor identity. Do not change `kb_layout`, `kb_options`, or NumLock for background input. If you previously followed the stock-US override instructions, remove only that Cua-specific override and reload to restore your underlying personal settings.
|
||||
|
||||
Start Driver with `CUA_DRIVER_RS_ENABLE_WAYLAND=1`. In a new disposable Inkscape document, test an admitted background key operation and pointer operation, then verify the result in both a fresh snapshot and a saved/reopened SVG. Driver text-route restrictions still apply. Never test against an existing document or automatically replay an action with a partial or unknown outcome.
|
||||
|
||||
To disable input, turn the Cua Input toggle off, or remove the manual `plugin.cua.enabled` setting and reload. Confirm that status reports input disabled. Retained inert agent pointers can remain until the compositor exits; disabling input does not unload the mapped module.
|
||||
|
||||
Before an incompatible desktop update, remove operator-added plugin activation
|
||||
settings, save work, and exit the graphical session. From a text console, run
|
||||
`sudo pacman -R cua-hyprland-plugin`, then apply the normal desktop update and
|
||||
verify a fresh session without the plugin. Declining removal preserves the
|
||||
dependency refusal. Disabling input alone leaves exact dependencies installed;
|
||||
do not force an upgrade past them.
|
||||
|
||||
Retain the previous package with its matching compositor, runtime, Driver, and
|
||||
provenance as a rollback set. Restore a consistent set outside the graphical
|
||||
session, then repeat the fresh-session consumer and app checks. Do not hot
|
||||
unload/reload or replace a mapped module.
|
||||
|
||||
## Ownership and publication
|
||||
|
||||
The [agreed ownership split](https://github.com/omacom/omarchy-pkgs/pull/346#issuecomment-5612834061)
|
||||
assigns profiles, build kits, plugin fixes, and native input evidence to Cua.
|
||||
Francesco (@f-trycua) is the Cua contact through this PR. Omarchy owns package
|
||||
integration, dependency-change detection, Omabot validation, and signing and
|
||||
publication decisions. Spencer (@spencerbull) and Emir (@emirb) jointly own
|
||||
that Omarchy package and release path. Maintenance is best effort, with no
|
||||
turnaround commitment.
|
||||
|
||||
Edge detects upcoming incompatibilities; RC validates the intended stable
|
||||
environment. Mirror/channel changes and changes to ABI dependencies, Driver,
|
||||
or admitted apps request a new candidate and affected qualification. They do
|
||||
not establish compatibility or authorize additional publication channels.
|
||||
|
||||
This package participates in scheduled builds, but has no upstream polling, AUR synchronization, or automatic rebuild bump. A checked-in version change or missing artifact can queue it for the normal release pipeline. Build selection, promotion, `push`, and `upload-prebuilt` do not supply native qualification or authorize a broader support claim. Do not silently substitute newly rebuilt bytes during signing or publication.
|
||||
|
||||
Before calling a release complete, install the signed published package on a fresh consumer, verify its signature and package/module digests, and perform a short activation, background-action, and cleanup smoke. Edge and RC artifacts are compatibility checkpoints, not qualified support for those environments.
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify the Omarchy patch separately from the unchanged upstream source kit."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def inventory(root):
|
||||
require(root.is_dir() and not root.is_symlink(), "source must be a real directory")
|
||||
result = {}
|
||||
for path in root.rglob("*"):
|
||||
require(not path.is_symlink() and (path.is_dir() or path.is_file()), "nonregular source entry")
|
||||
if path.is_file():
|
||||
result[path.relative_to(root).as_posix()] = digest(path)
|
||||
return result
|
||||
|
||||
|
||||
def verify_inputs(pristine, patch, manifest):
|
||||
require(manifest["schema"] == 1, "unsupported downstream schema")
|
||||
require(patch.is_file() and not patch.is_symlink() and digest(patch) == manifest["patch_sha256"],
|
||||
"downstream patch checksum mismatch")
|
||||
require(digest(pristine / "SOURCE-PROVENANCE.json") == manifest["upstream_manifest_sha256"],
|
||||
"downstream base manifest mismatch")
|
||||
require(manifest["files"], "empty downstream inventory")
|
||||
for name in manifest["files"]:
|
||||
path = PurePosixPath(name)
|
||||
require(name and not path.is_absolute() and path.as_posix() == name and
|
||||
".." not in path.parts and "\\" not in name, "invalid downstream path")
|
||||
|
||||
|
||||
def verify_tree(source, manifest):
|
||||
require(inventory(source) == manifest["files"], "patched source inventory/checksum mismatch")
|
||||
|
||||
|
||||
def prepare(pristine, source, patch, manifest):
|
||||
require(not source.exists() and not source.is_symlink(), "patched source requires a fresh destination")
|
||||
shutil.copytree(pristine, source)
|
||||
subprocess.run(["patch", "--batch", "--fuzz=0", "-p1", "-i", str(patch.resolve())], cwd=source, check=True)
|
||||
verify_tree(source, manifest)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("mode", choices=("prepare", "check", "build"))
|
||||
parser.add_argument("--pristine", required=True, type=Path)
|
||||
parser.add_argument("--source", required=True, type=Path)
|
||||
parser.add_argument("--patch", required=True, type=Path)
|
||||
parser.add_argument("--manifest", required=True, type=Path)
|
||||
parser.add_argument("--kit", required=True, type=Path)
|
||||
parser.add_argument("--kit-sha256", required=True)
|
||||
parser.add_argument("--archive", required=True, type=Path)
|
||||
parser.add_argument("--cxx", required=True, type=Path)
|
||||
parser.add_argument("--build", type=Path)
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
# PKGBUILD authenticates the verifier and this helper before execution.
|
||||
spec = importlib.util.spec_from_file_location("upstream_profile", args.kit / "profile_verify.py")
|
||||
upstream = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(upstream)
|
||||
profile, kit = upstream.verify_kit(args.kit, args.kit_sha256)
|
||||
base = upstream.verify_archive(args.archive, profile)
|
||||
require(upstream.verify_source(args.pristine, profile) == base, "upstream source identity mismatch")
|
||||
manifest = upstream.read_json(args.manifest.read_bytes())
|
||||
verify_inputs(args.pristine, args.patch, manifest)
|
||||
if args.mode == "prepare":
|
||||
prepare(args.pristine, args.source, args.patch, manifest)
|
||||
else:
|
||||
verify_tree(args.source, manifest)
|
||||
if args.mode == "build":
|
||||
require(args.build is not None and args.output is not None, "build evidence requires output")
|
||||
native = upstream.verify_native(args.cxx, profile)
|
||||
native["module_sha256"] = upstream.verify_build(args.build, args.source, args.cxx, profile)
|
||||
native["module_runtime_sha256"] = profile["runtime"]["sha256"]
|
||||
args.output.write_bytes(upstream.json_bytes(dict(native, source=base, profile=profile,
|
||||
kit=kit, downstream=manifest)))
|
||||
except (ValueError, KeyError, TypeError, OSError, subprocess.CalledProcessError) as error:
|
||||
parser.exit(1, f"error: {error}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise downstream integrity with real patch application and tampering."""
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import downstream
|
||||
|
||||
|
||||
class DownstreamTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.pristine = self.root / "pristine"
|
||||
self.pristine.mkdir()
|
||||
(self.pristine / "SOURCE-PROVENANCE.json").write_text("upstream\n")
|
||||
(self.pristine / "input.cpp").write_text("old\n")
|
||||
self.patch = self.root / "change.patch"
|
||||
self.patch.write_text("--- a/input.cpp\n+++ b/input.cpp\n@@ -1 +1 @@\n-old\n+new\n")
|
||||
self.source = self.root / "patched"
|
||||
self.manifest = {
|
||||
"schema": 1,
|
||||
"patch_sha256": downstream.digest(self.patch),
|
||||
"upstream_manifest_sha256": downstream.digest(self.pristine / "SOURCE-PROVENANCE.json"),
|
||||
"files": {"SOURCE-PROVENANCE.json": downstream.digest(self.pristine / "SOURCE-PROVENANCE.json"),
|
||||
"input.cpp": hashlib.sha256(b"new\n").hexdigest()},
|
||||
}
|
||||
|
||||
def test_applies_patch_without_changing_upstream(self):
|
||||
downstream.verify_inputs(self.pristine, self.patch, self.manifest)
|
||||
downstream.prepare(self.pristine, self.source, self.patch, self.manifest)
|
||||
self.assertEqual((self.pristine / "input.cpp").read_text(), "old\n")
|
||||
self.assertEqual((self.source / "input.cpp").read_text(), "new\n")
|
||||
|
||||
def test_changed_patch_refuses(self):
|
||||
self.patch.write_text(self.patch.read_text().replace("+new", "+bad"))
|
||||
with self.assertRaisesRegex(ValueError, "patch checksum"):
|
||||
downstream.verify_inputs(self.pristine, self.patch, self.manifest)
|
||||
|
||||
def test_changed_base_manifest_refuses(self):
|
||||
(self.pristine / "SOURCE-PROVENANCE.json").write_text("different\n")
|
||||
with self.assertRaisesRegex(ValueError, "base manifest"):
|
||||
downstream.verify_inputs(self.pristine, self.patch, self.manifest)
|
||||
|
||||
def test_tampered_missing_and_extra_files_refuse(self):
|
||||
downstream.prepare(self.pristine, self.source, self.patch, self.manifest)
|
||||
file = self.source / "input.cpp"
|
||||
for content in ("tampered\n", None):
|
||||
if content is None:
|
||||
file.unlink()
|
||||
else:
|
||||
file.write_text(content)
|
||||
with self.assertRaisesRegex(ValueError, "inventory/checksum"):
|
||||
downstream.verify_tree(self.source, self.manifest)
|
||||
file.write_text("new\n")
|
||||
(self.source / "unexpected.cpp").write_text("extra\n")
|
||||
with self.assertRaisesRegex(ValueError, "inventory/checksum"):
|
||||
downstream.verify_tree(self.source, self.manifest)
|
||||
|
||||
def test_symlink_and_reused_destination_refuse(self):
|
||||
downstream.prepare(self.pristine, self.source, self.patch, self.manifest)
|
||||
with self.assertRaisesRegex(ValueError, "fresh destination"):
|
||||
downstream.prepare(self.pristine, self.source, self.patch, self.manifest)
|
||||
file = self.source / "input.cpp"
|
||||
file.unlink()
|
||||
file.symlink_to(self.pristine / "input.cpp")
|
||||
with self.assertRaisesRegex(ValueError, "nonregular"):
|
||||
downstream.verify_tree(self.source, self.manifest)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,20 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "efe34ef2f0615a8ec251ef0b632d2999c906df47"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"json": "https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable",
|
||||
"path": "version",
|
||||
"fields": {
|
||||
"commit": "commitSha"
|
||||
},
|
||||
"variables": {
|
||||
"_commit": "{commit}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "cursor-bin",
|
||||
"commit": "a87a0de17b0d8176ee4f046499b1e6e4b37cc422"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,53 @@
|
||||
# Maintainer: Gunther Schulz <dev@guntherschulz.de>
|
||||
|
||||
pkgname=cursor-bin
|
||||
pkgver=3.20.10
|
||||
pkgrel=1
|
||||
pkgver=3.21.16
|
||||
pkgrel=3
|
||||
pkgdesc='AI-first coding environment'
|
||||
arch=('x86_64')
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://www.cursor.com"
|
||||
license=('LicenseRef-Cursor_EULA')
|
||||
# upstream uses Electron newer than internal VSCode
|
||||
_electron=electron42
|
||||
depends=(xdg-utils ripgrep $_electron nodejs
|
||||
'gcc-libs' 'hicolor-icon-theme' 'libxkbfile')
|
||||
depends=(xdg-utils gcc-libs hicolor-icon-theme libxkbfile)
|
||||
depends_x86_64=(ripgrep $_electron nodejs)
|
||||
# ARM retains bundled Electron because electron42 is unavailable in ALARM.
|
||||
depends_aarch64=(
|
||||
alsa-lib at-spi2-core ca-certificates cairo curl dbus expat glib2 glibc gtk3
|
||||
libcups libsecret libx11 libxcb libxcomposite libxdamage libxext libxfixes
|
||||
libxkbcommon libxrandr mesa nspr nss pango systemd-libs which
|
||||
)
|
||||
options=(!strip !debug) # Don't break ext of VSCode
|
||||
_commit=d6f462cdd0a6a6d1cff570daf980e671d0a63ded
|
||||
source=("https://downloads.cursor.com/production/${_commit}/linux/x64/deb/amd64/deb/cursor_${pkgver}_amd64.deb"
|
||||
_commit=8ae78e8eee1e63479c7e0504b664bc0a80c6800f
|
||||
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=('SKIP'
|
||||
'937299c6cb6be2f8d25f7dbc95cf77423875c5f8353b8bd6cd7cc8e5603cbf8405b14dbf8bd615db2e3b36ed680fc8e1909410815f7f8587b7267a699e00ab37'
|
||||
'793f9ff6306e3992ac89802d98110cba288ea1181a901467333293b7d76182ef9792c2a39ff49d9347a18a174b1f42bc58862091dff583f4146c2704eea28033'
|
||||
'e79fe7659f59d1ae02fc68816399bfd31587315df6cdb6ccf1d0ca76f7cdc692c2a42b30591c0091147bd97ef14b1c7745dc26bd7cb3ea6bba45698e5044fa2a')
|
||||
sha512sums[0]=cee826a72bdc9502b968cd1c3b185fa576861a92dfa5a751b0422fceb23e45328fed7e16917cebe605c5fc674b86f0717bc74c03a0545b9242bcc635861d246f
|
||||
noextract=(cursor_${pkgver}_amd64.deb) # avoid double tarball
|
||||
sha512sums_x86_64=('032c86a5d51f154ce36b1a0bf34aa06b2d117666b4372a787ea3117fc0b2b1686e952c721388f2eaf7787f2e0581378c98608048126f7470df2e85e9dbd75ca4' '937299c6cb6be2f8d25f7dbc95cf77423875c5f8353b8bd6cd7cc8e5603cbf8405b14dbf8bd615db2e3b36ed680fc8e1909410815f7f8587b7267a699e00ab37' '793f9ff6306e3992ac89802d98110cba288ea1181a901467333293b7d76182ef9792c2a39ff49d9347a18a174b1f42bc58862091dff583f4146c2704eea28033' 'e79fe7659f59d1ae02fc68816399bfd31587315df6cdb6ccf1d0ca76f7cdc692c2a42b30591c0091147bd97ef14b1c7745dc26bd7cb3ea6bba45698e5044fa2a')
|
||||
source_aarch64=("https://downloads.cursor.com/production/${_commit}/linux/arm64/deb/arm64/deb/cursor_${pkgver}_arm64.deb")
|
||||
sha512sums_aarch64=('88a163c130e7ee8d9f29b93ccf1258e9f5eb0138d4de5d2333f5bc2992c0a4d0c3a72e8f5912b2e9ac9819a8d758de8e7249cd33bdf3ac631271001ca92700f7')
|
||||
noextract=(cursor_${pkgver}_amd64.deb cursor_${pkgver}_arm64.deb) # avoid double tarball
|
||||
_app=usr/share/cursor/resources/app
|
||||
package() {
|
||||
# Exclude electron
|
||||
bsdtar -xOf ${noextract[0]} data.tar.xz | tar -xJf - -C "$pkgdir" \
|
||||
--exclude 'usr/share/cursor/[^r]*' --exclude 'usr/share/cursor/*.pak'
|
||||
local deb="cursor_${pkgver}_amd64.deb"
|
||||
local -a excludes=(--exclude 'usr/share/cursor/[^r]*' --exclude 'usr/share/cursor/*.pak')
|
||||
if [[ "$CARCH" == aarch64 ]]; then
|
||||
deb="cursor_${pkgver}_arm64.deb"
|
||||
# Keep bundled Electron; omit Debian's AppArmor and sysctl settings.
|
||||
excludes=(--exclude './etc')
|
||||
fi
|
||||
bsdtar -xOf "${srcdir}/${deb}" data.tar.xz | tar -xJf - -C "$pkgdir" "${excludes[@]}"
|
||||
cd "$pkgdir"
|
||||
# Disable Cursor's bundled updater; Omarchy manages updates via pacman (#238).
|
||||
sed -i '/^[[:space:]]*"\(backupUpdateUrl\|updateUrl\)":/d' \
|
||||
"${_app}/product.json"
|
||||
mv usr/share/zsh/{vendor-completions,site-functions}
|
||||
if [[ "$CARCH" == aarch64 ]]; then
|
||||
install -d usr/bin
|
||||
ln -s /usr/share/cursor/bin/cursor usr/bin/cursor
|
||||
# Use Electron's unprivileged namespace sandbox.
|
||||
chmod 0755 usr/share/cursor/chrome-sandbox
|
||||
return
|
||||
fi
|
||||
ln -sf /usr/bin/node ${_app}/resources/helpers/node
|
||||
install -Dm755 "${srcdir}/rg.sh" ${_app}/node_modules/@vscode/ripgrep/bin/rg
|
||||
ln -sf /usr/bin/xdg-open ${_app}/node_modules/open/xdg-open
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "954e5556aa88f2309b86992b231c8b76f273cfb0"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"regex": "https://cursor.com/install",
|
||||
"pattern": "https://downloads\\.cursor\\.com/lab/(?P<version>[0-9]{4}\\.[0-9]{2}\\.[0-9]{2})-(?P<hash>[a-f0-9]+)/",
|
||||
"version": "{version}.1.{hash}",
|
||||
"sequence": true
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "cursor-cli",
|
||||
"commit": "954e5556aa88f2309b86992b231c8b76f273cfb0"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Ismet Togay <ismet.togay at gmail dot com>
|
||||
# Contributor: Christopher Cooper <christopher@cg505.com>
|
||||
pkgname=cursor-cli
|
||||
pkgver=2026.08.25.1.3e8eec8
|
||||
pkgver=2026.09.18.1.9a7762b
|
||||
# Upstream is YYYY.MM.DD-<hash>. pkgver cannot contain hyphens, and hashes are
|
||||
# not monotonically ordered, so pkgver is YYYY.MM.DD.<n>.<hash>: n resets to 1
|
||||
# on a new date and increments when the same date gets a new hash.
|
||||
@@ -25,8 +25,8 @@ source_x86_64=("cursor-cli-${_upstream_ver}-x86_64.tar.gz::https://downloads.cur
|
||||
source_aarch64=("cursor-cli-${_upstream_ver}-aarch64.tar.gz::https://downloads.cursor.com/lab/${_upstream_ver}/linux/arm64/agent-cli-package.tar.gz")
|
||||
b2sums=('d241ee9895bdb1c17514438fde8528222a8f2326568bd7a033d7a1b11432ce6b4575ff1a50625764bfe6bc6f8a9dc060f7439c3be7e95f8fd02912cdd37a011d'
|
||||
'1928e04c713e13911ea607f84c3e4a2fed1f76af9795503811078f43d2b53c753e28b2233e553fc17e766831800fb0dbc272aad2a80b387f95ba6071d7d4116a')
|
||||
b2sums_x86_64=('cd5485f7524688e1a688daa2b64669c76bedcdd9ab87638bac78f9b42c2442bd5000559920a2f5171e00b5eb7fcf737f9111ef296f1eba40369cebf3279ee0a9')
|
||||
b2sums_aarch64=('191ff1c538f294134d93d501e9ca68cc6d4f8101cb1cee449753dfefcd9039daecd7bf9f9f2baf9ee9262f40eea19aaf15f834c9abb56d967fb48a3a3f23b8ea')
|
||||
b2sums_x86_64=('3fccee6929df1042d03461895e56c222a996d3ae9e4f9c61dcc5e6ab7b1d075d3265c21a44f48dd94de0dcde4f8012cc39bfbf323e2bb0c6106cac79885c35ae')
|
||||
b2sums_aarch64=('3d3bb0a3cb7e2409acf4925f207eaa4e3f41782c3c947e2834b69664b116b972da0b67eea17147fc524ea248af9919494570adc7c48d12c44f12deca17ba2c28')
|
||||
|
||||
prepare() {
|
||||
# Block cursor-agent auto-updates by making its versions directory
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "80626b9efefc215d55eede4330d56c017a522682"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "dropbox/dbxcli",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "dbxcli-bin",
|
||||
"commit": "80626b9efefc215d55eede4330d56c017a522682"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
_pkgname="dbxcli"
|
||||
pkgname="${_pkgname}-bin"
|
||||
pkgver=3.7.2
|
||||
pkgver=3.7.3
|
||||
pkgrel=1
|
||||
pkgdesc="A command line client for Dropbox built using the Go SDK"
|
||||
arch=(
|
||||
@@ -33,9 +33,9 @@ source_armv7h=(
|
||||
source_x86_64=(
|
||||
"${url}/releases/download/v${pkgver}/${_pkgname}_${pkgver}_linux_amd64.tar.gz"
|
||||
)
|
||||
sha256sums_aarch64=('fc451469c87ad0e4f2d3201f6e36f2b57c1119e5c0adb5ebaec6182b3eb378ad')
|
||||
sha256sums_armv7h=('22f21d8b40dd23b5777ffb0ec07696d135d2910daa84f46679231a573aeb9899')
|
||||
sha256sums_x86_64=('1b1fa67fb3d3f6e2940566afdb84f072a21804ddfdb4f0dfade385ec0683ee63')
|
||||
sha256sums_aarch64=('9d654da62a1ac10c9e32ee8f66fa6cc8d88ed29bc95555435a5ce4255eb4b96a')
|
||||
sha256sums_armv7h=('8067cee274dc2f062a06ceda26200c44d9251336ec235f7d7a3826743c9a379e')
|
||||
sha256sums_x86_64=('fee977ce4144174356cd7d1bae0b546aecad2570f14666bd44417e96af943484')
|
||||
|
||||
prepare() {
|
||||
local source_array="source_${CARCH}[0]"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
pkgname=dell-xps-touchpad-haptics
|
||||
pkgver=1.0.0
|
||||
pkgrel=3
|
||||
pkgrel=4
|
||||
pkgdesc="Synaptics haptic touchpad presets for Dell XPS on Omarchy"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/omacom-io/omarchy-pkgs"
|
||||
|
||||
@@ -3,6 +3,7 @@ _default_level="high"
|
||||
_env_path="/etc/dell-xps-touchpad-haptics.env"
|
||||
_legacy_env_path="/etc/omarchy-dell-haptic-touchpad.env"
|
||||
_legacy_override_dir="/etc/systemd/system/dell-xps-haptic-touchpad.service.d"
|
||||
_runuser_path="/usr/bin/runuser"
|
||||
|
||||
_existing_home() {
|
||||
local line value
|
||||
@@ -124,18 +125,13 @@ _ensure_user_config() {
|
||||
local config_dir="$home/.config/omarchy"
|
||||
local config_path="$config_dir/dell-haptic.conf"
|
||||
|
||||
if [[ ! -f $config_path ]] && ! env HOME="$home" USER="$user" LOGNAME="$user" \
|
||||
if [[ ! -f $config_path ]] && ! "$_runuser_path" --user "$user" -- \
|
||||
/usr/bin/env HOME="$home" USER="$user" LOGNAME="$user" \
|
||||
/usr/bin/dell-xps-touchpad-haptics set "$_default_level"; then
|
||||
echo ":: Failed to create ${config_path} for user '$user'." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -f $config_path ]]; then
|
||||
chown "$user:$user" "$home/.config" 2>/dev/null || true
|
||||
chown "$user:$user" "$config_dir" 2>/dev/null || true
|
||||
chown "$user:$user" "$config_path" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"source": "local",
|
||||
"origin": {
|
||||
"aur": "dotnet-core-bin",
|
||||
"commit": "2c499d7ce634efb8e93eee4c4239490b02e98e09"
|
||||
},
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"json": "https://builds.dotnet.microsoft.com/dotnet/release-metadata/10.0/releases.json",
|
||||
"path": "latest-sdk",
|
||||
"fields": {
|
||||
"runtime": "latest-runtime"
|
||||
},
|
||||
"variables": {
|
||||
"_runtimever": "{runtime}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
# Maintainer: Attila Greguss <floyd0122[at]gmail[dot]com>
|
||||
# Co-Maintainer: Nate Plumm <nate[at]ceresta[dot]com>
|
||||
|
||||
pkgbase=dotnet-core-bin
|
||||
pkgname=(
|
||||
'dotnet-host-bin'
|
||||
'aspnet-runtime-bin'
|
||||
'dotnet-runtime-bin'
|
||||
'dotnet-sdk-bin'
|
||||
'dotnet-targeting-pack-bin'
|
||||
'aspnet-targeting-pack-bin'
|
||||
)
|
||||
# Version the split family by SDK release; dependencies expose runtime versions.
|
||||
pkgver=10.0.401
|
||||
_runtimever=10.0.12
|
||||
_sdkver=$pkgver
|
||||
_short_ver=10.0
|
||||
pkgrel=2
|
||||
arch=('aarch64')
|
||||
url='https://www.microsoft.com/net/core'
|
||||
license=('MIT')
|
||||
options=('staticlibs' '!debug')
|
||||
source=('dotnet.sh')
|
||||
source_aarch64=("https://builds.dotnet.microsoft.com/dotnet/Sdk/${_sdkver}/dotnet-sdk-${_sdkver}-linux-arm64.tar.gz")
|
||||
sha512sums=('768151c7179fb6a126b3de9cae01e363e8894f6fab384b1e2c5066c2adca4578638983b1b62aea10dd18045e6d6e8f8ea13280481134de94f004a118919b2c06')
|
||||
sha512sums_aarch64=('58ace73ced6b4360754689a686bdfb8a317f4da6cb8bb416dbc7d0ba9f47e43e3c09f5eb1f1a1cfaacbd10df9558da4882bf2a5e195d6ab56a02c1f9f76102ed')
|
||||
|
||||
# Keep each split package's notices usable when installed independently.
|
||||
_install_license() {
|
||||
install -Dm644 LICENSE.txt "$pkgdir/usr/share/licenses/$pkgname/LICENSE.txt"
|
||||
install -Dm644 ThirdPartyNotices.txt "$pkgdir/usr/share/licenses/$pkgname/ThirdPartyNotices.txt"
|
||||
}
|
||||
|
||||
package_dotnet-host-bin() {
|
||||
pkgdesc='A generic driver for the .NET Core Command Line Interface (binary)'
|
||||
provides=("dotnet-host" "dotnet-host=${_runtimever}")
|
||||
conflicts=('dotnet-host')
|
||||
depends=(
|
||||
'libgcc'
|
||||
'libstdc++'
|
||||
'glibc'
|
||||
)
|
||||
|
||||
install -dm 755 "${pkgdir}"/usr/{bin,lib,share/{dotnet,dnx}}
|
||||
cp -dr --no-preserve='ownership' dotnet host dnx "${pkgdir}"/usr/share/dotnet/
|
||||
_install_license
|
||||
ln -sf /usr/share/dotnet/dotnet "${pkgdir}"/usr/bin/dotnet
|
||||
ln -sf /usr/share/dotnet/dnx "${pkgdir}"/usr/bin/dnx
|
||||
ln -sf /usr/share/dotnet/host/fxr/"${_runtimever}"/libhostfxr.so "${pkgdir}"/usr/lib/libhostfxr.so
|
||||
install -Dm 644 "${srcdir}"/dotnet.sh -t "${pkgdir}"/etc/profile.d/
|
||||
}
|
||||
|
||||
package_dotnet-runtime-bin() {
|
||||
pkgdesc='The .NET Core runtime (binary)'
|
||||
depends=(
|
||||
"dotnet-host>=${_runtimever}"
|
||||
'libgcc'
|
||||
'libstdc++'
|
||||
'glibc'
|
||||
'icu'
|
||||
'libunwind'
|
||||
'zlib'
|
||||
'openssl'
|
||||
)
|
||||
optdepends=('lttng-ust2.12: CoreCLR tracing')
|
||||
provides=("dotnet-runtime=${_runtimever}" "dotnet-runtime-${_short_ver}")
|
||||
conflicts=("dotnet-runtime=${_runtimever}" "dotnet-runtime-${_short_ver}")
|
||||
|
||||
install -dm 755 "${pkgdir}"/usr/share/{dotnet/shared,licenses}
|
||||
cp -dr --no-preserve='ownership' shared/Microsoft.NETCore.App "${pkgdir}"/usr/share/dotnet/shared/
|
||||
_install_license
|
||||
}
|
||||
|
||||
package_aspnet-runtime-bin() {
|
||||
pkgdesc='The ASP.NET Core runtime (binary)'
|
||||
depends=('dotnet-runtime-bin')
|
||||
provides=("aspnet-runtime=${_runtimever}" "aspnet-runtime-${_short_ver}")
|
||||
conflicts=("aspnet-runtime=${_runtimever}" "aspnet-runtime-${_short_ver}")
|
||||
|
||||
install -dm 755 "${pkgdir}"/usr/share/{dotnet/shared,licenses}
|
||||
cp -dr --no-preserve='ownership' shared/Microsoft.AspNetCore.App "${pkgdir}"/usr/share/dotnet/shared/
|
||||
_install_license
|
||||
}
|
||||
|
||||
package_dotnet-sdk-bin() {
|
||||
pkgdesc='The .NET Core SDK (binary)'
|
||||
depends=(
|
||||
'glibc'
|
||||
'libgcc'
|
||||
'libstdc++'
|
||||
'dotnet-runtime-bin'
|
||||
'dotnet-targeting-pack-bin'
|
||||
'aspnet-runtime-bin'
|
||||
'aspnet-targeting-pack-bin'
|
||||
)
|
||||
provides=("dotnet-sdk-bin" "dotnet-sdk=${pkgver}" "dotnet-sdk-${_short_ver}=${pkgver}")
|
||||
conflicts=("dotnet-sdk-bin" "dotnet-sdk=${pkgver}" "dotnet-sdk-${_short_ver}")
|
||||
|
||||
install -dm 755 "${pkgdir}"/usr/share/{dotnet,licenses}
|
||||
cp -dr --no-preserve='ownership' sdk sdk-manifests templates "${pkgdir}"/usr/share/dotnet/
|
||||
_install_license
|
||||
}
|
||||
|
||||
package_dotnet-targeting-pack-bin() {
|
||||
pkgdesc='The .NET Core targeting pack (binary)'
|
||||
provides=(dotnet-targeting-pack=${_runtimever} dotnet-targeting-pack-${_short_ver})
|
||||
conflicts=(dotnet-targeting-pack=${_runtimever} dotnet-targeting-pack-${_short_ver})
|
||||
|
||||
install -dm 755 "${pkgdir}"/usr/share/{dotnet,dotnet/packs,licenses}
|
||||
cp -dr --no-preserve='ownership' packs/Microsoft.NETCore.App.{Host.linux-arm64,Ref} "${pkgdir}"/usr/share/dotnet/packs/
|
||||
_install_license
|
||||
}
|
||||
|
||||
package_aspnet-targeting-pack-bin() {
|
||||
pkgdesc='The ASP.NET Core targeting pack (binary)'
|
||||
depends=(dotnet-targeting-pack-bin)
|
||||
provides=(aspnet-targeting-pack=${_runtimever} aspnet-targeting-pack-${_short_ver})
|
||||
conflicts=(aspnet-targeting-pack=${_runtimever} aspnet-targeting-pack-${_short_ver})
|
||||
|
||||
install -dm 755 "${pkgdir}"/usr/share/{dotnet,dotnet/packs,licenses}
|
||||
cp -dr --no-preserve='ownership' packs/Microsoft.AspNetCore.App.Ref "${pkgdir}"/usr/share/dotnet/packs/
|
||||
_install_license
|
||||
}
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
# Set location for AppHost lookup
|
||||
[ -z "$DOTNET_ROOT" ] && export DOTNET_ROOT=/usr/share/dotnet
|
||||
|
||||
# Add dotnet directory to PATH, according to docs it must be added, plus VSCode C# Dev Kit doesn't work without this.
|
||||
# See https://learn.microsoft.com/en-us/dotnet/core/install/linux-scripted-manual#set-environment-variables-system-wide
|
||||
case "$PATH" in
|
||||
*"$DOTNET_ROOT"* ) true ;;
|
||||
* ) PATH="$PATH:$DOTNET_ROOT" ;;
|
||||
esac
|
||||
|
||||
# Add dotnet tools directory to PATH
|
||||
[ -z "$DOTNET_TOOLS_PATH" ] && export DOTNET_TOOLS_PATH="$HOME/.dotnet/tools"
|
||||
case "$PATH" in
|
||||
*"$DOTNET_TOOLS_PATH"* ) true ;;
|
||||
* ) PATH="$PATH:$DOTNET_TOOLS_PATH" ;;
|
||||
esac
|
||||
|
||||
# Extract self-contained executables under HOME to avoid multi-user issues from using the default '/var/tmp'
|
||||
[ -z "$DOTNET_BUNDLE_EXTRACT_BASE_DIR" ] && export DOTNET_BUNDLE_EXTRACT_BASE_DIR="${XDG_CACHE_HOME:-"$HOME"/.cache}/dotnet_bundle_extract"
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "b67e475d16f4e061a16af53dae212a46d9bc3ea9"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"regex": "https://linux.dropbox.com/packages/",
|
||||
"pattern": "nautilus-dropbox-(?P<version>[0-9]{4}\\.[0-9]{2}\\.[0-9]{2})\\.tar\\.bz2"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "dropbox-cli",
|
||||
"commit": "b67e475d16f4e061a16af53dae212a46d9bc3ea9"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
--- a/PKGBUILD
|
||||
+++ b/PKGBUILD
|
||||
@@ -11,7 +11,7 @@ pkgname=dropbox-cli
|
||||
pkgver=2024.04.17
|
||||
pkgrel=2
|
||||
pkgdesc="Command line interface for Dropbox"
|
||||
-arch=("any")
|
||||
+arch=("x86_64")
|
||||
url="https://www.dropbox.com"
|
||||
license=("GPL-3.0-or-later")
|
||||
makedepends=("gdk-pixbuf2")
|
||||
@@ -6,8 +6,8 @@
|
||||
# Contributor: carstene1ns <arch carsten-teibes de>
|
||||
|
||||
pkgname=dropbox-cli
|
||||
pkgver=2024.04.17
|
||||
pkgrel=2.1
|
||||
pkgver=2026.05.06
|
||||
pkgrel=1
|
||||
pkgdesc="Command line interface for Dropbox"
|
||||
arch=("x86_64")
|
||||
url="https://www.dropbox.com"
|
||||
@@ -18,8 +18,7 @@ optdepends=("gtk3: Dropbox update GUI"
|
||||
"python-gpgme: verify binary signature")
|
||||
source=("https://linux.dropbox.com/packages/nautilus-dropbox-${pkgver}.tar.bz2"
|
||||
"dropboxd-fallback.patch")
|
||||
sha256sums=('a6a098cf16aa4747f40816ac793d59e37e8ae3b7080d0b30611d6c2b8663f2c1'
|
||||
'711ed63c6dfccfd05c6e9abaa291be9ac3c3909e84f788f568cb44dee2d48229')
|
||||
sha256sums=('c9d7ef418ccb0f34adc7cdda1952110be4fbbc788a79cdeb2cfd63e070bb3961' '711ed63c6dfccfd05c6e9abaa291be9ac3c3909e84f788f568cb44dee2d48229')
|
||||
|
||||
prepare() {
|
||||
cd "nautilus-dropbox-${pkgver}"
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "6379feda6f36bbbd496c97f040e4f71c5a1dcec7"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"redirect": "https://www.dropbox.com/download?plat=lnx.x86_64",
|
||||
"pattern": "dropbox-lnx\\.x86_64-(?P<version>[0-9]+\\.4\\.[0-9]+)\\.tar\\.gz"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "dropbox",
|
||||
"commit": "6379feda6f36bbbd496c97f040e4f71c5a1dcec7"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
# Contributor: David Manouchehri <d@32t.ca>
|
||||
|
||||
pkgname=dropbox
|
||||
pkgver=264.4.3421
|
||||
pkgver=270.4.3312
|
||||
pkgrel=1
|
||||
pkgdesc="A free service that lets you bring your photos, docs, and videos anywhere and share them easily."
|
||||
arch=("x86_64")
|
||||
@@ -27,12 +27,7 @@ source=("DropboxGlyph_Blue.svg"
|
||||
"dropbox@.service"
|
||||
"https://edge.dropboxstatic.com/dbx-releng/client/dropbox-lnx.x86_64-$pkgver.tar.gz"{,.asc})
|
||||
|
||||
sha256sums=('9ba76205ec5838db85d822f23cfd7e2112fd2757e8031d8374709f102143c548'
|
||||
'1610ff57e8b20ee7a37682c3cc505da4ddc9cec2bd7234c90c0f2073657521d2'
|
||||
'6c67a9c8c95c08fafafd2f1d828074b13e3347b05d2e4f4bf4e62746115d7477'
|
||||
'98581e65a91ae1f19ed42edcdaaa52e102298b5da0d71b50089393d364474d3d'
|
||||
'4aa06821de43b5e1cf4f27f83cb5f0bca82d01107c758091d7895f0d723f5411'
|
||||
'SKIP')
|
||||
sha256sums=('9ba76205ec5838db85d822f23cfd7e2112fd2757e8031d8374709f102143c548' '1610ff57e8b20ee7a37682c3cc505da4ddc9cec2bd7234c90c0f2073657521d2' '6c67a9c8c95c08fafafd2f1d828074b13e3347b05d2e4f4bf4e62746115d7477' '98581e65a91ae1f19ed42edcdaaa52e102298b5da0d71b50089393d364474d3d' '35404957d2a15dcac998d53cbec692d5236e197493f6c009accd91ea9aa8f34c' 'SKIP')
|
||||
# The PGP key fingerprint should match the one on https://www.dropbox.com/help/desktop-web/linux-commands
|
||||
validpgpkeys=(
|
||||
'1C61A2656FB57B7E4DE0F4C1FC918B335044912E' # Dropbox Automatic Signing Key <linux@dropbox.com>
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "5614a0a61616643e448fb7c68d58237715ce2739"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/elephant",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "elephant-all",
|
||||
"commit": "5614a0a61616643e448fb7c68d58237715ce2739"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "659b81f1aa74a13fd2ebec222e19da2046d8e977"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/elephant",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "elephant-archlinuxpkgs",
|
||||
"commit": "659b81f1aa74a13fd2ebec222e19da2046d8e977"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "42d9dd5424884c51b8fe6bf7692caf0a31007f05"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/elephant",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "elephant-bluetooth",
|
||||
"commit": "42d9dd5424884c51b8fe6bf7692caf0a31007f05"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "84aba9e90bbd65af45f83168cd6c7bce6ec4322e"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/elephant",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "elephant-calc",
|
||||
"commit": "84aba9e90bbd65af45f83168cd6c7bce6ec4322e"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "3176f9de1445e7115009383f9cdac116729fc7be"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/elephant",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "elephant-clipboard",
|
||||
"commit": "3176f9de1445e7115009383f9cdac116729fc7be"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "c30e33db5fef912dec9aa157773b10ffab1e0307"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/elephant",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "elephant-desktopapplications",
|
||||
"commit": "c30e33db5fef912dec9aa157773b10ffab1e0307"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "2d16502b7a905e7d7a03e0026c5519c3b3c4abf2"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/elephant",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "elephant-files",
|
||||
"commit": "2d16502b7a905e7d7a03e0026c5519c3b3c4abf2"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "5ab583fee6ba3e387d49ffe4e409bb84bc60ea24"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/elephant",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "elephant-menus",
|
||||
"commit": "5ab583fee6ba3e387d49ffe4e409bb84bc60ea24"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "1d756a138e926a81b9d33265f0197b8661168845"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/elephant",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "elephant-providerlist",
|
||||
"commit": "1d756a138e926a81b9d33265f0197b8661168845"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "e47641530d912199372204cf8780ef37f99f0b37"
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/elephant",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "elephant-runner",
|
||||
"commit": "e47641530d912199372204cf8780ef37f99f0b37"
|
||||
}
|
||||
}
|
||||
Loaded 100 of 737 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user