Every pull request now builds the package directories it touches on ephemeral DigitalOcean droplets, and every merge to master publishes the resulting artifacts into the channels each package belongs to. The repository host's timers become the fallback rather than the pipeline. Build (.github/workflows/build-pr.yml) One job per package per architecture, always against edge. The artifact is labelled with the package directory's git tree hash. Tooling (bin/, helpers/, build/) is checked out from the base branch; the PR supplies only pkgbuilds/, so a PR can change what is built, never how. Builds run only for trusted authors: collaborators, .github/VOUCHED.td, or a PR carrying the build-approved label. A single required check, result, aggregates the matrix. Publish (.github/workflows/publish.yml, bin/publish-artifact) One job per merge. It collects the PR artifacts for the merged tree, builds anything that has none, then walks each channel/architecture slot once: pull that database, repo-add every package that belongs in it, upload packages, signatures, then the database. A published filename is immutable; identical bytes under an existing name only gain a database entry, different bytes are refused. Fast-ring packages reach edge, rc and stable in the same run from the same file. Matrix (bin/build-matrix) Package x architecture, with the channels the artifact ships to, decided by package_builds_for_mirror so CI and the host agree. arch=any packages build once and land in every architecture database. Builder (build/build.sh, bin/build, build/Dockerfile) With no local published tree, plan against and resolve from the public channel over HTTPS. PACKAGER is set to Omarchy. rclone is in the image. Runners (ci/) A controller droplet polls GitHub with curl and creates one g5 droplet per queued job from cloud-init, deleting them when off or over-age. Builders carry QEMU with credential support for aarch64. Operator SSH keys come from GitHub. tests/controller.sh and tests/publish-artifact.sh cover the decisions against fixtures and real makepkg output. Tests run on pull requests only; branch protection requires result, self-tests and build-isolation with up-to-date branches.
715 lines
27 KiB
Bash
Executable File
715 lines
27 KiB
Bash
Executable File
#!/bin/bash
|
|
# Plan a run or build one planned package in an isolated container.
|
|
# Unscoped edge builds exclude skip_build packages. Stable also requires the fast release ring.
|
|
# Explicit --package selections may build packages with skip_build=true.
|
|
|
|
# Setup directories
|
|
ARCH=${ARCH:-x86_64}
|
|
# ARCH selects the repository target for this script, but make and Kbuild also
|
|
# interpret an exported ARCH themselves (Linux calls this target "arm64").
|
|
# Keep the shell variable local to the orchestrator so PKGBUILDs see CARCH only.
|
|
export -n ARCH
|
|
MIRROR=${MIRROR:-edge}
|
|
DRY_RUN=${DRY_RUN:-false}
|
|
# bin/build plans the whole run, then invokes this script once per package in
|
|
# a fresh container. PACKAGES retains the original request for validation.
|
|
BUILD_PACKAGE=${BUILD_PACKAGE:-}
|
|
BUILD_PLAN_DIR=${BUILD_PLAN_DIR:-}
|
|
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}
|
|
# Set by bin/build from OMARCHY_DEFER_RUNTIME_DEPS after it has checked the
|
|
# request; re-checked here so the container never trusts a stray value.
|
|
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
|
|
fi
|
|
if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then
|
|
deferred_runtime=0
|
|
deferred_settings=0
|
|
deferred_count=0
|
|
for package in $PACKAGES; do
|
|
((deferred_count += 1))
|
|
case $package in
|
|
omarchy|omarchy-dev) deferred_runtime=1 ;;
|
|
omarchy-settings|omarchy-settings-dev) deferred_settings=1 ;;
|
|
*)
|
|
echo "Runtime dependency deferral only applies to the omarchy pair, not $package" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
if (( deferred_runtime != 1 || deferred_settings != 1 || deferred_count != 2 )); then
|
|
echo "Runtime dependency deferral requires exactly the omarchy pair" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" != true ]]; then
|
|
if [[ -z "$BUILD_PACKAGE" || -z "$BUILD_PLAN_DIR" ]]; then
|
|
echo "Use bin/build to plan and run isolated package builds" >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -Fxq -- "$BUILD_PACKAGE" "$BUILD_PLAN_DIR/packages"; then
|
|
echo "Package is not in the build plan: $BUILD_PACKAGE" >&2
|
|
exit 1
|
|
fi
|
|
# Import GPG keys
|
|
/build/import-gpg-keys.sh || exit 1
|
|
|
|
mkdir -p "$BUILD_OUTPUT_DIR" "$FINAL_OUTPUT_DIR"
|
|
|
|
# Bring the container up to date before any makedepends are installed. The
|
|
# image is layer-cached, so its glibc drifts behind the mirror while makepkg
|
|
# -s pulls makedepends from the freshly synced database -- a partial upgrade
|
|
# that breaks the new packages (imagemagick wanting GLIBC_2.44, etc).
|
|
# Done before the Omarchy repos are added so only core/extra participate.
|
|
echo "==> Updating build container packages..."
|
|
sudo pacman -Syu --noconfirm || exit 1
|
|
|
|
# Configure Omarchy repositories for dependency resolution
|
|
echo "==> Configuring Omarchy repositories for dependency resolution..."
|
|
|
|
# Always add omarchy-build repo first (for incremental builds). Repository
|
|
# order is pacman's priority order, so this must precede the official repos;
|
|
# otherwise pacman can select an older official package with the same name.
|
|
# Packages in build-output are unsigned, so use SigLevel = Never.
|
|
sudo sed -i "/^\[core\]$/i [omarchy-build]\nSigLevel = Never\nServer = file://$BUILD_OUTPUT_DIR\n" /etc/pacman.conf
|
|
echo " -> omarchy-build (priority 1): $BUILD_OUTPUT_DIR"
|
|
|
|
# Initialize empty build database if it doesn't exist
|
|
cd "$BUILD_OUTPUT_DIR" || exit 1
|
|
if [[ ! -f "omarchy-build.db.tar.zst" ]]; then
|
|
# Create an empty database
|
|
repo-add omarchy-build.db.tar.zst >/dev/null 2>&1 || exit 1
|
|
ln -sf omarchy-build.db.tar.zst omarchy-build.db || exit 1
|
|
fi
|
|
# Fold any packages already in the workspace into the database, whether
|
|
# they came with an existing database or were dropped in by an earlier
|
|
# workflow job (OMARCHY_KEEP_BUILD_WORKSPACE). Without this a seeded
|
|
# workspace with no database would leave those packages invisible to
|
|
# dependency resolution.
|
|
staged_packages=()
|
|
for staged in *.pkg.tar.*; do
|
|
[[ -f "$staged" && "$staged" != *.sig ]] || continue
|
|
staged_packages+=("$staged")
|
|
done
|
|
if (( ${#staged_packages[@]} )); then
|
|
# Seed once per run. After that, only successful builds update the DB;
|
|
# rescanning in every container could reintroduce a failed build's partial
|
|
# outputs or overwrite the new version with an older kept artifact.
|
|
if [[ ! -e "$BUILD_PLAN_DIR/repository-initialized" ]]; then
|
|
echo "==> Rebuilding build database from existing packages..."
|
|
repo-add omarchy-build.db.tar.zst "${staged_packages[@]}" >/dev/null 2>&1 || exit 1
|
|
ln -sf omarchy-build.db.tar.zst omarchy-build.db || exit 1
|
|
fi
|
|
# A resumed/repeated build can produce different bytes under the same
|
|
# filename. Never let the shared download cache substitute older bytes
|
|
# for the staged artifacts described by this run's database.
|
|
for staged in "${staged_packages[@]}"; do
|
|
sudo rm -f "/var/cache/pacman/pkg/$staged" "/var/cache/pacman/pkg/$staged.sig" || exit 1
|
|
done
|
|
fi
|
|
touch "$BUILD_PLAN_DIR/repository-initialized" || exit 1
|
|
|
|
# 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
|
|
sudo pacman -Sy || exit 1
|
|
fi
|
|
|
|
echo "==> Package Builder"
|
|
echo "==> Target architecture: $ARCH"
|
|
echo "==> Mirror: $MIRROR"
|
|
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
|
|
|
|
SKIPPED_PACKAGES=""
|
|
|
|
# Find package directory
|
|
find_package_dir() {
|
|
local pkg="$1"
|
|
package_dir_for_name "$pkg"
|
|
}
|
|
|
|
# Get version from final output (production packages)
|
|
#
|
|
# Source package directories are named after the PKGBUILD pkgbase, but split
|
|
# packages are stored in the repo DB under their individual pkgname entries.
|
|
# Cache versions by both %NAME% and %BASE% so a pkgbase like
|
|
# libretro-vice-git can be found even though the DB only contains packages like
|
|
# libretro-vice-x64-git.
|
|
declare -A LOCAL_VERSION_BY_NAME=()
|
|
declare -A LOCAL_VERSION_BY_BASE=()
|
|
LOCAL_VERSION_CACHE_LOADED=false
|
|
LOCAL_VERSION_CACHE_DB=""
|
|
|
|
load_local_versions() {
|
|
local db="$PUBLISHED_DB_DIR/omarchy.db.tar.zst"
|
|
|
|
if [[ ! -f "$db" ]]; then
|
|
db="$PUBLISHED_DB_DIR/omarchy.db"
|
|
fi
|
|
|
|
[[ -f "$db" ]] || return 0
|
|
[[ "$LOCAL_VERSION_CACHE_LOADED" == true && "$LOCAL_VERSION_CACHE_DB" == "$db" ]] && return 0
|
|
|
|
LOCAL_VERSION_BY_NAME=()
|
|
LOCAL_VERSION_BY_BASE=()
|
|
|
|
local name base version
|
|
while IFS=$'\t' read -r name base version; do
|
|
[[ -n "$name" && -n "$version" ]] && LOCAL_VERSION_BY_NAME["$name"]="$version"
|
|
[[ -n "$base" && -n "$version" ]] && LOCAL_VERSION_BY_BASE["$base"]="$version"
|
|
done < <(
|
|
tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk '
|
|
function emit() {
|
|
if (name != "" && version != "") print name "\t" base "\t" version
|
|
name=""; base=""; version=""
|
|
}
|
|
$0 == "%FILENAME%" { emit(); next }
|
|
$0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next }
|
|
$0 == "%BASE%" { getline; base=$0; next }
|
|
$0 == "%VERSION%" { getline; version=$0; next }
|
|
END { emit() }
|
|
'
|
|
)
|
|
|
|
LOCAL_VERSION_CACHE_LOADED=true
|
|
LOCAL_VERSION_CACHE_DB="$db"
|
|
}
|
|
|
|
get_local_version() {
|
|
local pkg="$1"
|
|
|
|
load_local_versions
|
|
|
|
if [[ -n "${LOCAL_VERSION_BY_NAME[$pkg]:-}" ]]; then
|
|
echo "${LOCAL_VERSION_BY_NAME[$pkg]}"
|
|
elif [[ -n "${LOCAL_VERSION_BY_BASE[$pkg]:-}" ]]; then
|
|
echo "${LOCAL_VERSION_BY_BASE[$pkg]}"
|
|
fi
|
|
}
|
|
|
|
# Check if package should be built for current architecture
|
|
# Returns 0 (success) if should build, 1 if should skip
|
|
should_build_for_arch() {
|
|
local pkg="$1"
|
|
local pkgdir
|
|
pkgdir=$(find_package_dir "$pkg")
|
|
[[ -n "$pkgdir" ]] && package_supports_arch "$pkgdir" "$ARCH"
|
|
}
|
|
|
|
# For VCS packages, makepkg recalculates pkgver() before the build. If the
|
|
# recalculated pkgver differs from the static PKGBUILD value, makepkg resets
|
|
# pkgrel to 1. That is right for stock VCS packages, but wrong for Omarchy's
|
|
# patched AUR packages where sync-aur intentionally applies a dotted local
|
|
# pkgrel suffix (for example 1.1) to sort above the upstream/AUR package.
|
|
# Refresh pkgver once, then restore the local dotted pkgrel before the real
|
|
# build so the produced package filename carries the Omarchy revision.
|
|
refresh_vcs_pkgver_preserving_local_pkgrel() {
|
|
local pkg="$1"
|
|
local pkgbuild="PKGBUILD"
|
|
|
|
grep -qE '^pkgver[[:space:]]*\(\)' "$pkgbuild" || return 0
|
|
|
|
local original_pkgver original_pkgrel refreshed_pkgver refreshed_pkgrel
|
|
original_pkgver=$(bash -c 'source PKGBUILD 2>/dev/null; echo "${pkgver:-}"')
|
|
original_pkgrel=$(bash -c 'source PKGBUILD 2>/dev/null; echo "${pkgrel:-}"')
|
|
|
|
# Omarchy local rebuilds use dotted pkgrels (AUR pkgrel + .suffix). Plain
|
|
# integer pkgrels can keep makepkg's normal reset-to-1 behavior on new VCS
|
|
# revisions.
|
|
[[ "$original_pkgrel" == *.* ]] || return 0
|
|
|
|
echo " Refreshing VCS pkgver before build (preserving local pkgrel=$original_pkgrel)..."
|
|
if [[ -x /usr/local/bin/pacman-for-makepkg ]]; then
|
|
PACMAN=/usr/local/bin/pacman-for-makepkg makepkg --nobuild --nodeps --skipinteg --skippgpcheck --noprepare --noconfirm
|
|
else
|
|
makepkg --nobuild --nodeps --skipinteg --skippgpcheck --noprepare --noconfirm
|
|
fi
|
|
|
|
if [[ $? -ne 0 ]]; then
|
|
echo " Failed to refresh VCS pkgver for $pkg"
|
|
return 1
|
|
fi
|
|
|
|
refreshed_pkgver=$(bash -c 'source PKGBUILD 2>/dev/null; echo "${pkgver:-}"')
|
|
refreshed_pkgrel=$(bash -c 'source PKGBUILD 2>/dev/null; echo "${pkgrel:-}"')
|
|
|
|
if [[ "$refreshed_pkgrel" != "$original_pkgrel" ]]; then
|
|
sed -i "s/^pkgrel=.*/pkgrel=$original_pkgrel/" PKGBUILD
|
|
echo " Restored local pkgrel suffix: $refreshed_pkgrel -> $original_pkgrel"
|
|
fi
|
|
|
|
if [[ -n "$refreshed_pkgver" && "$refreshed_pkgver" != "$original_pkgver" ]]; then
|
|
echo " Refreshed VCS version: $original_pkgver -> $refreshed_pkgver"
|
|
fi
|
|
}
|
|
|
|
# With runtime dependency checks deferred, makepkg runs --nodeps, so the
|
|
# build-time dependencies it would normally install with -s have to be
|
|
# installed explicitly: makedepends and checkdepends, including the
|
|
# architecture-suffixed variants for the current CARCH.
|
|
install_deferred_build_dependencies() {
|
|
local pkg="$1"
|
|
local -a build_deps=()
|
|
|
|
mapfile -t build_deps < <(
|
|
CARCH="$ARCH" bash -c '
|
|
source PKGBUILD
|
|
arch_makedepends="makedepends_${CARCH}[@]"
|
|
arch_checkdepends="checkdepends_${CARCH}[@]"
|
|
printf "%s\n" \
|
|
"${makedepends[@]}" "${!arch_makedepends}" \
|
|
"${checkdepends[@]}" "${!arch_checkdepends}"
|
|
' | awk 'NF && !seen[$0]++'
|
|
)
|
|
|
|
if (( ${#build_deps[@]} )); then
|
|
echo " Installing build-only dependencies for $pkg..."
|
|
sudo pacman -S --needed --noconfirm -- "${build_deps[@]}"
|
|
fi
|
|
}
|
|
|
|
# Build a package
|
|
build_package() {
|
|
local pkg="$1"
|
|
local pkgdir
|
|
pkgdir=$(find_package_dir "$pkg") || return 1
|
|
|
|
echo ""
|
|
echo " -> Processing: $pkg"
|
|
|
|
# Install this consumer's freshly built prerequisites in its own container.
|
|
# Qualifying the repository also upgrades an older dependency baked into
|
|
# the base image, even if that version would satisfy makepkg's check.
|
|
if [[ "$DEFER_RUNTIME_DEPS" != true ]]; then
|
|
local consumer dependency
|
|
local -a built_deps=()
|
|
while read -r consumer dependency; do
|
|
[[ "$consumer" == "$pkg" ]] && built_deps+=("omarchy-build/$dependency")
|
|
done < "$BUILD_PLAN_DIR/dependencies"
|
|
if (( ${#built_deps[@]} )); then
|
|
echo " Installing freshly built dependencies for $pkg..."
|
|
sudo /usr/local/bin/pacman-for-makepkg -S --needed --noconfirm -- "${built_deps[@]}" || return 1
|
|
fi
|
|
fi
|
|
|
|
# Copy to build directory
|
|
cd /src || return 1
|
|
rm -rf "$pkg" || return 1
|
|
cp -r "$pkgdir" "$pkg" || return 1
|
|
cd "/src/$pkg" || return 1
|
|
|
|
refresh_vcs_pkgver_preserving_local_pkgrel "$pkg" || {
|
|
return 1
|
|
}
|
|
|
|
# Get PKGBUILD version (including epoch if present)
|
|
local pkgbuild_version=$(bash -c 'source PKGBUILD; if [[ -n "$epoch" ]]; then echo "${epoch}:${pkgver}-${pkgrel}"; else echo "${pkgver}-${pkgrel}"; fi' 2>/dev/null)
|
|
|
|
if [[ -z "$pkgbuild_version" ]]; then
|
|
echo " Failed to read PKGBUILD version"
|
|
return 1
|
|
fi
|
|
|
|
# Show version info (version check already done in first pass)
|
|
local local_version=$(get_local_version "$pkg")
|
|
if [[ -n "$local_version" ]]; then
|
|
echo " Update available: $local_version -> $pkgbuild_version"
|
|
else
|
|
echo " New package (version: $pkgbuild_version)"
|
|
fi
|
|
|
|
# Import PGP keys from PKGBUILD validpgpkeys and keys/pgp/ directory
|
|
local pgp_keys=$(bash -c 'source PKGBUILD 2>/dev/null; echo "${validpgpkeys[@]}"')
|
|
if [[ -n "$pgp_keys" ]]; then
|
|
echo " Importing PGP keys from validpgpkeys..."
|
|
for key in $pgp_keys; do
|
|
gpg --receive-keys "$key" 2>/dev/null && echo " Received $key" || echo " Failed to receive $key"
|
|
done
|
|
fi
|
|
if [[ -d "keys/pgp" ]]; then
|
|
echo " Importing package-specific PGP keys..."
|
|
for keyfile in keys/pgp/*.asc; do
|
|
if [[ -f "$keyfile" ]]; then
|
|
gpg --import "$keyfile" 2>/dev/null && echo " Imported $(basename "$keyfile")" || echo " Failed to import $(basename "$keyfile")"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Build package without signing (signing is done separately)
|
|
# PACMAN override uses a wrapper that adds --ask 4 to auto-resolve conflicts
|
|
# (e.g. rustup replacing rust) since --noconfirm defaults to 'N' on those prompts
|
|
local -a makepkg_flags=(-scf --noconfirm)
|
|
if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then
|
|
# The pair's runtime dependencies (each other, and packages other jobs
|
|
# of the same pipeline build) are not resolvable here; the assembled set
|
|
# is installed in one verified transaction downstream. Only the
|
|
# build-time dependencies are installed, then makepkg skips the check.
|
|
install_deferred_build_dependencies "$pkg" || {
|
|
return 1
|
|
}
|
|
makepkg_flags=(-cf --noconfirm --nodeps)
|
|
fi
|
|
|
|
if PACMAN=/usr/local/bin/pacman-for-makepkg makepkg "${makepkg_flags[@]}"; then
|
|
# Ensure output directory exists
|
|
mkdir -p "$BUILD_OUTPUT_DIR"
|
|
|
|
# Copy only the artifacts makepkg declares as outputs. A PKGBUILD may use
|
|
# another pacman package as a source (schist-bin does); a *.pkg.tar.* glob
|
|
# would mistake that source archive for one of our freshly built packages.
|
|
local -a package_files=()
|
|
mapfile -t package_files < <(makepkg --packagelist)
|
|
|
|
if [[ ${#package_files[@]} -eq 0 ]]; then
|
|
echo " Makepkg produced no package files for $pkg"
|
|
return 1
|
|
fi
|
|
|
|
local -a new_pkgs=()
|
|
local pkg_path pkg_file
|
|
for pkg_path in "${package_files[@]}"; do
|
|
pkg_file=${pkg_path##*/}
|
|
if [[ ! -f "$pkg_file" ]]; then
|
|
# makepkg predicts an automatic -debug output whenever debug is
|
|
# enabled, but data-only packages may contain no symbols and therefore
|
|
# legitimately produce no debug archive.
|
|
if [[ "$pkg_file" == *-debug-*.pkg.tar.* ]]; then
|
|
continue
|
|
fi
|
|
|
|
echo " Expected package file was not produced: $pkg_file"
|
|
return 1
|
|
fi
|
|
|
|
cp "$pkg_file" "$BUILD_OUTPUT_DIR/" || return 1
|
|
new_pkgs+=("$pkg_file")
|
|
done
|
|
|
|
cd "$BUILD_OUTPUT_DIR" || return 1
|
|
|
|
# Add every output from this build, including split packages.
|
|
if [[ ${#new_pkgs[@]} -gt 0 ]]; then
|
|
repo-add omarchy-build.db.tar.zst "${new_pkgs[@]}" >/dev/null 2>&1 || return 1
|
|
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
|
|
echo " Makepkg failed for $pkg"
|
|
echo " DEBUG: Files in build directory:"
|
|
ls -lah *.pkg.tar.* 2>&1 | head -20 || echo " No package files found"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Get package dependencies from PKGBUILD
|
|
get_package_deps() {
|
|
local pkg="$1"
|
|
local pkgdir=$(find_package_dir "$pkg")
|
|
local pkgbuild="$pkgdir/PKGBUILD"
|
|
|
|
if [[ ! -f "$pkgbuild" ]]; then
|
|
return
|
|
fi
|
|
|
|
# Include test dependencies and target-specific arrays: each container must
|
|
# receive its prerequisites through the repository, not a previous build.
|
|
(
|
|
CARCH="$ARCH"
|
|
source "$pkgbuild" 2>/dev/null
|
|
for kind in depends makedepends checkdepends; do
|
|
generic="${kind}[@]"
|
|
specific="${kind}_${CARCH}[@]"
|
|
printf '%s\n' "${!generic}" "${!specific}"
|
|
done
|
|
) | awk 'NF && !seen[$0]++' | 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
|
|
if find_package_dir "$dep" >/dev/null 2>&1; then
|
|
echo "$dep"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# For VCS packages (those with a pkgver() function), the static pkgver= in the
|
|
# PKGBUILD is just a placeholder; the real version is computed at build time
|
|
# from the git checkout. Without this check, version comparison always reports a
|
|
# mismatch and we rebuild on every run, producing a package with the same
|
|
# name+version as one already in production. Detect this by comparing the
|
|
# upstream commit hash to the hash suffix already in the production version
|
|
# (both `...gabcdef0` and `...abcdef0` styles are common). Returns 0 when
|
|
# upstream is unchanged (build can be skipped).
|
|
check_vcs_unchanged() {
|
|
local pkg="$1"
|
|
local pkgdir="$2"
|
|
local pkgbuild="$pkgdir/PKGBUILD"
|
|
|
|
grep -qE '^pkgver[[:space:]]*\(\)' "$pkgbuild" || return 1
|
|
|
|
local local_version=$(get_local_version "$pkg")
|
|
[[ -z "$local_version" ]] && return 1
|
|
|
|
# If epoch or pkgrel changed in PKGBUILD, rebuild even if upstream is unchanged
|
|
local pkgbuild_epoch=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${epoch:-}"')
|
|
local pkgbuild_pkgrel=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${pkgrel}"')
|
|
|
|
local prod_pkgrel="${local_version##*-}"
|
|
local prod_no_pkgrel="${local_version%-*}"
|
|
local prod_epoch=""
|
|
if [[ "$prod_no_pkgrel" == *:* ]]; then
|
|
prod_epoch="${prod_no_pkgrel%%:*}"
|
|
fi
|
|
|
|
[[ "$pkgbuild_epoch" != "$prod_epoch" ]] && return 1
|
|
[[ "$pkgbuild_pkgrel" != "$prod_pkgrel" ]] && return 1
|
|
|
|
# Compare the commit represented in the published version to the current
|
|
# upstream ref. Supports unfragmented git sources as well as #branch=,
|
|
# #tag=, and #commit= fragments.
|
|
local prod_hash=$(package_extract_vcs_hash_from_version "$local_version")
|
|
[[ -z "$prod_hash" ]] && return 1
|
|
|
|
local upstream_hash=$(package_git_upstream_hash "$pkgdir")
|
|
[[ -z "$upstream_hash" ]] && return 1
|
|
|
|
[[ "$prod_hash" == "$upstream_hash" ]]
|
|
}
|
|
|
|
# Check which packages need building (version check only)
|
|
check_needs_build() {
|
|
local pkg="$1"
|
|
local pkgdir=$(find_package_dir "$pkg")
|
|
local pkgbuild="$pkgdir/PKGBUILD"
|
|
|
|
[[ ! -f "$pkgbuild" ]] && return 1
|
|
|
|
# Get PKGBUILD version (including epoch if present)
|
|
local pkgbuild_version=$(cd "$pkgdir" && bash -c 'source PKGBUILD; if [[ -n "$epoch" ]]; then echo "${epoch}:${pkgver}-${pkgrel}"; else echo "${pkgver}-${pkgrel}"; fi' 2>/dev/null)
|
|
[[ -z "$pkgbuild_version" ]] && return 1
|
|
|
|
# Check if already built
|
|
local local_version=$(get_local_version "$pkg")
|
|
|
|
if grep -qE '^pkgver[[:space:]]*\(\)' "$pkgbuild"; then
|
|
if [[ -n "$local_version" && -n "$(package_extract_vcs_hash_from_version "$local_version")" ]]; then
|
|
if check_vcs_unchanged "$pkg" "$pkgdir"; then
|
|
return 1 # VCS upstream ref is already represented in the repo
|
|
else
|
|
return 0 # New VCS ref, missing repo package, or pkgrel/epoch changed
|
|
fi
|
|
elif [[ "$local_version" == "$pkgbuild_version" ]]; then
|
|
return 1 # VCS package does not expose a hash; fall back to static version
|
|
else
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if [[ "$local_version" == "$pkgbuild_version" ]]; then
|
|
return 1 # Already up to date
|
|
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
|
|
collect_packages() {
|
|
packages_for_unscoped_build "$MIRROR" "$ARCH"
|
|
}
|
|
|
|
# Main execution
|
|
if [[ "$DRY_RUN" != true ]]; then
|
|
cd "$SRC_DIR" || exit 1
|
|
build_package "$BUILD_PACKAGE"
|
|
exit $?
|
|
fi
|
|
|
|
echo "==> Checking which packages need building..."
|
|
|
|
# First pass: determine which packages need building
|
|
PACKAGES_TO_BUILD=()
|
|
ORDERED_PACKAGES=()
|
|
PLANNED_DEPENDENCIES=()
|
|
|
|
# If PACKAGES is specified, only check those packages
|
|
if [[ -n "$PACKAGES" ]]; then
|
|
echo "==> Checking specified packages: $PACKAGES"
|
|
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 $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"
|
|
SKIPPED_PACKAGES="$SKIPPED_PACKAGES $pkg_name"
|
|
continue
|
|
fi
|
|
|
|
if check_needs_build "$pkg_name"; then
|
|
PACKAGES_TO_BUILD+=("$pkg_name")
|
|
else
|
|
echo " + $pkg_name - already up to date"
|
|
SKIPPED_PACKAGES="$SKIPPED_PACKAGES $pkg_name"
|
|
fi
|
|
done
|
|
else
|
|
# Build all packages that need updates from the relevant directories
|
|
while IFS= read -r pkg; do
|
|
if check_needs_build "$pkg"; then
|
|
PACKAGES_TO_BUILD+=("$pkg")
|
|
else
|
|
echo " + $pkg - already up to date"
|
|
SKIPPED_PACKAGES="$SKIPPED_PACKAGES $pkg"
|
|
fi
|
|
done < <(collect_packages)
|
|
fi
|
|
|
|
if [[ ${#PACKAGES_TO_BUILD[@]} -eq 0 ]]; then
|
|
echo "==> All packages are up to date!"
|
|
else
|
|
echo "==> ${#PACKAGES_TO_BUILD[@]} package(s) need building: ${PACKAGES_TO_BUILD[*]}"
|
|
echo "==> Determining build order based on dependencies..."
|
|
|
|
# Second pass: order only the packages that need building
|
|
# Strategy: build packages with no unmet dependencies first
|
|
declare -A unmet_deps_count # How many dependencies does this package still need?
|
|
declare -A blocks_packages # Which packages are waiting for this one?
|
|
|
|
# Count unmet dependencies for each package
|
|
for pkg in "${PACKAGES_TO_BUILD[@]}"; do
|
|
unmet_deps_count[$pkg]=0
|
|
done
|
|
|
|
# Build the dependency relationships
|
|
for pkg in "${PACKAGES_TO_BUILD[@]}"; do
|
|
while IFS= read -r dep; do
|
|
# Only care about deps that are being built in this run
|
|
for build_pkg in "${PACKAGES_TO_BUILD[@]}"; do
|
|
if [[ "$dep" == "$build_pkg" ]]; then
|
|
# pkg needs dep, so increment pkg's unmet count
|
|
((unmet_deps_count[$pkg]++))
|
|
# Track that dep blocks pkg from building
|
|
blocks_packages[$dep]="${blocks_packages[$dep]} $pkg"
|
|
PLANNED_DEPENDENCIES+=("$pkg $dep")
|
|
fi
|
|
done
|
|
done < <(get_package_deps "$pkg")
|
|
done
|
|
|
|
# Start with packages that have all dependencies met (count = 0)
|
|
ready_to_build=()
|
|
for pkg in "${PACKAGES_TO_BUILD[@]}"; do
|
|
if [[ ${unmet_deps_count[$pkg]} -eq 0 ]]; then
|
|
ready_to_build+=("$pkg")
|
|
fi
|
|
done
|
|
|
|
# Build packages as dependencies become available
|
|
while [[ ${#ready_to_build[@]} -gt 0 ]]; do
|
|
# Take the first ready package
|
|
current="${ready_to_build[0]}"
|
|
ready_to_build=("${ready_to_build[@]:1}")
|
|
ORDERED_PACKAGES+=("$current")
|
|
|
|
# This package is now built, so packages waiting for it can proceed
|
|
for blocked_pkg in ${blocks_packages[$current]}; do
|
|
((unmet_deps_count[$blocked_pkg]--))
|
|
if [[ ${unmet_deps_count[$blocked_pkg]} -eq 0 ]]; then
|
|
ready_to_build+=("$blocked_pkg")
|
|
fi
|
|
done
|
|
done
|
|
|
|
# Check for circular dependencies
|
|
if [[ ${#ORDERED_PACKAGES[@]} -ne ${#PACKAGES_TO_BUILD[@]} ]]; then
|
|
echo "ERROR: Circular dependency detected!"
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> Build order: ${ORDERED_PACKAGES[*]}"
|
|
fi
|
|
|
|
# The host consumes plain data, never shell code or parsed human log output.
|
|
if [[ -n "$BUILD_PLAN_DIR" ]]; then
|
|
mkdir -p "$BUILD_PLAN_DIR" || exit 1
|
|
printf '%s\n' "${ORDERED_PACKAGES[@]}" | sed '/^$/d' > "$BUILD_PLAN_DIR/packages" || exit 1
|
|
printf '%s\n' "${PLANNED_DEPENDENCIES[@]}" | sed '/^$/d' > "$BUILD_PLAN_DIR/dependencies" || exit 1
|
|
printf '%s\n' $SKIPPED_PACKAGES | sed '/^$/d' > "$BUILD_PLAN_DIR/skipped" || exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo "==> Plan complete. Packages that would build: ${ORDERED_PACKAGES[*]}"
|