#!/usr/bin/env bash set -euo pipefail repoUrl="https://github.com/X11Libre/pkgbuilds-arch-based.git" rootfsUrl="http://os.archlinuxarm.org/os/ArchLinuxARM-aarch64-latest.tar.gz" aarch64Packages=( xlibre-xserver xlibre-input-libinput xlibre-video-fbdev xlibre-video-amdgpu xlibre-video-ati xlibre-video-nouveau ) outputDir=$(pwd -P) workDir="" chrootDir="" binfmtMounted=false binfmtRegistered=false log() { printf '%s %s\n' "$*" "$(date '+%Y-%m-%d %H:%M:%S')" } die() { printf 'Error: %s\n' "$*" >&2 exit 1 } cleanup() { local exitStatus=$? local cleanupFailed=false local mountTarget local chrootMounts=() trap - EXIT INT TERM if [[ -n "$workDir" && -d "$workDir" ]]; then log "Removing temporary chroot" if [[ -n "$chrootDir" ]]; then mapfile -t chrootMounts < <( findmnt -rn -o TARGET | awk -v root="$chrootDir" '$0 == root || index($0, root "/") == 1' | sort -r ) fi for mountTarget in "${chrootMounts[@]}"; do if ! mountpoint -q "$mountTarget"; then continue fi if ! umount --recursive "$mountTarget"; then printf 'Warning: normal chroot unmount failed; detaching it lazily\n' >&2 umount --recursive --lazy "$mountTarget" || cleanupFailed=true fi done rm -rf --one-file-system "$workDir" || cleanupFailed=true if [[ -e "$workDir" ]]; then printf 'Error: failed to remove temporary chroot: %s\n' "$workDir" >&2 cleanupFailed=true fi fi if [[ "$binfmtRegistered" == true && -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]]; then printf '%s' -1 > /proc/sys/fs/binfmt_misc/qemu-aarch64 || cleanupFailed=true fi if [[ "$binfmtMounted" == true ]]; then umount /proc/sys/fs/binfmt_misc || cleanupFailed=true fi if [[ "$cleanupFailed" == true && "$exitStatus" -eq 0 ]]; then exitStatus=1 fi exit "$exitStatus" } require_commands() { local commandName local missingCommands=() for commandName in arch-chroot awk bsdtar chown cp curl date findmnt grep install mkdir mktemp mount mountpoint rm sort tr umount; do if ! command -v "$commandName" >/dev/null 2>&1; then missingCommands+=("$commandName") fi done if ((${#missingCommands[@]})); then die "Missing required commands: ${missingCommands[*]}" fi [[ -x /usr/bin/qemu-aarch64-static ]] || die "/usr/bin/qemu-aarch64-static is required" [[ -r /usr/lib/binfmt.d/qemu-aarch64-static.conf ]] || die "The qemu-user-static-binfmt package is required" } configure_binfmt() { if ! mountpoint -q /proc/sys/fs/binfmt_misc; then mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc binfmtMounted=true fi if [[ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]]; then tr -d '\n' < /usr/lib/binfmt.d/qemu-aarch64-static.conf > /proc/sys/fs/binfmt_misc/register binfmtRegistered=true fi grep -qx 'enabled' /proc/sys/fs/binfmt_misc/qemu-aarch64 || die "aarch64 binfmt registration is not enabled" } create_build_script() { local buildScript=$1 install -m 0755 /dev/stdin "$buildScript" <<'CHROOT_SCRIPT' #!/usr/bin/env bash set -euo pipefail repoUrl=$1 shift packages=("$@") repoDir=/build/pkgbuilds log() { printf '%s %s\n' "$*" "$(date '+%Y-%m-%d %H:%M:%S')" } install_dependencies() { local packageDir=$1 local dependency local outputPackage local srcInfo local dependencies=() local filteredDependencies=() local missingDependencies=() local outputPackages=() local -A internalPackages=() # $1 is expanded by the nested builder shell, not this root shell. # shellcheck disable=SC2016 srcInfo=$(runuser -u builder -- bash -c \ 'cd "$1" && makepkg --printsrcinfo' _ "$packageDir") mapfile -t outputPackages < <( printf '%s\n' "$srcInfo" | awk -F ' = ' '/^[[:space:]]*pkgname = / { print $2 }' ) for outputPackage in "${outputPackages[@]}"; do internalPackages["$outputPackage"]=1 done mapfile -t dependencies < <( printf '%s\n' "$srcInfo" | awk -F ' = ' ' /^[[:space:]]*(depends|makedepends|checkdepends)(_aarch64)? = / { dependency=$2 sub(/[<>=].*/, "", dependency) print dependency } ' | sort -u ) for dependency in "${dependencies[@]}"; do if [[ -z ${internalPackages[$dependency]+present} ]]; then filteredDependencies+=("$dependency") fi done ((${#filteredDependencies[@]})) || return 0 mapfile -t missingDependencies < <(pacman -T "${filteredDependencies[@]}" || true) ((${#missingDependencies[@]})) || return 0 log "Installing dependencies for ${packageDir##*/}" pacman -S --asdeps --needed --noconfirm "${missingDependencies[@]}" } remove_installed_packages() { local packageName local installedPackages=() for packageName in "$@"; do if pacman -Q "$packageName" >/dev/null 2>&1; then installedPackages+=("$packageName") fi done ((${#installedPackages[@]})) || return 0 log "Removing conflicting packages: ${installedPackages[*]}" pacman -Rdd --noconfirm "${installedPackages[@]}" } build_package() { local packageName=$1 local installAfterBuild=${2:-false} local packageDir="$repoDir/$packageName" local builtPackages=() [[ -f "$packageDir/PKGBUILD" ]] || { printf 'Missing PKGBUILD for %s\n' "$packageName" >&2 return 1 } log "Building $packageName" install_dependencies "$packageDir" # $1 is expanded by the nested builder shell, not this root shell. # shellcheck disable=SC2016 runuser -u builder -- bash -c \ 'cd "$1" && makepkg --noconfirm --clean --cleanbuild' _ "$packageDir" mapfile -d '' builtPackages < <( find "$packageDir" -maxdepth 1 -type f -name '*.pkg.tar.*' ! -name '*.sig' -print0 ) ((${#builtPackages[@]})) || { printf 'No package artifacts were produced for %s\n' "$packageName" >&2 return 1 } if [[ "$installAfterBuild" == true ]]; then case "$packageName" in xlibre-input-libinput) remove_installed_packages xf86-input-libinput ;; xlibre-xserver) remove_installed_packages \ xorg-server \ xorg-server-common \ xorg-server-devel \ xorg-server-xephyr \ xorg-server-xnest \ xorg-server-xvfb \ glamor-egl \ xf86-video-modesetting ;; esac log "Installing $packageName for subsequent builds" pacman -U --noconfirm "${builtPackages[@]}" fi } log "Initializing Arch Linux ARM" # Pacman's Landlock sandbox is unavailable through qemu-user emulation. sed -i \ -e '/^[[:space:]]*DisableSandbox[[:space:]]*$/d' \ -e '/^\[options\][[:space:]]*$/a DisableSandbox' \ /etc/pacman.conf pacman-key --init pacman-key --populate archlinuxarm pacman -Syu --noconfirm pacman -S --needed --noconfirm base-devel git useradd --create-home --shell /bin/bash builder install -d -o builder -g builder /build log "Cloning XLibre PKGBUILDs" runuser -u builder -- git clone --depth 1 "$repoUrl" "$repoDir" # The input driver must first be built against the stock xorg-server-devel. build_package xlibre-input-libinput true # Installing all server split packages replaces the stock Xorg server/devel # packages and provides the ABI dependencies needed by the video drivers. build_package xlibre-xserver true for packageName in "${packages[@]}"; do case "$packageName" in xlibre-input-libinput|xlibre-xserver) continue ;; esac build_package "$packageName" done install -d /output find "$repoDir" -type f -name '*.pkg.tar.*' ! -name '*.sig' -exec cp -t /output -- {} + log "All requested aarch64 packages built" CHROOT_SCRIPT } main() { local rootfsArchive local packageFile local copiedPackageCount=0 ((EUID == 0)) || die "Run this script as root: sudo $0" require_commands trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM configure_binfmt workDir=$(mktemp -d /tmp/xlibre-aarch64.XXXXXX) chrootDir="$workDir/root" rootfsArchive="$workDir/ArchLinuxARM-aarch64-latest.tar.gz" mkdir -p "$chrootDir" log "Downloading a fresh Arch Linux ARM aarch64 root filesystem" curl --fail --location --retry 3 --output "$rootfsArchive" "$rootfsUrl" log "Creating aarch64 chroot" bsdtar -xpf "$rootfsArchive" -C "$chrootDir" install -m 0755 /usr/bin/qemu-aarch64-static "$chrootDir/usr/bin/qemu-aarch64-static" cp --remove-destination /etc/resolv.conf "$chrootDir/etc/resolv.conf" create_build_script "$chrootDir/root/build-xlibre" # arch-chroot expects the chroot root to be a mountpoint. mount --bind "$chrootDir" "$chrootDir" log "Starting aarch64 package build" arch-chroot "$chrootDir" /root/build-xlibre "$repoUrl" "${aarch64Packages[@]}" shopt -s nullglob for packageFile in "$chrootDir"/output/*.pkg.tar.*; do [[ ${packageFile##*/} == *-aarch64.pkg.tar.* ]] || die "Unexpected non-aarch64 artifact: ${packageFile##*/}" install -m 0644 "$packageFile" "$outputDir/" ((copiedPackageCount += 1)) if [[ -n ${SUDO_UID:-} && -n ${SUDO_GID:-} ]]; then chown "$SUDO_UID:$SUDO_GID" "$outputDir/${packageFile##*/}" fi done shopt -u nullglob ((copiedPackageCount > 0)) || die "No package artifacts were copied to $outputDir" log "Copied $copiedPackageCount completed packages to $outputDir" } main "$@"