diff --git a/.github/workflows/sync-aur-edge.yml b/.github/workflows/sync-aur-edge.yml
deleted file mode 100644
index cfa1aed..0000000
--- a/.github/workflows/sync-aur-edge.yml
+++ /dev/null
@@ -1,70 +0,0 @@
-name: Sync AUR Packages (Edge)
-
-on:
- schedule:
- # Daily at 6 AM UTC
- - 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
-
- - name: Sync AUR packages (edge tier)
- run: |
- if [ -n "$PACKAGES" ]; then
- ./bin/sync-aur --tier edge $PACKAGES
- else
- ./bin/sync-aur --tier edge
- fi
- env:
- PACKAGES: ${{ github.event.inputs.packages }}
-
- - name: Check for changes
- id: changes
- run: |
- if git diff --quiet; 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 (edge)'
- title: 'chore: sync AUR packages (edge)'
- body: |
- Automated AUR package sync for edge tier.
-
- Please review the changes before merging.
- branch: auto/sync-aur-edge
- 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 \
- "🔴 AUR sync failed (edge)
View run" \
- '{content: $content}')" \
- "$BASECAMP_CHATBOT_URL"
diff --git a/.github/workflows/sync-aur-shared.yml b/.github/workflows/sync-aur-shared.yml
deleted file mode 100644
index ba2461b..0000000
--- a/.github/workflows/sync-aur-shared.yml
+++ /dev/null
@@ -1,62 +0,0 @@
-name: Sync AUR Packages (Shared)
-
-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
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Sync AUR packages (shared tier)
- run: |
- if [ -n "$PACKAGES" ]; then
- ./bin/sync-aur --tier shared $PACKAGES
- else
- ./bin/sync-aur --tier shared
- fi
- env:
- PACKAGES: ${{ github.event.inputs.packages }}
-
- - name: Check for changes
- id: changes
- run: |
- if git diff --quiet; then
- echo "has_changes=false" >> "$GITHUB_OUTPUT"
- else
- echo "has_changes=true" >> "$GITHUB_OUTPUT"
- fi
-
- - name: Commit and push changes
- if: steps.changes.outputs.has_changes == 'true'
- run: |
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
- git add -A
- git commit -m "chore: sync AUR packages (shared)"
- git push
-
- - 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 \
- "🔴 AUR sync failed (shared)
View run" \
- '{content: $content}')" \
- "$BASECAMP_CHATBOT_URL"
diff --git a/.github/workflows/sync-aur.yml b/.github/workflows/sync-aur.yml
new file mode 100644
index 0000000..b5d3fd3
--- /dev/null
+++ b/.github/workflows/sync-aur.yml
@@ -0,0 +1,91 @@
+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 \
+ "🔴 AUR sync failed
View run" \
+ '{content: $content}')" \
+ "$BASECAMP_CHATBOT_URL"
diff --git a/.gitignore b/.gitignore
index d6290ee..07b6c6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,34 @@ src/
logs/
build-output/
pkgs.omarchy.org/
+pkgbuilds/*/.SRCINFO
+pkgbuilds/*/.gitignore
+pkgbuilds/*/src/
+pkgbuilds/*/pkg/
+pkgbuilds/*/*.pkg.tar*
+pkgbuilds/*/*.tar
+pkgbuilds/*/*.tar.*
+pkgbuilds/*/*.tgz
+pkgbuilds/*/*.gz
+pkgbuilds/*/*.zip
+pkgbuilds/*/*.deb
+pkgbuilds/*/*.AppImage
+pkgbuilds/*/*.log
+pkgbuilds/*/*.sig
+pkgbuilds/*/*.zst
+pkgbuilds/*/*.in
+pkgbuilds/*/.xml
+pkgbuilds/*/*~
+pkgbuilds/*/*.swp
+pkgbuilds/*/*.swo
+pkgbuilds/*/.DS_Store
+pkgbuilds/*/__pycache__/
+pkgbuilds/*/squashfs-root/
+pkgbuilds/*/starter/
+pkgbuilds/asdcontrol/asdcontrol/
+pkgbuilds/dropbox-cli/dropbox-cli-*.py
+pkgbuilds/macbook12-spi-driver-dkms/macbook12-spi-driver/
+pkgbuilds/omazed/steps.txt
+pkgbuilds/symfony-cli/symfony*
+!pkgbuilds/symfony-cli/symfony-cli.install
+pkgbuilds/yay/yay/
diff --git a/README.md b/README.md
index abf490c..b4fa686 100644
--- a/README.md
+++ b/README.md
@@ -5,11 +5,15 @@ Build system for the Omarchy Package Repository. Builds PKGBUILDs from local sou
**Multi-Architecture**: Supports both x86_64 and aarch64 (ARM64).
## PKGBUILDs
-There are 3 folders housing PKGBUILD that drive what is ultimately on the respective repos.
-- `edge` - Built and pushed to the edge repo. These are synced daily with AUR if they're set to mirror it.
-- `stable` - Built and pushed to the stable repo. These are only synced manually when stable is bumped.
-- `shared` - Built and pushed to both stable and edge repo every 6hrs.
+Each package lives directly under `pkgbuilds//` and carries Omarchy metadata in `.omarchy/package.json`.
+
+The filesystem no longer encodes release policy. Instead:
+
+- all packages build for `edge`
+- packages with `"release_ring": "fast"` also build directly for `stable`
+- all other packages reach `stable` by promoting tested edge artifacts with `bin/repo migrate`
+- AUR sync behavior is controlled by `source`, `sync`, `aur`, patches, and hooks in `.omarchy/`
## Prerequisites
### aarch64 Builds (Optional)
@@ -54,6 +58,9 @@ bin/repo release --arch aarch64
# Force build specific package (useful for debugging failures)
bin/repo release --package omarchy-nvim
+
+# Show what would build without signing/promoting/syncing
+bin/repo release --dry-run
```
### Step-by-Step
@@ -83,10 +90,13 @@ bin/repo build # All packages (x86_64, edge)
bin/repo build --arch aarch64 # ARM64
bin/repo build --mirror stable # Stable mirror
bin/repo build --package yay cursor-bin # Specific packages
+bin/repo build --dry-run # Show what would build
```
**Output**: Unsigned `.pkg.tar.zst` in `build-output/`. Only builds packages that are newer than what is in the repository.
+Use `--dry-run` to show the build plan without running `makepkg`.
+
### Sign
```bash
@@ -123,7 +133,7 @@ bin/repo update # Update database
Updates the repository database (adding the newest version of each package). Run this after `promote` or `clean`.
-### Sync
+### Sync Repository
```bash
bin/repo sync # Sync current arch/mirror
@@ -132,30 +142,56 @@ bin/repo sync --arch aarch64 # Sync ARM64
bin/repo sync --skip-prod-check # No confirmation
```
-Syncs to remote server using rclone based on the configured mirror and architecture.
+Syncs package repositories to the remote server using rclone based on the configured mirror and architecture.
+
+### Sync AUR PKGBUILDs
+
+```bash
+bin/sync-aur # Sync all AUR packages with sync enabled
+bin/sync-aur yay v4l2-relayd # Sync specific packages
+```
+
+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`.
### Other
```bash
-bin/repo migrate --arch x86_64 # Migrate missing packages edge -> stable, then clean + update
+bin/repo migrate --arch x86_64 # Promote tested edge artifacts -> stable, then clean + update
bin/repo migrate --dry-run # Preview migration and cleanup
-bin/repo list # List packages
-bin/repo remove # Remove package
-bin/clean-docker # Clear Docker images/cache (forces fresh rebuild)
+bin/repo list # List package metadata
+bin/add-package # Add an AUR/local package with metadata
+bin/package-worktree # Create upstream/patched/current scratch workspace
+bin/repo remove # Remove package
+bin/clean-docker # Clear Docker images/cache (forces fresh rebuild)
+```
+
+### Package Metadata Tools
+
+```bash
+bin/add-package yay # Add AUR package, create metadata, sync from AUR
+bin/add-package spotify --fast # Add package to the fast release ring
+bin/add-package foo --no-sync # Sync once, then mark AUR sync disabled
+bin/add-package my-package --local # Create local package metadata
+
+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
```
## Directory Structure
```
omarchy-pkgs/
-├── pkgbuilds/ # Source PKGBUILDs (tiered)
-│ ├── stable/ # Manual promotion only, builds for stable
-│ ├── edge/ # Daily sync w/ PR review, builds for edge only
-│ └── shared/ # 6-hourly sync, auto-merge, builds for BOTH
+├── pkgbuilds/ # Source PKGBUILDs
+│ └── package-name/
+│ ├── PKGBUILD
+│ └── .omarchy/
+│ ├── package.json # Source/sync/release metadata
+│ ├── patches/ # Omarchy patches reapplied after AUR sync
+│ └── post-sync.sh # Optional dynamic post-sync customization hook
├── build/
-│ └── packages/
-│ ├── edge.packages # Package list for daily sync (PR review)
-│ └── shared.packages # Package list for 6-hourly sync (auto-merge)
├── build-output/ # Unsigned packages (temporary)
│ ├── edge/
│ │ ├── x86_64/
@@ -173,66 +209,121 @@ omarchy-pkgs/
└── bin/ # CLI tools (on host)
```
-## Package Tiers
+## Package Metadata
-Packages are organized into three tiers based on their release cadence:
+Each source package has Omarchy metadata at `pkgbuilds//.omarchy/package.json`.
-| Tier | Directory | Sync Frequency | Review | Builds To |
-|------|-----------|----------------|--------|-----------|
-| **Stable** | `pkgbuilds/stable/` | Manual | N/A | stable only |
-| **Edge** | `pkgbuilds/edge/` | Daily | PR required | edge only |
-| **Shared** | `pkgbuilds/shared/` | Every 6 hours | Auto-merge | edge AND stable |
+Minimal examples:
+
+```json
+{ "source": "aur" }
+```
+
+```json
+{ "source": "aur", "sync": false }
+```
+
+```json
+{ "source": "aur", "release_ring": "fast" }
+```
+
+```json
+{ "source": "local" }
+```
+
+```json
+{ "source": "aur", "pkgrel": { "suffix": 1 } }
+```
+
+Fields:
+
+- `source`: `aur` or `local`
+- `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.
+- `release_ring`: optional. `fast` means the package is built directly for stable as well as edge. Packages without a ring build in edge and reach stable through tested artifact promotion (`bin/repo migrate`).
+- `pkgrel`: optional Omarchy pkgrel suffix for a version-pinned rebuild bump. This emits `.` 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.
+- `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.
### Build Matrix
-- **Edge builds** (`--mirror edge`): `pkgbuilds/edge/*` + `pkgbuilds/shared/*`
-- **Stable builds** (`--mirror stable`): `pkgbuilds/stable/*` + `pkgbuilds/shared/*`
+- **Edge builds** (`--mirror edge`): all packages in `pkgbuilds/*`
+- **Stable builds** (`--mirror stable`): packages with `"release_ring": "fast"`
+- **Stable promotion** (`bin/repo migrate`): copies tested edge artifacts into stable
## Adding Packages
-### From AUR (Edge - Daily Sync, PR Review)
+### From AUR
```bash
-# Add to edge package list
-echo "package-name" >> build/packages/edge.packages
-
-# Sync PKGBUILD
-bin/sync-aur --tier edge package-name
-
-# Build and release
+bin/add-package package-name
bin/repo release --package package-name
```
-### From AUR (Shared - 6-Hourly Sync, Auto-Merge)
+### From AUR, fast release ring
```bash
-# Add to shared package list
-echo "package-name" >> build/packages/shared.packages
-
-# Sync PKGBUILD
-bin/sync-aur --tier shared package-name
-
-# Build and release (will publish to both edge and stable)
+bin/add-package package-name --fast
bin/repo release --package package-name
bin/repo release --mirror stable --package package-name
```
-### Local Patches for AUR Packages
-
-Create `pkgbuilds//package-name/patches/*.patch` to maintain modifications across AUR syncs:
+### AUR-origin, manually maintained by Omarchy
```bash
-cd pkgbuilds/edge/package-name
-# Make changes
-git diff > patches/my-fix.patch
-# Next sync will auto-apply patches
+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
-mkdir pkgbuilds/edge/my-package
-# Add PKGBUILD and files
+bin/add-package my-package --local --scaffold
+# Fill in PKGBUILD and package files
bin/repo release --package my-package
```
@@ -286,14 +377,13 @@ The repository includes GitHub workflows and systemd services for automated rele
#### GitHub Workflows
-1. **sync-aur-edge.yml** (Daily at 6:00 AM UTC): Syncs edge packages from AUR, creates PR for review
-2. **sync-aur-shared.yml** (Every 6 hours): Syncs shared packages from AUR, auto-commits to master
+1. **sync-aur.yml** (Every 6 hours): Syncs AUR packages according to `.omarchy/package.json` and opens a PR when changes are found.
#### Systemd Services
1. **check-versions** (Every 6 hours at :30): Pulls latest from git, compares PKGBUILD versions to published versions, creates state files if builds are needed
-2. **auto-release-edge** (Every 6 hours at +1:00): If state file exists, builds edge packages
-3. **auto-release-stable** (Every 6 hours at +1:00): If state file exists, builds stable packages (runs in parallel with edge)
+2. **auto-release-edge** (Every 6 hours at +1:00): If state file exists, builds all edge packages that need updates
+3. **auto-release-stable** (Every 6 hours at +1:00): If state file exists, builds `release_ring=fast` packages for stable (runs in parallel with edge)
State files are stored in `/root/.state/`:
- `.sync-needed-edge`
diff --git a/bin/add-package b/bin/add-package
new file mode 100755
index 0000000..0fef98a
--- /dev/null
+++ b/bin/add-package
@@ -0,0 +1,180 @@
+#!/bin/bash
+set -euo pipefail
+
+BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
+source "$BUILD_ROOT/helpers/message-helpers.sh"
+source "$BUILD_ROOT/helpers/paths.sh"
+
+PACKAGE=""
+SOURCE="aur"
+SYNC="true"
+RELEASE_RING=""
+AUR_PACKAGE=""
+FORCE=false
+SCAFFOLD=false
+
+usage() {
+ cat < [OPTIONS]
+
+Create pkgbuilds/ with Omarchy metadata. AUR packages are synced
+immediately after metadata is written.
+
+Options:
+ --source Package source (default: aur)
+ --local Shortcut for --source local
+ --aur AUR package name when different from local directory
+ --no-sync For AUR packages, mark sync disabled after initial setup
+ --fast Put package in the fast release ring
+ --release-ring Release ring (currently: fast)
+ --scaffold For local packages, create a starter PKGBUILD
+ --force Overwrite existing .omarchy/package.json
+ -h, --help Show this help message
+
+Examples:
+ $0 yay
+ $0 spotify --fast
+ $0 signal-desktop --no-sync
+ $0 omarchy-zsh --local --scaffold
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --source)
+ SOURCE="$2"
+ shift 2
+ ;;
+ --local)
+ SOURCE="local"
+ shift
+ ;;
+ --aur)
+ AUR_PACKAGE="$2"
+ shift 2
+ ;;
+ --no-sync)
+ SYNC="false"
+ shift
+ ;;
+ --fast)
+ RELEASE_RING="fast"
+ shift
+ ;;
+ --release-ring)
+ RELEASE_RING="$2"
+ shift 2
+ ;;
+ --scaffold)
+ SCAFFOLD=true
+ shift
+ ;;
+ --force)
+ FORCE=true
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ --*)
+ print_error "Unknown option: $1"
+ exit 1
+ ;;
+ *)
+ if [[ -n "$PACKAGE" ]]; then
+ print_error "Only one package can be added at a time"
+ exit 1
+ fi
+ PACKAGE="$1"
+ shift
+ ;;
+ esac
+done
+
+if [[ -z "$PACKAGE" ]]; then
+ print_error "Package name required"
+ usage
+ exit 1
+fi
+
+case "$SOURCE" in
+ aur|local) ;;
+ *) print_error "Invalid source: $SOURCE (must be aur or local)"; exit 1 ;;
+esac
+
+case "$RELEASE_RING" in
+ ""|fast) ;;
+ *) print_error "Invalid release ring: $RELEASE_RING (currently only 'fast' is supported)"; exit 1 ;;
+esac
+
+PACKAGE_DIR="$PKGBUILDS_DIR/$PACKAGE"
+OMARCHY_DIR="$PACKAGE_DIR/.omarchy"
+METADATA_FILE="$OMARCHY_DIR/package.json"
+
+if [[ -f "$METADATA_FILE" && "$FORCE" != true ]]; then
+ print_error "Package metadata already exists: $METADATA_FILE"
+ print_info "Use --force to overwrite it"
+ exit 1
+fi
+
+print_header "Add Package"
+print_info "Package: $PACKAGE"
+print_info "Source: $SOURCE"
+[[ -n "$RELEASE_RING" ]] && print_info "Release ring: $RELEASE_RING"
+
+mkdir -p "$OMARCHY_DIR"
+
+jq_args=(--arg source "$SOURCE")
+jq_filter='{source: $source}'
+
+if [[ "$SOURCE" == "aur" && -n "$AUR_PACKAGE" && "$AUR_PACKAGE" != "$PACKAGE" ]]; then
+ jq_args+=(--arg aur "$AUR_PACKAGE")
+ jq_filter="$jq_filter + {aur: $aur}"
+fi
+
+if [[ "$SOURCE" == "aur" && "$SYNC" == "false" ]]; then
+ jq_filter="$jq_filter + {sync: false}"
+fi
+
+if [[ -n "$RELEASE_RING" ]]; then
+ jq_args+=(--arg release_ring "$RELEASE_RING")
+ jq_filter="$jq_filter + {release_ring: $release_ring}"
+fi
+
+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
+else
+ if [[ "$SCAFFOLD" == true && ! -f "$PACKAGE_DIR/PKGBUILD" ]]; then
+ cat > "$PACKAGE_DIR/PKGBUILD" < Target architecture (x86_64 or aarch64, default: x86_64)"
echo " --mirror Mirror to use (edge or stable, default: edge)"
echo " --package Build only the specified package(s) (space-separated)"
+ echo " --dry-run Show what would build without running makepkg"
echo " -h, --help Show this help message"
echo ""
- echo "This script builds packages from pkgbuilds/ directories based on mirror:"
- echo " --mirror edge: builds from pkgbuilds/edge/ + pkgbuilds/shared/"
- echo " --mirror stable: builds from pkgbuilds/shared/ only (edge pkgs promoted via 'repo migrate')"
+ echo "This script builds packages from pkgbuilds/ based on .omarchy/package.json:"
+ echo " --mirror edge: builds all packages"
+ echo " --mirror stable: builds packages with release_ring=fast only"
echo ""
echo "Or build specific packages with --package:"
- echo " Package names should match directories in the relevant pkgbuilds/ tier"
+ echo " Package names should match directories in pkgbuilds/"
echo ""
echo "Examples:"
echo " $0 --arch aarch64"
@@ -80,6 +81,27 @@ print_info "Mirror: $MIRROR"
print_info "Build workspace: $BUILD_OUTPUT_DIR"
print_info "Final output: $REPO_DIR"
+if [[ "$DRY_RUN" == true ]]; then
+ print_warning "DRY RUN MODE - build plan only; no Docker or makepkg will run"
+ ARCH="$ARCH" \
+ MIRROR="$MIRROR" \
+ PACKAGES="$PACKAGES" \
+ DRY_RUN=true \
+ PKGBUILDS_DIR="$PKGBUILDS_DIR" \
+ BUILD_OUTPUT_DIR="$BUILD_OUTPUT_DIR" \
+ FINAL_OUTPUT_DIR="$REPO_DIR" \
+ HELPERS_DIR="$BUILD_ROOT/helpers" \
+ SRC_DIR="$SRC_DIR" \
+ "$BUILD_ROOT/build/build.sh"
+ exit $?
+fi
+
+# Create directories if they don't exist
+mkdir -p "$BUILD_OUTPUT_DIR" "$REPO_DIR" "$SRC_DIR"
+
+# Check Docker is available
+check_docker
+
# Setup QEMU for aarch64 builds on x86_64 hosts
if [[ "$(uname -m)" == "x86_64" && "$ARCH" == "aarch64" ]]; then
# Check if QEMU is already working
@@ -104,7 +126,7 @@ fi
# Build/update the Docker image
build_docker_image "$BUILD_DIR" "$ARCH" "$MIRROR"
-print_info "Running AUR package build..."
+print_info "Running package build..."
# Create output directories if they don't exist
mkdir -p "$BUILD_OUTPUT_DIR"
@@ -123,6 +145,7 @@ DOCKER_ARGS=(
-v "$BUILD_ROOT/build-output:/build-output"
-v "$BUILD_ROOT/pkgs.omarchy.org:/pkgs.omarchy.org"
-v "$BUILD_DIR:/build:ro"
+ -v "$BUILD_ROOT/helpers:/helpers:ro"
-v "$BUILD_ROOT/pkgbuilds:/pkgbuilds:ro"
)
@@ -137,8 +160,8 @@ BUILD_RESULT=$?
# Summary
echo ""
if [[ $BUILD_RESULT -eq 0 ]]; then
- print_success "AUR build completed successfully!"
+ print_success "Build completed successfully!"
else
- print_warning "Some AUR packages failed (see details above)"
+ print_warning "Some packages failed (see details above)"
exit $BUILD_RESULT
fi
diff --git a/bin/check-versions b/bin/check-versions
index 754daeb..9498c44 100755
--- a/bin/check-versions
+++ b/bin/check-versions
@@ -7,30 +7,29 @@ set -e
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"
STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}"
ARCH="${ARCH:-x86_64}"
-# Ensure state directory exists
mkdir -p "$STATE_DIR"
print_header "Package Version Check"
-# Get version from repo database
get_repo_version() {
local pkg="$1"
local mirror="$2"
local repo_db="$BUILD_ROOT/pkgs.omarchy.org/$mirror/$ARCH/omarchy.db.tar.zst"
-
+
if [[ -f "$repo_db" ]]; then
- local desc_file=$(tar -tf "$repo_db" 2>/dev/null | grep "^${pkg}-[0-9r].*/desc$" | head -1)
+ local desc_file
+ desc_file=$(tar -tf "$repo_db" 2>/dev/null | grep "^${pkg}-[0-9r].*/desc$" | head -1)
if [[ -n "$desc_file" ]]; then
tar -xOf "$repo_db" "$desc_file" 2>/dev/null | awk '/%VERSION%/{getline; print; exit}'
fi
fi
}
-# Get version from PKGBUILD
get_pkgbuild_version() {
local pkgdir="$1"
if [[ -f "$pkgdir/PKGBUILD" ]]; then
@@ -38,88 +37,57 @@ get_pkgbuild_version() {
fi
}
-# Check if package needs building for a mirror
check_package() {
local pkg="$1"
local pkgdir="$2"
local mirror="$3"
-
- local pkgbuild_version=$(get_pkgbuild_version "$pkgdir")
- local repo_version=$(get_repo_version "$pkg" "$mirror")
-
+
+ local pkgbuild_version repo_version
+ pkgbuild_version=$(get_pkgbuild_version "$pkgdir")
+ repo_version=$(get_repo_version "$pkg" "$mirror")
+
if [[ -z "$pkgbuild_version" ]]; then
- return 1 # Can't read version, skip
+ return 1
fi
-
+
if [[ "$pkgbuild_version" != "$repo_version" ]]; then
- return 0 # Needs building
+ return 0
fi
-
- return 1 # Up to date
+
+ return 1
}
-edge_needs_build=false
-stable_needs_build=false
-edge_packages=()
-stable_packages=()
+check_mirror() {
+ local mirror="$1"
+ local state_file="$STATE_DIR/.sync-needed-$mirror"
+ local needs_build=false
+ local packages=()
-print_info "Checking edge packages..."
+ print_info "Checking $mirror packages..."
-# Check edge/ packages -> only affects edge
-for pkgdir in "$BUILD_ROOT/pkgbuilds/edge"/*/; do
- [[ ! -d "$pkgdir" ]] && continue
- pkg=$(basename "$pkgdir")
- [[ ! -f "$pkgdir/PKGBUILD" ]] && continue
-
- if check_package "$pkg" "$pkgdir" "edge"; then
- edge_needs_build=true
- edge_packages+=("$pkg")
+ while IFS= read -r pkg; do
+ local pkgdir="$PKGBUILDS_DIR/$pkg"
+ if check_package "$pkg" "$pkgdir" "$mirror"; then
+ needs_build=true
+ packages+=("$pkg")
+ fi
+ done < <(packages_for_mirror "$mirror")
+
+ echo ""
+
+ if [[ "$needs_build" == true ]]; then
+ touch "$state_file"
+ print_success "${mirror^} needs building (${#packages[@]} packages)"
+ print_info "Packages: ${packages[*]}"
+ print_info "State file created: $state_file"
+ else
+ print_info "${mirror^} is up to date"
fi
-done
-print_info "Checking shared packages..."
+ echo ""
+}
-# Check shared/ packages -> affects BOTH edge and stable
-for pkgdir in "$BUILD_ROOT/pkgbuilds/shared"/*/; do
- [[ ! -d "$pkgdir" ]] && continue
- pkg=$(basename "$pkgdir")
- [[ ! -f "$pkgdir/PKGBUILD" ]] && continue
-
- # Check against edge
- if check_package "$pkg" "$pkgdir" "edge"; then
- edge_needs_build=true
- edge_packages+=("$pkg")
- fi
-
- # Check against stable
- if check_package "$pkg" "$pkgdir" "stable"; then
- stable_needs_build=true
- stable_packages+=("$pkg")
- fi
-done
+check_mirror edge
+check_mirror stable
-echo ""
-
-# Drop state files as needed
-if [[ "$edge_needs_build" == true ]]; then
- touch "$STATE_DIR/.sync-needed-edge"
- print_success "Edge needs building (${#edge_packages[@]} packages)"
- print_info "Packages: ${edge_packages[*]}"
- print_info "State file created: $STATE_DIR/.sync-needed-edge"
-else
- print_info "Edge is up to date"
-fi
-
-echo ""
-
-if [[ "$stable_needs_build" == true ]]; then
- touch "$STATE_DIR/.sync-needed-stable"
- print_success "Stable needs building (${#stable_packages[@]} packages)"
- print_info "Packages: ${stable_packages[*]}"
- print_info "State file created: $STATE_DIR/.sync-needed-stable"
-else
- print_info "Stable is up to date"
-fi
-
-echo ""
print_success "Version check complete!"
diff --git a/bin/list-packages b/bin/list-packages
index 0fec794..a2f5d5b 100755
--- a/bin/list-packages
+++ b/bin/list-packages
@@ -1,15 +1,66 @@
#!/bin/bash
+set -euo pipefail
-# Get the directory of this script
SCRIPT_DIR=$(realpath "${BASH_SOURCE[0]%/*}")
BUILD_ROOT=$(realpath "$SCRIPT_DIR/..")
source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh"
+source "$BUILD_ROOT/helpers/package-metadata.sh"
+
+OUTPUT_JSON=false
+LIST_REPO=false
+SOURCE_FILTER=""
+SYNC_FILTER=""
+RING_FILTER=""
+
+usage() {
+ cat </.omarchy/package.json.
+
+Options:
+ --json Output JSON for agents/scripts
+ --source Filter by source
+ --sync Filter by AUR sync state
+ --release-ring Filter by release ring, e.g. fast
+ --repo List packages from the published repo database instead
+ --arch Target architecture for --repo (default: x86_64)
+ --mirror Mirror for --repo (edge or stable, default: edge)
+ -h, --help Show this help message
+
+Examples:
+ $0
+ $0 --release-ring fast
+ $0 --source aur --sync false
+ $0 --json
+ $0 --repo --mirror stable
+EOF
+}
-# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
+ --json)
+ OUTPUT_JSON=true
+ shift
+ ;;
+ --repo)
+ LIST_REPO=true
+ shift
+ ;;
+ --source)
+ SOURCE_FILTER="$2"
+ shift 2
+ ;;
+ --sync)
+ SYNC_FILTER="$2"
+ shift 2
+ ;;
+ --release-ring)
+ RING_FILTER="$2"
+ shift 2
+ ;;
--arch)
ARCH="$2"
update_arch_paths
@@ -21,12 +72,7 @@ while [[ $# -gt 0 ]]; do
shift 2
;;
-h | --help)
- echo "Usage: $0 [OPTIONS]"
- echo ""
- echo "Options:"
- echo " --arch Target architecture (x86_64 or aarch64, default: x86_64)"
- echo " --mirror Mirror to use (edge or stable, default: edge)"
- echo " -h, --help Show this help message"
+ usage
exit 0
;;
*)
@@ -36,18 +82,149 @@ while [[ $# -gt 0 ]]; do
esac
done
-DB_FILE="$REPO_DIR/omarchy.db.tar.zst"
+list_repo_packages() {
+ local db_file="$REPO_DIR/omarchy.db.tar.zst"
-if [[ ! -f "$DB_FILE" ]]; then
- echo "Repository database not found: $DB_FILE"
- exit 1
+ if [[ ! -f "$db_file" ]]; then
+ echo "Repository database not found: $db_file" >&2
+ exit 1
+ fi
+
+ tar -xOf "$db_file" --wildcards "*/desc" 2>/dev/null |
+ awk '/%NAME%/{getline; name=$0}
+ /%VERSION%/{getline; print name "-" $0}' |
+ sort
+}
+
+get_pkgbuild_version() {
+ local pkgdir="$1"
+
+ if [[ -f "$pkgdir/PKGBUILD" ]]; then
+ (cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; if [[ -n "$epoch" ]]; then echo "${epoch}:${pkgver}-${pkgrel}"; else echo "${pkgver}-${pkgrel}"; fi') || true
+ fi
+}
+
+patch_count() {
+ local pkgdir="$1"
+ local patches_dir="$pkgdir/.omarchy/patches"
+
+ [[ -d "$patches_dir" ]] || { echo 0; return; }
+ find "$patches_dir" -maxdepth 1 -type f -name '*.patch' | wc -l
+}
+
+post_sync_hook_enabled() {
+ local pkgdir="$1"
+
+ [[ -f "$pkgdir/.omarchy/post-sync.sh" ]] && echo true || echo false
+}
+
+package_matches_filters() {
+ local source="$1"
+ local sync="$2"
+ local ring="$3"
+
+ [[ -z "$SOURCE_FILTER" || "$source" == "$SOURCE_FILTER" ]] || return 1
+ [[ -z "$RING_FILTER" || "$ring" == "$RING_FILTER" ]] || return 1
+
+ if [[ -n "$SYNC_FILTER" ]]; then
+ [[ "$source" == "aur" ]] || return 1
+ [[ "$sync" == "$SYNC_FILTER" ]] || return 1
+ fi
+
+ return 0
+}
+
+emit_package_json() {
+ local name="$1" source="$2" aur="$3" sync="$4" ring="$5" pkgrel="$6" upstream_commit="$7" patches="$8" hook="$9" version="${10}" path="${11}"
+
+ jq -n \
+ --arg name "$name" \
+ --arg source "$source" \
+ --arg aur "$aur" \
+ --arg sync "$sync" \
+ --arg release_ring "$ring" \
+ --argjson pkgrel "$pkgrel" \
+ --arg upstream_commit "$upstream_commit" \
+ --argjson patches "$patches" \
+ --arg hook "$hook" \
+ --arg version "$version" \
+ --arg path "$path" \
+ '{name: $name, source: $source, aur: $aur, sync: (if $sync == "" then null else $sync == "true" end), release_ring: $release_ring, pkgrel: $pkgrel, upstream_commit: $upstream_commit, patches: $patches, post_sync_hook: ($hook == "true"), version: $version, path: $path}'
+}
+
+if [[ "$LIST_REPO" == true ]]; then
+ list_repo_packages
+ exit 0
fi
-echo "Packages in $MIRROR mirror ($ARCH):"
-echo ""
+rows_file=$(mktemp)
+json_file=$(mktemp)
+trap 'rm -f "$rows_file" "$json_file"' EXIT
-# Extract package names and versions from database
-tar -xOf "$DB_FILE" --wildcards "*/desc" 2>/dev/null |
- awk '/%NAME%/{getline; name=$0}
- /%VERSION%/{getline; print name "-" $0}' |
- sort
+first_json=true
+printf '[\n' > "$json_file"
+
+while IFS= read -r pkgdir; do
+ name=$(basename "$pkgdir")
+ metadata=$(metadata_file_for_dir "$pkgdir")
+ source=$(jq -r '.source // ""' "$metadata")
+ ring=$(jq -r '.release_ring // ""' "$metadata")
+ aur=$(jq -r --arg name "$name" '.aur // $name' "$metadata")
+ aur_display="$aur"
+ [[ "$aur_display" == "$name" ]] && aur_display=""
+
+ if [[ "$source" == "aur" ]]; then
+ sync=$(jq -r 'if has("sync") then .sync else true end' "$metadata")
+ else
+ sync=""
+ fi
+
+ package_matches_filters "$source" "$sync" "$ring" || continue
+
+ pkgrel=$(jq -c '.pkgrel // {}' "$metadata")
+ pkgrel_display=$(jq -r '.pkgrel // {} | to_entries | map("\(.key)=\(.value)") | join(",")' "$metadata")
+ upstream_commit=$(jq -r '.upstream_commit // ""' "$metadata")
+ upstream_display="${upstream_commit:0:7}"
+ patches=$(patch_count "$pkgdir" | tr -d ' ')
+ hook=$(post_sync_hook_enabled "$pkgdir")
+ hook_display="$([[ "$hook" == true ]] && echo yes || echo no)"
+ version=$(get_pkgbuild_version "$pkgdir")
+ relpath=${pkgdir#"$BUILD_ROOT/"}
+
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+ "$name" \
+ "$source" \
+ "$aur_display" \
+ "$([[ -n "$sync" ]] && [[ "$sync" == true ]] && echo yes || ([[ "$sync" == false ]] && echo no || echo -))" \
+ "$ring" \
+ "$pkgrel_display" \
+ "$upstream_display" \
+ "$patches" \
+ "$hook_display" \
+ "$version" \
+ "$relpath" >> "$rows_file"
+
+ if [[ "$first_json" == false ]]; then
+ printf ',\n' >> "$json_file"
+ fi
+ emit_package_json "$name" "$source" "$aur" "$sync" "$ring" "$pkgrel" "$upstream_commit" "$patches" "$hook" "$version" "$relpath" >> "$json_file"
+ first_json=false
+done < <(package_dirs)
+
+printf '\n]\n' >> "$json_file"
+
+if [[ "$OUTPUT_JSON" == true ]]; then
+ jq . "$json_file"
+ exit 0
+fi
+
+if command -v gum >/dev/null 2>&1; then
+ gum table --print --separator $'\t' \
+ --columns Package,Source,AUR,Sync,Release_Ring,Pkgrel,Upstream,Patches,Post_Sync,Version,Path \
+ < "$rows_file"
+else
+ {
+ printf 'Package\tSource\tAUR\tSync\tRelease_Ring\tPkgrel\tUpstream\tPatches\tPost_Sync\tVersion\tPath\n'
+ cat "$rows_file"
+ } | column -t -s $'\t'
+fi
diff --git a/bin/migrate-edge-to-stable b/bin/migrate-edge-to-stable
index 3c38787..3c49c3b 100755
--- a/bin/migrate-edge-to-stable
+++ b/bin/migrate-edge-to-stable
@@ -1,5 +1,5 @@
#!/bin/bash
-# Migrate package files from edge repo to stable repo without overwriting
+# Promote tested edge package artifacts into stable without overwriting
set -e
@@ -41,7 +41,7 @@ while [[ $# -gt 0 ]]; do
echo " -h, --help Show this help message"
echo ""
echo "What it does:"
- echo " 1) Rsync package files from edge -> stable (without overwriting existing files)"
+ echo " 1) Rsync tested package artifacts from edge -> stable (without overwriting existing files)"
echo " 2) Run stable cleanup (keep 2 versions)"
echo " 3) Update stable repository database"
echo " 4) Sync stable repository to remote (prompts for confirmation)"
@@ -57,7 +57,7 @@ done
SOURCE_DIR="$BUILD_ROOT/pkgs.omarchy.org/edge/$ARCH"
TARGET_DIR="$BUILD_ROOT/pkgs.omarchy.org/stable/$ARCH"
-print_header "Migrate Edge to Stable"
+print_header "Promote Edge Artifacts to Stable"
print_info "Architecture: $ARCH"
print_info "Source: $SOURCE_DIR"
print_info "Target: $TARGET_DIR"
diff --git a/bin/package-worktree b/bin/package-worktree
new file mode 100755
index 0000000..8deca92
--- /dev/null
+++ b/bin/package-worktree
@@ -0,0 +1,288 @@
+#!/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"
+
+PACKAGE=""
+DEST_DIR=""
+COMMIT_OVERRIDE=""
+
+usage() {
+ cat < [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
+ patched/ Raw AUR package with Omarchy .omarchy patches/hooks/pkgrel applied
+ current/ Current checked-in package directory
+
+Options:
+ --dir Workspace directory (default: mktemp under /tmp)
+ --commit Use a specific AUR commit instead of upstream_commit
+ -h, --help Show this help message
+
+Examples:
+ $0 v4l2-relayd
+ $0 v4l2-relayd --dir /tmp/v4l2-relayd-worktree
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --dir)
+ DEST_DIR="$2"
+ shift 2
+ ;;
+ --commit)
+ COMMIT_OVERRIDE="$2"
+ shift 2
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ --*)
+ print_error "Unknown option: $1"
+ exit 1
+ ;;
+ *)
+ if [[ -n "$PACKAGE" ]]; then
+ print_error "Only one package can be inspected at a time"
+ exit 1
+ fi
+ PACKAGE="$1"
+ shift
+ ;;
+ esac
+done
+
+if [[ -z "$PACKAGE" ]]; then
+ print_error "Package name required"
+ usage
+ exit 1
+fi
+
+PACKAGE_DIR="$PKGBUILDS_DIR/$PACKAGE"
+METADATA="$PACKAGE_DIR/.omarchy/package.json"
+
+if [[ ! -f "$METADATA" ]]; then
+ print_error "Missing package metadata: $METADATA"
+ exit 1
+fi
+
+if [[ "$(jq -r '.source // ""' "$METADATA")" != "aur" ]]; 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")}
+
+if [[ -z "$DEST_DIR" ]]; then
+ DEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/omarchy-${PACKAGE}.XXXXXX")
+else
+ mkdir -p "$DEST_DIR"
+ DEST_DIR=$(realpath "$DEST_DIR")
+fi
+
+UPSTREAM_DIR="$DEST_DIR/upstream"
+PATCHED_DIR="$DEST_DIR/patched"
+CURRENT_DIR="$DEST_DIR/current"
+
+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"
+
+ sed -i "s/^pkgrel=.*/pkgrel=$pkgrel/" "$package_dir/PKGBUILD"
+}
+
+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
+}
+
+remove_aur_only_files() {
+ local package_dir="$1"
+
+ rm -f "$package_dir/.SRCINFO" "$package_dir/.gitignore"
+}
+
+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)
+ shopt -u nullglob
+ [[ ${#patch_files[@]} -gt 0 ]] || return 1
+
+ local patch_file
+ for patch_file in "${patch_files[@]}"; do
+ print_info " Applying $(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_post_sync_hook() {
+ local package_dir="$1"
+ local aur_pkgrel="$2"
+ local hook="$package_dir/.omarchy/post-sync.sh"
+
+ [[ -f "$hook" ]] || return 1
+
+ print_info " Running post-sync.sh"
+ if ! (
+ cd "$package_dir"
+ PACKAGE_NAME="$PACKAGE" \
+ AUR_PACKAGE_NAME="$AUR_PACKAGE" \
+ AUR_PKGREL="$aur_pkgrel" \
+ bash ".omarchy/post-sync.sh"
+ ); then
+ print_error "post-sync.sh failed"
+ return 2
+ fi
+}
+
+apply_pkgrel_metadata() {
+ local package_dir="$1"
+ local aur_pkgrel="$2"
+ local previous_pkgver="$3"
+ local metadata="$package_dir/.omarchy/package.json"
+
+ jq -e 'has("pkgrel")' "$metadata" >/dev/null || return 1
+
+ local current_pkgver suffix offset base
+ current_pkgver=$(get_pkgbuild_field "$package_dir" pkgver)
+
+ if [[ -n "$previous_pkgver" && "$current_pkgver" != "$previous_pkgver" ]]; then
+ print_info " Skipping stale pkgrel metadata (pkgver changed: $previous_pkgver -> $current_pkgver)"
+ return 1
+ fi
+
+ suffix=$(jq -r '.pkgrel.suffix // 1' "$metadata")
+ offset=$(jq -r '.pkgrel.offset // 0' "$metadata")
+ base=$((aur_pkgrel + offset))
+
+ print_info " Applying pkgrel metadata: $base.$suffix"
+ set_pkgrel "$package_dir" "$base.$suffix"
+}
+
+apply_auto_pkgrel_suffix() {
+ local package_dir="$1"
+ local aur_pkgrel="$2"
+
+ print_info " Applying automatic pkgrel suffix: $aur_pkgrel.1"
+ set_pkgrel "$package_dir" "$aur_pkgrel.1"
+}
+
+print_header "Package Worktree: $PACKAGE"
+print_info "Workspace: $DEST_DIR"
+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"
+fi
+
+rm -rf "$UPSTREAM_DIR" "$PATCHED_DIR" "$CURRENT_DIR"
+
+git clone "https://aur.archlinux.org/${AUR_PACKAGE}.git" "$UPSTREAM_DIR" >/dev/null 2>&1
+if [[ -n "$UPSTREAM_COMMIT" ]]; then
+ git -C "$UPSTREAM_DIR" checkout --detach "$UPSTREAM_COMMIT" >/dev/null 2>&1
+fi
+rm -rf "$UPSTREAM_DIR/.git"
+
+copy_aur_contents "$UPSTREAM_DIR" "$PATCHED_DIR"
+rm -rf "$PATCHED_DIR/.omarchy"
+cp -a "$PACKAGE_DIR/.omarchy" "$PATCHED_DIR/.omarchy"
+cp -a "$PACKAGE_DIR" "$CURRENT_DIR"
+
+AUR_PKGREL=$(get_pkgbuild_field "$UPSTREAM_DIR" pkgrel)
+PREVIOUS_PKGVER=$(get_pkgbuild_field "$PACKAGE_DIR" pkgver || true)
+CUSTOMIZED=false
+PKGREL_APPLIED=false
+
+print_info "Creating patched/ from upstream/ + .omarchy customizations"
+if apply_omarchy_patches "$PATCHED_DIR"; then
+ CUSTOMIZED=true
+else
+ patch_status=$?
+ if [[ $patch_status -eq 2 ]]; then
+ exit 1
+ fi
+fi
+
+if run_post_sync_hook "$PATCHED_DIR" "$AUR_PKGREL"; then
+ CUSTOMIZED=true
+else
+ hook_status=$?
+ if [[ $hook_status -eq 2 ]]; then
+ exit 1
+ fi
+fi
+
+if apply_pkgrel_metadata "$PATCHED_DIR" "$AUR_PKGREL" "$PREVIOUS_PKGVER"; then
+ PKGREL_APPLIED=true
+fi
+
+if [[ "$CUSTOMIZED" == true && "$PKGREL_APPLIED" == false ]]; then
+ apply_auto_pkgrel_suffix "$PATCHED_DIR" "$AUR_PKGREL"
+fi
+
+remove_aur_only_files "$PATCHED_DIR"
+remove_aur_only_files "$CURRENT_DIR"
+
+cat < Mirror to use (edge or stable, default: edge)"
echo " --package Build only the specified package"
echo " --sync-remote Rclone remote for sync (default: pkgs.omarchy.org:omarchy-pkgs)"
- echo " --skip-prod-check Skip production environment check during sync"
+ echo " --skip-prod-check Skip production environment check during sync
+ --dry-run Show build plan only; do not sign/promote/clean/update/sync"
echo " -h, --help Show this help message"
echo ""
echo "This script runs the complete workflow:"
@@ -80,13 +87,23 @@ print_info "Build workspace: $BUILD_OUTPUT_DIR"
# Step 1: Build
echo ""
-print_info "Step 1/6: Building packages..."
+if [[ "$DRY_RUN" == true ]]; then
+ print_info "Step 1/6: Planning build..."
+else
+ print_info "Step 1/6: Building packages..."
+fi
"$BUILD_ROOT/bin/build" "${BUILD_ARGS[@]}" || {
print_error "Build failed"
notify_error "Release failed: Build step failed" "Mirror: $MIRROR | Arch: $ARCH"
exit 1
}
+if [[ "$DRY_RUN" == true ]]; then
+ echo ""
+ print_success "Release dry run complete (build plan only)."
+ exit 0
+fi
+
# Step 2: Sign
echo ""
print_info "Step 2/6: Signing packages..."
diff --git a/bin/repo b/bin/repo
index 6ca8fce..bce345f 100755
--- a/bin/repo
+++ b/bin/repo
@@ -47,13 +47,13 @@ show_usage() {
echo ""
echo "Commands:"
echo " release Run complete workflow: build → sign → promote → clean → update → sync"
- echo " build Build AUR packages (unsigned)"
+ echo " build Build packages (unsigned)"
echo " sign Sign all packages in build-output"
echo " promote Promote build to production (build-output → pkgs.omarchy.org)"
echo " update Update repository database"
echo " clean Clean old package versions"
echo " migrate Migrate edge packages into stable (no overwrite), then clean + update"
- echo " list List packages in repository"
+ echo " list List source package metadata (use --repo for published repo)"
echo " remove Remove a specific package"
echo " sync Sync repository to remote"
echo ""
diff --git a/bin/sync-aur b/bin/sync-aur
index 66e9b92..1d177ad 100755
--- a/bin/sync-aur
+++ b/bin/sync-aur
@@ -1,46 +1,50 @@
#!/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
-trap "rm -rf $TEMP_DIR" EXIT
-
-# Default tier
-TIER=""
SPECIFIC_PACKAGES=()
-# Parse arguments
+usage() {
+ cat </.
+
+Package selection is driven by pkgbuilds//.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
- --tier)
- TIER="$2"
- if [[ "$TIER" != "edge" && "$TIER" != "shared" ]]; then
- print_error "Invalid tier: $TIER (must be 'edge' or 'shared')"
- exit 1
- fi
- shift 2
- ;;
-h|--help)
- echo "Usage: $0 --tier [PACKAGE...]"
- echo ""
- echo "Sync AUR packages to pkgbuilds// directory"
- echo ""
- echo "Options:"
- echo " --tier Target tier: edge or shared (required)"
- echo ""
- echo "Arguments:"
- echo " PACKAGE One or more package names to sync (optional)"
- echo ""
- echo "Examples:"
- echo " $0 --tier edge # Sync all packages from edge.packages"
- echo " $0 --tier shared # Sync all packages from shared.packages"
- echo " $0 --tier edge yay # Sync only the yay package to edge/"
- echo " $0 --tier shared cursor-bin # Sync only cursor-bin to shared/"
+ 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
@@ -48,127 +52,355 @@ while [[ $# -gt 0 ]]; do
esac
done
-# Require tier
-if [[ -z "$TIER" ]]; then
- print_error "Missing required --tier argument"
- echo "Usage: $0 --tier [PACKAGE...]"
- exit 1
-fi
-
-# Set paths based on tier
-PACKAGES_FILE="$BUILD_ROOT/build/packages/${TIER}.packages"
-TARGET_DIR="$PKGBUILDS_DIR/$TIER"
-
-print_header "AUR Package Sync (tier: $TIER)"
-
-mkdir -p "$TARGET_DIR"
-
-if [[ ! -f "$PACKAGES_FILE" ]]; then
- print_error "Package list not found: $PACKAGES_FILE"
- exit 1
-fi
+print_header "AUR Package Sync"
+mkdir -p "$PKGBUILDS_DIR"
SYNCED=0
+SKIPPED=0
FAILED=0
SYNCED_PACKAGES=()
+SPECIFIC_MODE=false
-# Function to sync a package from AUR
-sync_package() {
- local package="$1"
-
- print_info "Syncing $package from AUR..."
-
- cd "$TEMP_DIR"
- rm -rf "$package"
-
- if git clone "https://aur.archlinux.org/${package}.git" 2>/dev/null; then
- # Create target directory if it doesn't exist
- mkdir -p "$TARGET_DIR/$package"
-
- # Copy files (not .git directory)
- cp -r "$package"/* "$TARGET_DIR/$package/" 2>/dev/null || true
- cp -r "$package"/.* "$TARGET_DIR/$package/" 2>/dev/null || true
-
- # Remove .git from destination
- rm -rf "$TARGET_DIR/$package/.git"
-
- SYNCED_PACKAGES+=("$package")
- ((SYNCED++))
- else
- print_warning "Failed to clone $package (may not exist in AUR)"
- ((FAILED++))
+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
}
-# If specific packages provided, sync only those
+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
+ current_pkgver=$(grep -m1 '^pkgver=' "$pkgbuild" | cut -d= -f2)
+
+ 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
+}
+
+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"
+ rm -rf "$aur_package"
+
+ if ! git clone "https://aur.archlinux.org/${aur_package}.git" "$aur_package" >/dev/null 2>&1; then
+ print_warning "Failed to clone $aur_package (may 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
- # Sync all packages from list
- while IFS= read -r line || [ -n "$line" ]; do
- [[ "$line" =~ ^#.*$ ]] && continue
- [[ -z "$line" ]] && continue
-
- package=$(echo "$line" | awk '{print $1}')
-
- # Skip GitHub repos (contain /)
- if [[ "$package" == *"/"* ]]; then
- continue
- fi
-
+ while IFS= read -r package; do
sync_package "$package"
- done < <(grep -v "^#" "$PACKAGES_FILE" | grep -v "^$")
+ done < <(packages_for_aur_sync)
fi
-# Apply patches only to packages that were actually synced
-for package in "${SYNCED_PACKAGES[@]}"; do
- if [[ -d "$TARGET_DIR/$package/patches" ]]; then
- print_info "Applying patches for $package..."
- (cd "$TARGET_DIR/$package" && shopt -s nullglob && for patch in patches/*.patch; do
- if ! patch -p1 --no-backup-if-mismatch < "$patch"; then
- print_error "Failed to apply patch: $patch"
- exit 1
- fi
- done)
- fi
-done
-
-# Apply pkgrel overrides (version-pinned pkgrel bumps that self-clean)
-for package in "${SYNCED_PACKAGES[@]}"; do
- override_file="$TARGET_DIR/$package/pkgrel.override"
- [[ -f "$override_file" ]] || continue
-
- pkgbuild="$TARGET_DIR/$package/PKGBUILD"
- srcinfo="$TARGET_DIR/$package/.SRCINFO"
- gitignore="$TARGET_DIR/$package/.gitignore"
- current_pkgver=$(grep -m1 '^pkgver=' "$pkgbuild" | cut -d= -f2)
- matched=false
-
- while IFS='=' read -r ver rel; do
- [[ "$ver" =~ ^#.*$ || -z "$ver" ]] && continue
- if [[ "$current_pkgver" == "$ver" ]]; then
- print_info "Applying pkgrel override for $package: pkgver=$ver -> pkgrel=$rel"
- sed -i "s/^pkgrel=.*/pkgrel=$rel/" "$pkgbuild"
- sed -i "s/pkgrel = .*/pkgrel = $rel/" "$srcinfo"
- # Ensure .gitignore tracks the override file (AUR may have overwritten it)
- if [[ -f "$gitignore" ]] && ! grep -q '^!pkgrel.override$' "$gitignore"; then
- echo '!pkgrel.override' >> "$gitignore"
- fi
- matched=true
- break
- fi
- done < "$override_file"
-
- if [[ "$matched" == false ]]; then
- print_info "Removing stale pkgrel.override for $package (no match for pkgver=$current_pkgver)"
- rm "$override_file"
- fi
-done
-
echo ""
-print_success "Sync complete! (tier: $TIER)"
-echo " Target: $TARGET_DIR"
+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
diff --git a/build/build.sh b/build/build.sh
index 28edfeb..b698a72 100755
--- a/build/build.sh
+++ b/build/build.sh
@@ -1,90 +1,86 @@
#!/bin/bash
-# Build script - builds packages based on mirror tier
-# Edge builds: /pkgbuilds/edge/* + /pkgbuilds/shared/*
-# Stable builds: /pkgbuilds/shared/* only (edge packages are promoted via binary copy)
-
-# Import GPG keys
-/build/import-gpg-keys.sh || exit 1
+# Build script - builds packages based on package metadata
+# Edge builds all packages. Stable builds only packages in the fast release ring.
# Setup directories
ARCH=${ARCH:-x86_64}
MIRROR=${MIRROR:-edge}
-BUILD_OUTPUT_DIR="/build-output/$MIRROR/$ARCH"
-FINAL_OUTPUT_DIR="/pkgs.omarchy.org/$MIRROR/$ARCH"
+DRY_RUN=${DRY_RUN:-false}
+PKGBUILDS_DIR=${PKGBUILDS_DIR:-/pkgbuilds}
+BUILD_OUTPUT_DIR=${BUILD_OUTPUT_DIR:-/build-output/$MIRROR/$ARCH}
+FINAL_OUTPUT_DIR=${FINAL_OUTPUT_DIR:-/pkgs.omarchy.org/$MIRROR/$ARCH}
+HELPERS_DIR=${HELPERS_DIR:-/helpers}
+SRC_DIR=${SRC_DIR:-/src}
-# Determine which package directories to use based on mirror
-# Edge builds from edge + shared; stable only builds shared (edge packages are promoted via binary copy)
-if [[ "$MIRROR" == "stable" ]]; then
- PKGBUILD_DIRS="/pkgbuilds/shared"
-else
- PKGBUILD_DIRS="/pkgbuilds/edge /pkgbuilds/shared"
-fi
+source "$HELPERS_DIR/package-metadata.sh"
-mkdir -p "$BUILD_OUTPUT_DIR" "$FINAL_OUTPUT_DIR"
+if [[ "$DRY_RUN" != true ]]; then
+ # Import GPG keys
+ /build/import-gpg-keys.sh || exit 1
-# Configure Omarchy repositories for dependency resolution
-echo "==> Configuring Omarchy repositories for dependency resolution..."
+ mkdir -p "$BUILD_OUTPUT_DIR" "$FINAL_OUTPUT_DIR"
-# Always add omarchy-build repo (for incremental builds)
-# Packages in build-output are unsigned, so use SigLevel = Never
-sudo tee -a /etc/pacman.conf > /dev/null < Configuring Omarchy repositories for dependency resolution..."
+
+ # Always add omarchy-build repo (for incremental builds)
+ # Packages in build-output are unsigned, so use SigLevel = Never
+ sudo tee -a /etc/pacman.conf > /dev/null < omarchy-build (priority 1): $BUILD_OUTPUT_DIR"
+ echo " -> omarchy-build (priority 1): $BUILD_OUTPUT_DIR"
-# Initialize empty build database if it doesn't exist
-cd "$BUILD_OUTPUT_DIR"
-if [[ ! -f "omarchy-build.db.tar.zst" ]]; then
- # Create an empty database
- repo-add omarchy-build.db.tar.zst >/dev/null 2>&1
- ln -sf omarchy-build.db.tar.zst omarchy-build.db
-else
- # Database exists, check if we need to rebuild it from packages
- if ls *.pkg.tar.* 2>/dev/null | grep -v '\.sig$' | grep -v 'omarchy-build\.db' | grep -q .; then
- echo "==> Rebuilding build database from existing packages..."
- ls *.pkg.tar.* | grep -v '\.sig$' | grep -v 'omarchy-build\.db' | xargs -r repo-add omarchy-build.db.tar.zst >/dev/null 2>&1
+ # Initialize empty build database if it doesn't exist
+ cd "$BUILD_OUTPUT_DIR"
+ if [[ ! -f "omarchy-build.db.tar.zst" ]]; then
+ # Create an empty database
+ repo-add omarchy-build.db.tar.zst >/dev/null 2>&1
ln -sf omarchy-build.db.tar.zst omarchy-build.db
+ else
+ # Database exists, check if we need to rebuild it from packages
+ if ls *.pkg.tar.* 2>/dev/null | grep -v '\.sig$' | grep -v 'omarchy-build\.db' | grep -q .; then
+ echo "==> Rebuilding build database from existing packages..."
+ ls *.pkg.tar.* | grep -v '\.sig$' | grep -v 'omarchy-build\.db' | xargs -r repo-add omarchy-build.db.tar.zst >/dev/null 2>&1
+ ln -sf omarchy-build.db.tar.zst omarchy-build.db
+ fi
fi
-fi
-# Add omarchy repo if it has a database (stable packages)
-if [[ -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" ]] || [[ -f "$FINAL_OUTPUT_DIR/omarchy.db" ]]; then
- sudo tee -a /etc/pacman.conf > /dev/null < /dev/null < omarchy (priority 2): $FINAL_OUTPUT_DIR"
-fi
+ echo " -> omarchy (priority 2): $FINAL_OUTPUT_DIR"
+ fi
-# Sync pacman database
-sudo pacman -Sy
+ # Sync pacman database
+ sudo pacman -Sy
+fi
echo "==> Package Builder"
echo "==> Target architecture: $ARCH"
echo "==> Mirror: $MIRROR"
-echo "==> Package directories: $PKGBUILD_DIRS"
+echo "==> Package root: $PKGBUILDS_DIR"
echo "==> Build workspace: $BUILD_OUTPUT_DIR"
echo "==> Final output: $FINAL_OUTPUT_DIR"
+if [[ "$DRY_RUN" == true ]]; then
+ echo "==> Dry run: yes (plan only; makepkg will not run)"
+fi
FAILED_PACKAGES=""
SUCCESSFUL_PACKAGES=""
SKIPPED_PACKAGES=""
-# Find package directory - searches through PKGBUILD_DIRS
+# Find package directory
find_package_dir() {
local pkg="$1"
- for dir in $PKGBUILD_DIRS; do
- if [[ -d "$dir/$pkg" ]]; then
- echo "$dir/$pkg"
- return 0
- fi
- done
- return 1
+ package_dir_for_name "$pkg"
}
# Get version from final output (production packages)
@@ -228,7 +224,7 @@ get_package_deps() {
) | tr ' ' '\n' | while read -r dep; do
# Strip version constraints (e.g., 'hyprshade>=1.0' -> 'hyprshade')
dep=$(echo "$dep" | sed 's/[<>=].*$//')
- # Check if this dependency exists in our pkgbuilds (any tier)
+ # Check if this dependency exists in our pkgbuilds
if find_package_dir "$dep" >/dev/null 2>&1; then
echo "$dep"
fi
@@ -315,22 +311,15 @@ check_needs_build() {
fi
}
-# Collect all packages from the relevant directories
+# Collect packages that should be built for the selected mirror
collect_packages() {
- for dir in $PKGBUILD_DIRS; do
- if [[ -d "$dir" ]]; then
- for pkgdir in "$dir"/*/; do
- [[ ! -d "$pkgdir" ]] && continue
- local pkg=$(basename "$pkgdir")
- [[ ! -f "$pkgdir/PKGBUILD" ]] && continue
- echo "$pkg"
- done
- fi
- done
+ packages_for_mirror "$MIRROR"
}
# Main execution
-cd /src
+if [[ "$DRY_RUN" != true ]]; then
+ cd "$SRC_DIR"
+fi
TOTAL_COUNT=0
@@ -345,10 +334,20 @@ if [[ -n "$PACKAGES" ]]; then
for pkg_name in $PACKAGES; do
pkgdir=$(find_package_dir "$pkg_name")
if [[ -z "$pkgdir" || ! -f "$pkgdir/PKGBUILD" ]]; then
- echo "==> ERROR: Package '$pkg_name' not found in $PKGBUILD_DIRS"
+ echo "==> ERROR: Package '$pkg_name' not found in $PKGBUILDS_DIR"
exit 1
fi
+ if ! package_builds_for_mirror "$pkgdir" "$MIRROR"; then
+ if [[ "$MIRROR" == "stable" ]]; then
+ echo " - $pkg_name - not in release_ring=fast; build edge and promote with repo migrate"
+ else
+ echo " - $pkg_name - not configured for direct $MIRROR builds"
+ fi
+ SKIPPED_PACKAGES="$SKIPPED_PACKAGES $pkg_name"
+ continue
+ fi
+
# Check if package should be built for this architecture
if ! should_build_for_arch "$pkg_name"; then
echo " - $pkg_name - not built for $ARCH"
@@ -446,6 +445,12 @@ else
echo "==> Build order: ${ORDERED_PACKAGES[@]}"
+ if [[ "$DRY_RUN" == true ]]; then
+ echo ""
+ echo "==> Dry run complete. Packages that would build: ${ORDERED_PACKAGES[@]}"
+ exit 0
+ fi
+
# Determine which packages need to be installed for other packages being built
declare -A INSTALL_PACKAGES
for pkg in "${ORDERED_PACKAGES[@]}"; do
diff --git a/helpers/package-metadata.sh b/helpers/package-metadata.sh
new file mode 100644
index 0000000..68e008b
--- /dev/null
+++ b/helpers/package-metadata.sh
@@ -0,0 +1,184 @@
+# Package metadata helpers for Omarchy package build tooling
+#
+# 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", "pkgrel": { "suffix": 1, "offset": 1 } }
+# { "source": "local" }
+#
+# bin/sync-aur also writes upstream_commit for AUR-backed packages.
+
+if [[ -z "${PKGBUILDS_DIR:-}" ]]; then
+ if [[ -n "${BUILD_ROOT:-}" ]]; then
+ PKGBUILDS_DIR="$BUILD_ROOT/pkgbuilds"
+ elif [[ -d /pkgbuilds ]]; then
+ PKGBUILDS_DIR="/pkgbuilds"
+ else
+ PKGBUILDS_DIR="pkgbuilds"
+ fi
+fi
+
+metadata_file_for_dir() {
+ local pkgdir="$1"
+ echo "$pkgdir/.omarchy/package.json"
+}
+
+package_dir_for_name() {
+ local package="$1"
+ local pkgdir="$PKGBUILDS_DIR/$package"
+
+ [[ -d "$pkgdir" && -f "$pkgdir/PKGBUILD" ]] || return 1
+ echo "$pkgdir"
+}
+
+package_metadata_value() {
+ local pkgdir="$1"
+ local jq_filter="$2"
+ local default="${3:-}"
+ local metadata
+
+ metadata=$(metadata_file_for_dir "$pkgdir")
+ if [[ ! -f "$metadata" ]]; then
+ echo "$default"
+ return 0
+ fi
+
+ jq -r --arg default "$default" "$jq_filter // \$default" "$metadata"
+}
+
+package_sync_enabled() {
+ local pkgdir="$1"
+ local metadata source sync
+
+ metadata=$(metadata_file_for_dir "$pkgdir")
+ [[ -f "$metadata" ]] || return 1
+
+ source=$(jq -r '.source // ""' "$metadata")
+ [[ "$source" == "aur" ]] || return 1
+
+ sync=$(jq -r 'if has("sync") then .sync else true end' "$metadata")
+ [[ "$sync" != "false" ]]
+}
+
+package_release_ring() {
+ local pkgdir="$1"
+ package_metadata_value "$pkgdir" '.release_ring' ""
+}
+
+package_is_fast_ring() {
+ local pkgdir="$1"
+ [[ "$(package_release_ring "$pkgdir")" == "fast" ]]
+}
+
+package_has_metadata() {
+ local pkgdir="$1"
+ [[ -f "$(metadata_file_for_dir "$pkgdir")" ]]
+}
+
+package_has_pkgbuild() {
+ local pkgdir="$1"
+ [[ -f "$pkgdir/PKGBUILD" ]]
+}
+
+package_builds_for_mirror() {
+ local pkgdir="$1"
+ local mirror="$2"
+
+ package_has_pkgbuild "$pkgdir" || return 1
+ package_has_metadata "$pkgdir" || return 1
+
+ case "$mirror" in
+ edge)
+ return 0
+ ;;
+ stable)
+ package_is_fast_ring "$pkgdir"
+ ;;
+ *)
+ return 1
+ ;;
+ esac
+}
+
+package_dirs() {
+ [[ -d "$PKGBUILDS_DIR" ]] || return 0
+
+ find "$PKGBUILDS_DIR" -mindepth 1 -maxdepth 1 -type d -print | sort | while IFS= read -r pkgdir; do
+ package_has_pkgbuild "$pkgdir" || continue
+ package_has_metadata "$pkgdir" || continue
+ echo "$pkgdir"
+ done
+}
+
+packages_for_aur_sync() {
+ package_dirs | while IFS= read -r pkgdir; do
+ if package_sync_enabled "$pkgdir"; then
+ basename "$pkgdir"
+ fi
+ done
+}
+
+packages_for_mirror() {
+ local mirror="$1"
+
+ package_dirs | while IFS= read -r pkgdir; do
+ if package_builds_for_mirror "$pkgdir" "$mirror"; then
+ basename "$pkgdir"
+ fi
+ done
+}
+
+validate_package_metadata() {
+ local pkgdir="$1"
+ local metadata source sync aur ring pkgrel_type
+
+ metadata=$(metadata_file_for_dir "$pkgdir")
+ [[ -f "$metadata" ]] || { echo "missing metadata: $metadata"; return 1; }
+
+ jq empty "$metadata" >/dev/null || return 1
+
+ source=$(jq -r '.source // ""' "$metadata")
+ case "$source" in
+ aur|local) ;;
+ *) echo "invalid source for $(basename "$pkgdir"): $source"; return 1 ;;
+ esac
+
+ sync=$(jq -r 'if has("sync") then .sync | type else "missing" end' "$metadata")
+ case "$sync" in
+ boolean|missing) ;;
+ *) echo "invalid sync for $(basename "$pkgdir"): must be boolean"; return 1 ;;
+ esac
+
+ aur=$(jq -r 'if has("aur") then .aur | type else "missing" end' "$metadata")
+ case "$aur" in
+ string|missing) ;;
+ *) echo "invalid aur for $(basename "$pkgdir"): must be string"; return 1 ;;
+ esac
+
+ ring=$(jq -r '.release_ring // ""' "$metadata")
+ case "$ring" in
+ ""|fast) ;;
+ *) echo "invalid release_ring for $(basename "$pkgdir"): $ring"; return 1 ;;
+ esac
+
+ pkgrel_type=$(jq -r 'if has("pkgrel") then .pkgrel | type else "missing" end' "$metadata")
+ case "$pkgrel_type" in
+ object|missing) ;;
+ *) echo "invalid pkgrel for $(basename "$pkgdir"): must be an object with optional suffix/offset"; return 1 ;;
+ esac
+
+ if ! jq -e '(.pkgrel // {}) | type == "object" and ((.suffix // 1) | type == "number" and floor == . and . >= 1) and ((.offset // 0) | type == "number" and floor == . and . >= 0)' "$metadata" >/dev/null; then
+ echo "invalid pkgrel for $(basename "$pkgdir"): must be an object with optional integer suffix >= 1 and offset >= 0"
+ return 1
+ fi
+
+ if ! jq -e '(.upstream_commit // "") | type == "string"' "$metadata" >/dev/null; then
+ echo "invalid upstream_commit for $(basename "$pkgdir"): must be a string"
+ return 1
+ fi
+}