#!/bin/bash # 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} 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} source "$HELPERS_DIR/package-metadata.sh" if [[ "$DRY_RUN" != true ]]; then # Import GPG keys /build/import-gpg-keys.sh || exit 1 mkdir -p "$BUILD_OUTPUT_DIR" "$FINAL_OUTPUT_DIR" # Configure Omarchy repositories for dependency resolution echo "==> 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" # 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 # 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 < omarchy (priority 2): $FINAL_OUTPUT_DIR" fi # Sync pacman database sudo pacman -Sy 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 FAILED_PACKAGES="" SUCCESSFUL_PACKAGES="" SKIPPED_PACKAGES="" # Find package directory find_package_dir() { local pkg="$1" package_dir_for_name "$pkg" } # Get version from final output (production packages) get_local_version() { local pkg="$1" if [[ -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" ]]; then local desc_file=$(tar -tf "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" | grep "^${pkg}-[0-9r].*/desc$" | head -1) if [[ -n "$desc_file" ]]; then tar -xOf "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" "$desc_file" 2>/dev/null | awk '/%VERSION%/{getline; print; exit}' fi 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 current_arch="$ARCH" local pkgdir=$(find_package_dir "$pkg") local pkgbuild="$pkgdir/PKGBUILD" [[ ! -f "$pkgbuild" ]] && return 1 # Check PKGBUILD arch=() array local pkgbuild_archs=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${arch[@]}"') # If arch=('any'), build for all architectures if [[ "$pkgbuild_archs" == "any" ]]; then return 0 fi # Check if current arch is in PKGBUILD arch=() if echo "$pkgbuild_archs" | grep -qw "$current_arch"; then return 0 # Build else return 1 # Skip fi } # Build a package build_package() { local pkg="$1" local pkgdir=$(find_package_dir "$pkg") echo "" echo " -> Processing: $pkg" # Copy to build directory cd /src rm -rf "$pkg" cp -r "$pkgdir" "$pkg" cd "/src/$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" FAILED_PACKAGES="$FAILED_PACKAGES $pkg" 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 MAKEPKG_FLAGS="-scf --noconfirm" if PACMAN=/usr/local/bin/pacman-for-makepkg makepkg $MAKEPKG_FLAGS; then # Ensure output directory exists mkdir -p "$BUILD_OUTPUT_DIR" for pkg_file in *.pkg.tar.*; do [[ -f "$pkg_file" ]] && cp "$pkg_file" "$BUILD_OUTPUT_DIR/" done cd "$BUILD_OUTPUT_DIR" # Find ALL package files (handles split packages) local new_pkgs=($(ls -t ${pkg}-*.pkg.tar.* 2>/dev/null | grep -v '\.sig$' | grep -v 'omarchy-build\.db')) if [[ ${#new_pkgs[@]} -gt 0 ]]; then repo-add omarchy-build.db.tar.zst "${new_pkgs[@]}" >/dev/null 2>&1 ln -sf omarchy-build.db.tar.zst omarchy-build.db sudo pacman -Sy >/dev/null 2>&1 fi cd /src/$pkg echo " Successfully built $pkg" SUCCESSFUL_PACKAGES="$SUCCESSFUL_PACKAGES $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" FAILED_PACKAGES="$FAILED_PACKAGES $pkg" 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 # Extract depends and makedepends, filter for packages in our pkgbuilds/ ( source "$pkgbuild" 2>/dev/null echo "${depends[@]} ${makedepends[@]}" ) | 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 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 `git describe`. 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 HEAD commit hash to the g suffix already in the production # version. 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 # Find the first git+ source URL (skip non-git sources like patch files). # Bail out on any #fragment (commit/tag/branch pinning) — HEAD comparison # wouldn't be meaningful there. local source_url=$(cd "$pkgdir" && bash -c ' source PKGBUILD 2>/dev/null for s in "${source[@]}"; do url="${s#*::}" [[ "$url" == git+* ]] && { echo "${url#git+}"; break; } done') [[ -z "$source_url" ]] && return 1 [[ "$source_url" == *"#"* ]] && return 1 local prod_hash=$(echo "$local_version" | grep -oE '\.g[a-f0-9]{7,}' | tail -1) prod_hash="${prod_hash#.g}" prod_hash="${prod_hash:0:7}" [[ -z "$prod_hash" ]] && return 1 local upstream_hash=$(git ls-remote "$source_url" HEAD 2>/dev/null | awk 'NR==1 {print substr($1, 1, 7)}') [[ -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 if check_vcs_unchanged "$pkg" "$pkgdir"; then return 1 fi # 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 [[ "$local_version" == "$pkgbuild_version" ]]; then return 1 # Already up to date else return 0 # Needs building fi } # Collect packages that should be built for the selected mirror collect_packages() { packages_for_mirror "$MIRROR" } # Main execution if [[ "$DRY_RUN" != true ]]; then cd "$SRC_DIR" fi TOTAL_COUNT=0 echo "==> Checking which packages need building..." # First pass: determine which packages need building PACKAGES_TO_BUILD=() # 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 # Check if package should be built for this architecture if ! should_build_for_arch "$pkg"; then echo " - $pkg - not built for $ARCH" SKIPPED_PACKAGES="$SKIPPED_PACKAGES $pkg" continue fi 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" 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 ORDERED_PACKAGES=() 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[@]}" 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 while IFS= read -r dep; do [[ -z "$dep" ]] && continue # Only install if it's being built in this run for build_pkg in "${ORDERED_PACKAGES[@]}"; do [[ "$dep" == "$build_pkg" ]] && INSTALL_PACKAGES["$dep"]=1 done done < <(get_package_deps "$pkg") done if [[ ${#INSTALL_PACKAGES[@]} -gt 0 ]]; then echo "==> Packages needed as dependencies: ${!INSTALL_PACKAGES[@]}" fi # Build packages in dependency order for pkg in "${ORDERED_PACKAGES[@]}"; do ((TOTAL_COUNT++)) build_package "$pkg" done fi echo "" echo "========================================" echo "==> Build Summary" echo "========================================" # Count results SUCCESS_COUNT=$(echo $SUCCESSFUL_PACKAGES | wc -w) SKIPPED_COUNT=$(echo $SKIPPED_PACKAGES | wc -w) FAILED_COUNT=$(echo $FAILED_PACKAGES | wc -w) echo " Total packages: $TOTAL_COUNT" echo " Built: $SUCCESS_COUNT" echo " Skipped: $SKIPPED_COUNT (already up-to-date)" echo " Failed: $FAILED_COUNT" # List failures if any if [[ -n "$FAILED_PACKAGES" ]]; then echo "" echo "Failed packages:" for pkg in $FAILED_PACKAGES; do echo " - $pkg" done echo "" echo "==> Some packages failed to build" exit 1 fi echo "" echo "==> All packages processed successfully!"