diff --git a/README.md b/README.md index d63b0eb..5f1e891 100644 --- a/README.md +++ b/README.md @@ -228,9 +228,11 @@ setting is named for the repository rather than for building, which happens wherever you like. The same setting tells `bin/omarchy-pkgs release` which host to poke after a release push. -Split packages are selected by their own names, not their pkgbase — pushing -`nvidia-580xx-utils` does not carry `nvidia-580xx-dkms` along. Omit `--package` to -push everything built. +`--package` means the same thing as it does to `build`: a pkgbase, whose every +output ships together. Pushing `nvidia-580xx-utils` carries `nvidia-580xx-dkms` +and `opencl-nvidia-580xx` with it, because that is what the build produced. An +output's own name still selects just that one, for publishing a single package +on purpose. Omit `--package` to push everything built. Publishing signs and promotes everything staged on the host, not just what this push uploaded, so `push` stops when it finds packages already staged there — @@ -558,21 +560,27 @@ State files are stored in `/root/.state/`: ### Installation ```bash -# Copy systemd units -cp /root/omarchy-pkgs/systemd/*.service /root/omarchy-pkgs/systemd/*.timer /etc/systemd/system/ - -# Reload systemd -systemctl daemon-reload - -# Enable and start timers -systemctl enable --now omarchy-check-versions.timer -systemctl enable --now omarchy-auto-release-edge.timer -systemctl enable --now omarchy-auto-release-stable.timer - -# Create state directory -mkdir -p /root/.state +ssh root@ 'cd /root/omarchy-pkgs && bin/setup' ``` +`bin/setup` installs the dependencies, ensures Docker is running, creates the +state directory, and installs and enables the release timers. It works on +Debian/Ubuntu and on Arch, and is idempotent, so run it again whenever a +dependency is added. + +The host does not need to be Arch: makepkg, repo-add and package signing all +run inside containers, so it needs only Docker, rclone, bsdtar, jq, git and +rsync. Docker is left alone when it already works, rather than replacing a +working installation from Docker's own repository with the distribution's. + +```bash +bin/repo setup --check # Report what is missing, change nothing +bin/repo setup --skip-timers # Prepare the host without the release timers +``` + +Signing credentials (`/root/.omarchy/build-credentials`) and the rclone remote +hold secrets, so setup reports on them rather than creating them. + ### Management ```bash diff --git a/bin/push-build b/bin/push-build index 21523e8..bbb7d67 100755 --- a/bin/push-build +++ b/bin/push-build @@ -143,29 +143,41 @@ if [[ -z "$PACKAGES" && "$ASSUME_YES" == true ]] && ! on_repo_host; then exit 1 fi +# --package means the same thing here as it does to bin/build: a pkgbase, whose +# every output ships together. Selecting only the artifact whose filename matched +# would publish one third of a split package like nvidia-580xx-utils and silently +# leave nvidia-580xx-dkms and opencl-nvidia-580xx behind. An output's own name +# still matches, for pushing just one of them on purpose. +# +# pkgbase comes from .PKGINFO rather than the PKGBUILD: it is what makepkg +# actually recorded, and it needs no guessing about which directory built what. +pkgbase_of() { + bsdtar -xOf "$1" .PKGINFO 2>/dev/null | + awk -F ' = ' '$1 == "pkgbase" { print $2; exit }' +} + FILES=() if [[ -z "$PACKAGES" ]]; then FILES=("${ALL_FILES[@]}") else + declare -A MATCHED=() for file in "${ALL_FILES[@]}"; do # name-version-release-arch.pkg.tar.zst -> name pkgname="${file%-*-*-*.pkg.tar.*}" + pkgbase=$(pkgbase_of "$BUILD_OUTPUT_DIR/$file") for wanted in $PACKAGES; do - if [[ "$pkgname" == "$wanted" ]]; then + if [[ "$pkgname" == "$wanted" || "$pkgbase" == "$wanted" ]]; then FILES+=("$file") + MATCHED["$wanted"]=1 break fi done done for wanted in $PACKAGES; do - found=false - for file in "${FILES[@]}"; do - [[ "${file%-*-*-*.pkg.tar.*}" == "$wanted" ]] && found=true && break - done - if [[ "$found" != true ]]; then + if [[ -z "${MATCHED[$wanted]:-}" ]]; then print_error "No built artifact for '$wanted' in $BUILD_OUTPUT_DIR" - print_warning "Split packages are named after their outputs, not their pkgbase" + print_warning "Name a package or the pkgbase it was built from" exit 1 fi done diff --git a/bin/repo b/bin/repo index 0e2c40e..48c91cc 100755 --- a/bin/repo +++ b/bin/repo @@ -60,6 +60,7 @@ show_usage() { echo " sync Sync repository to remote" echo " push Upload local builds to the repository host and publish them there" echo " deploy Build locally, then push: one command from a build machine" + echo " setup Install everything the repository host needs" echo "" echo "Typical workflows:" echo " $0 release # Complete release workflow" @@ -135,6 +136,10 @@ deploy) "$SCRIPT_DIR/deploy" "$@" 2>&1 | tee "$LOG_FILE" exit ${PIPESTATUS[0]} ;; +setup) + "$SCRIPT_DIR/setup" "$@" 2>&1 | tee "$LOG_FILE" + exit ${PIPESTATUS[0]} + ;; -h | --help | help) show_usage ;; diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..7b1427c --- /dev/null +++ b/bin/setup @@ -0,0 +1,246 @@ +#!/bin/bash +# Prepare this machine to serve as the Omarchy repository host. +# +# The host receives uploads, promotes packages into the published tree, and +# syncs that tree to the mirror. Everything Arch-specific — makepkg, repo-add, +# package signing — happens inside containers, so the host itself needs very +# little and does not need to be Arch. The production host is Ubuntu. +# +# Run this on the host itself: +# ssh root@ 'cd /root/omarchy-pkgs && bin/setup' +# +# It is idempotent — run it again after adding a dependency. + +set -e + +BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") +source "$BUILD_ROOT/helpers/message-helpers.sh" + +CHECK_ONLY=false +SKIP_TIMERS=false + +STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}" +CREDENTIALS="/root/.omarchy/build-credentials" + +print_header "Omarchy Repository Host Setup" + +while [[ $# -gt 0 ]]; do + case $1 in + --check) + CHECK_ONLY=true + shift + ;; + --skip-timers) + SKIP_TIMERS=true + shift + ;; + -h | --help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Install and enable everything the repository host needs." + echo "Works on Debian/Ubuntu (apt) and Arch (pacman)." + echo "" + echo "Options:" + echo " --check Report what is missing, change nothing" + echo " --skip-timers Do not install or enable the release timers" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + print_error "Unknown option: $1" + exit 1 + ;; + esac +done + +# --- distribution ------------------------------------------------------------ + +# Package names differ where it matters: bsdtar is libarchive-tools on Debian +# and libarchive on Arch, and Docker is docker.io rather than docker. +if command -v apt-get >/dev/null 2>&1; then + DISTRO="debian" + PKG_BSDTAR="libarchive-tools" + PKG_DOCKER="docker.io" +elif command -v pacman >/dev/null 2>&1; then + DISTRO="arch" + PKG_BSDTAR="libarchive" + PKG_DOCKER="docker" +else + print_error "Unsupported distribution — need apt-get or pacman" + exit 1 +fi + +print_info "Distribution: $DISTRO" + +if [[ "$CHECK_ONLY" != true && $EUID -ne 0 ]]; then + print_error "Run as root (installing packages and systemd units)" + exit 1 +fi + +# Docker and the release timers are both systemd units. Say so plainly rather +# than failing later on a missing command — a container is the usual way to end +# up here, and it cannot be a repository host. +if [[ "$CHECK_ONLY" != true ]] && ! command -v systemctl >/dev/null 2>&1; then + print_error "systemctl not found — the repository host must run systemd" + echo "" + echo "Docker and the release timers are systemd units. This looks like a" + echo "container; run setup on the host itself." + exit 1 +fi + +install_packages() { + case "$DISTRO" in + debian) + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" + ;; + arch) + pacman -S --needed --noconfirm "$@" + ;; + esac +} + +# --- dependencies ------------------------------------------------------------ + +# Only what the host runs directly. Signing and repo-add happen in containers, +# so gnupg and the Arch build tools are deliberately absent from this list. +REQUIREMENTS=( + "bsdtar:$PKG_BSDTAR" # reads repo databases and .PKGINFO out of packages + "git:git" # pulls this repository + "jq:jq" # package metadata in .omarchy/package.json + "rsync:rsync" # receives uploads from bin/repo push + "rclone:rclone" # publishes to the mirror +) + +print_info "Checking dependencies..." +MISSING_PACKAGES=() +for requirement in "${REQUIREMENTS[@]}"; do + cmd="${requirement%%:*}" + pkg="${requirement#*:}" + if command -v "$cmd" >/dev/null 2>&1; then + print_step "$cmd" + else + print_warning "$cmd missing (provided by $pkg)" + MISSING_PACKAGES+=("$pkg") + fi +done +echo "" + +if [[ ${#MISSING_PACKAGES[@]} -gt 0 ]]; then + if [[ "$CHECK_ONLY" == true ]]; then + print_warning "Would install: ${MISSING_PACKAGES[*]}" + else + print_info "Installing: ${MISSING_PACKAGES[*]}" + install_packages "${MISSING_PACKAGES[@]}" + print_success "Dependencies installed" + fi +else + print_success "All dependencies present" +fi +echo "" + +# --- docker ------------------------------------------------------------------ + +# Docker is left alone when it already works. A host may well be running a +# version from Docker's own repository rather than the distribution's, and +# replacing that underneath a working builder would be a poor trade for +# tidiness. +print_info "Checking Docker..." + +if command -v docker >/dev/null 2>&1; then + print_step "docker present: $(docker --version 2>/dev/null | head -1)" + if docker info >/dev/null 2>&1; then + print_success "Docker is installed and running — leaving it alone" + elif [[ "$CHECK_ONLY" == true ]]; then + print_warning "Docker is installed but not running; would start it" + else + print_info "Docker is installed but not running — starting it" + systemctl enable --now docker.service + if docker info >/dev/null 2>&1; then + print_success "Docker started" + else + print_error "Docker is installed but still not responding" + echo " Check 'systemctl status docker' — builds cannot run without it." + exit 1 + fi + fi +elif [[ "$CHECK_ONLY" == true ]]; then + print_warning "Would install $PKG_DOCKER and enable it" +else + print_info "Installing $PKG_DOCKER..." + install_packages "$PKG_DOCKER" + systemctl enable --now docker.service + if docker info >/dev/null 2>&1; then + print_success "Docker installed and running" + else + print_error "Docker installed but not responding" + echo " Check 'systemctl status docker' — builds cannot run without it." + exit 1 + fi +fi +echo "" + +# --- state directory --------------------------------------------------------- + +if [[ -d "$STATE_DIR" ]]; then + print_success "State directory present: $STATE_DIR" +elif [[ "$CHECK_ONLY" == true ]]; then + print_warning "Would create $STATE_DIR" +else + mkdir -p "$STATE_DIR" + print_success "Created $STATE_DIR" +fi +echo "" + +# --- release timers ---------------------------------------------------------- + +TIMERS=(omarchy-check-versions omarchy-auto-release-edge omarchy-auto-release-stable) + +if [[ "$SKIP_TIMERS" == true ]]; then + print_info "Skipping release timers (--skip-timers)" +elif [[ "$CHECK_ONLY" == true ]]; then + for timer in "${TIMERS[@]}"; do + if systemctl is-enabled "$timer.timer" >/dev/null 2>&1; then + print_success "$timer.timer is enabled" + else + print_warning "$timer.timer would be enabled" + fi + done +else + print_info "Installing release timers..." + cp "$BUILD_ROOT"/systemd/*.service "$BUILD_ROOT"/systemd/*.timer /etc/systemd/system/ + systemctl daemon-reload + for timer in "${TIMERS[@]}"; do + systemctl enable --now "$timer.timer" + print_step "$timer.timer" + done + print_success "Release timers enabled" +fi +echo "" + +# --- credentials ------------------------------------------------------------- + +# These hold secrets, so setup reports on them rather than creating them. +print_info "Checking credentials..." + +if [[ -f "$CREDENTIALS" ]]; then + print_success "Signing credentials present: $CREDENTIALS" +else + print_warning "Missing $CREDENTIALS" + echo " Must export GPG_PRIVATE_KEY and GPG_PASSPHRASE; the release" + echo " services source it before signing." +fi + +if rclone listremotes 2>/dev/null | grep -q '^pkgs.omarchy.org:'; then + print_success "rclone remote 'pkgs.omarchy.org' configured" +else + print_warning "rclone remote 'pkgs.omarchy.org' not configured" + echo " bin/repo sync publishes there; configure it with 'rclone config'." +fi +echo "" + +if [[ "$CHECK_ONLY" == true ]]; then + print_info "Check complete — nothing was changed" +else + print_success "Repository host ready" +fi diff --git a/bin/sync-repo b/bin/sync-repo index fc8394f..52fa72f 100755 --- a/bin/sync-repo +++ b/bin/sync-repo @@ -141,6 +141,17 @@ if grep -qx 'omarchy\.db' <<<"$REMOTE_LISTING"; then REMOTE_DB_FILE=$(mktemp) trap 'rm -f "$REMOTE_DB_FILE"' EXIT rclone cat "$REMOTE/$DESTINATION_DIRECTORY/omarchy.db" --s3-no-head >"$REMOTE_DB_FILE" 2>/dev/null + + if ! command -v bsdtar >/dev/null 2>&1; then + print_error "bsdtar is not installed" + echo "" + echo "The repository host needs it to read the repository database." + echo "Run bin/setup to install everything this host requires." + exit 1 + fi + + # bsdtar, not tar: repo-add has used both gzip and zstd for the database, and + # libarchive detects either without being told which. REMOTE_NAMES=$(bsdtar -tf "$REMOTE_DB_FILE" 2>/dev/null | sed 's|/.*||' | sed -E 's/-[^-]+-[^-]+$//' | sort -u) @@ -148,7 +159,8 @@ if grep -qx 'omarchy\.db' <<<"$REMOTE_LISTING"; then print_error "The remote database exists but could not be read" echo "" echo "Refusing to sync rather than assume the remote is empty. Check that" - echo "bsdtar is installed and that omarchy.db is not corrupt." + echo "omarchy.db is not corrupt:" + echo " rclone cat $REMOTE/$DESTINATION_DIRECTORY/omarchy.db | bsdtar -tf -" exit 1 fi