diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9fc270f..3d0a68e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,20 @@ on: workflow_dispatch: jobs: + build-isolation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Prepare fixture builder + run: docker build -t omarchy-build-isolation-test -f tests/build-isolation.Dockerfile tests + - name: Verify isolated builds with real pacman transactions + env: + CONTAINER_ENGINE: docker + TEST_BUILDER_IMAGE: omarchy-build-isolation-test + run: tests/build-isolation.sh + self-tests: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 3f0c128..7873a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ src/ logs/ build-output/ +cache/ pkgs.omarchy.org/ pkgbuilds/*/.SRCINFO pkgbuilds/*/.gitignore diff --git a/README.md b/README.md index d57fcb1..64a5d58 100644 --- a/README.md +++ b/README.md @@ -858,12 +858,34 @@ bin/repo sync --arch aarch64 The build system automatically handles inter-package dependencies: -1. Parses `depends=()` and `makedepends=()` from PKGBUILDs -2. Builds in correct order -3. Makes newly-built packages available via temporary `[omarchy-build]` repo +1. Plans dependency order once, including `depends`, `makedepends`, + `checkdepends`, and their architecture-specific arrays. +2. Builds each package in a fresh container. Installed packages and changes + to the container's system files cannot carry over to the next build. +3. Shares successful artifacts through the temporary `[omarchy-build]` repo, + installing newly built prerequisites in each consumer's container. +4. Blocks consumers of a failed prerequisite while continuing independent + builds. Any failure still prevents the release from publishing. Example: If `aether` depends on `hyprshade`, `hyprshade` is built first. +Isolation also lets the release and dev Omarchy pairs build in the same run: +Flea can install `omarchy` without preventing `omarchy-dev` from installing +its conflicting settings package in a different container. + +Pacman downloads are cached under `cache/pacman///` across +containers and runs. The installed package database is never shared. Each +container updates its base system before resolving build dependencies, so a +cached builder image cannot cause a partial system upgrade. The existing +`OMARCHY_KEEP_BUILD_WORKSPACE`, `OMARCHY_SKIP_BUILDER_IMAGE`, and +`OMARCHY_DEFER_RUNTIME_DEPS` flags retain their behavior. + +`tests/build-isolation.sh` exercises conflicting package pairs, failed +prerequisites, resumed builds, cache replacement, and deferred dependencies +using real containers and pacman transactions. It uses the prepared builder +image, or an image named by `TEST_BUILDER_IMAGE`; CI builds the small fixture +image in `tests/build-isolation.Dockerfile`. + ## Version Management Packages are only rebuilt if: diff --git a/bin/build b/bin/build index c2d001c..0b16b59 100755 --- a/bin/build +++ b/bin/build @@ -226,17 +226,24 @@ else build_docker_image "$BUILD_DIR" "$ARCH" "$MIRROR" fi -print_info "Running package build..." +print_info "Planning isolated package builds..." # Create output directories if they don't exist mkdir -p "$BUILD_OUTPUT_DIR" mkdir -p "$REPO_DIR" +# Share downloaded archives, never /var/lib/pacman or an installed root. +# Channel/architecture separation preserves each mirror's dependency set. +PACKAGE_CACHE_DIR="$BUILD_ROOT/cache/pacman/$MIRROR/$ARCH" +mkdir -p "$PACKAGE_CACHE_DIR" +PLAN_DIR=$(mktemp -d "$SRC_DIR/build-plan.XXXXXX") +trap 'rm -rf "$PLAN_DIR"' EXIT + # Rootful Docker writes as the image uid, so retain its existing permission # workaround. Rootless Podman uses keep-id and must leave ownership/modes alone. if [[ "$CONTAINER_ENGINE" == "docker" ]]; then make_dir_writable "$BUILD_OUTPUT_DIR" - make_dir_writable "$REPO_DIR" + make_dir_writable "$PLAN_DIR" fi # Build Docker arguments @@ -247,8 +254,11 @@ DOCKER_ARGS=( -e PACKAGES="$PACKAGES" -e OMARCHY_RC_PINS="${OMARCHY_RC_PINS:-}" -e DEFER_RUNTIME_DEPS="$DEFER_RUNTIME_DEPS" + -e BUILD_PLAN_DIR=/build-plan + -v "$PLAN_DIR:/build-plan" + -v "$PACKAGE_CACHE_DIR:/var/cache/pacman/pkg" -v "$BUILD_ROOT/build-output:/build-output" - -v "$REPO_ROOT:/pkgs.omarchy.org" + -v "$REPO_ROOT:/pkgs.omarchy.org:ro" -v "$BUILD_DIR:/build:ro" -v "$BUILD_ROOT/helpers:/helpers:ro" -v "$BUILD_ROOT/pkgbuilds:/pkgbuilds:ro" @@ -260,18 +270,67 @@ if [[ "$CONTAINER_ENGINE" == "podman" ]]; then DOCKER_ARGS+=(-v "$SRC_DIR:/src") fi -# Run the builder with assembled args +# Plan once against the published database, then keep that order throughout +# the run. Each package sees the staged artifacts but starts with a fresh +# pacman database and root filesystem, even after a failed build. PLATFORM_ARG=$(get_platform_arg "$ARCH") -"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" "$PLATFORM_ARG" "${DOCKER_ARGS[@]}" "$IMAGE_TAG" /build/build.sh +"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" "$PLATFORM_ARG" "${DOCKER_ARGS[@]}" \ + -e DRY_RUN=true "$IMAGE_TAG" /build/build.sh -BUILD_RESULT=$? +mapfile -t ORDERED_PACKAGES < "$PLAN_DIR/packages" +mapfile -t SKIPPED_PACKAGES < "$PLAN_DIR/skipped" +SUCCESSFUL_PACKAGES=() +FAILED_PACKAGES=() +BLOCKED_PACKAGES=() +declare -A BUILD_STATUS=() + +for package in "${ORDERED_PACKAGES[@]}"; do + blocked_by="" + while read -r consumer dependency; do + if [[ "$consumer" == "$package" && "${BUILD_STATUS[$dependency]:-}" != success ]]; then + blocked_by="$dependency" + break + fi + done < "$PLAN_DIR/dependencies" + + if [[ -n "$blocked_by" ]]; then + print_warning "$package blocked by unsuccessful dependency: $blocked_by" + BUILD_STATUS[$package]=blocked + BLOCKED_PACKAGES+=("$package") + continue + fi + + print_info "Building $package in a fresh container..." + if "$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" "$PLATFORM_ARG" "${DOCKER_ARGS[@]}" \ + -e BUILD_PACKAGE="$package" "$IMAGE_TAG" /build/build.sh; then + BUILD_STATUS[$package]=success + SUCCESSFUL_PACKAGES+=("$package") + else + BUILD_STATUS[$package]=failed + FAILED_PACKAGES+=("$package") + fi +done -# Summary echo "" -if [[ $BUILD_RESULT -eq 0 ]]; then - print_success "Build completed successfully!" -else +print_header "Build Summary" +echo " Total packages: ${#ORDERED_PACKAGES[@]}" +echo " Built: ${#SUCCESSFUL_PACKAGES[@]}" +echo " Skipped: ${#SKIPPED_PACKAGES[@]} (up-to-date or excluded)" +echo " Failed: ${#FAILED_PACKAGES[@]}" +echo " Blocked: ${#BLOCKED_PACKAGES[@]}" + +if (( ${#FAILED_PACKAGES[@]} + ${#BLOCKED_PACKAGES[@]} )); then + if (( ${#FAILED_PACKAGES[@]} )); then + echo "Failed packages:" + printf ' - %s\n' "${FAILED_PACKAGES[@]}" + fi + if (( ${#BLOCKED_PACKAGES[@]} )); then + echo "Packages blocked by failed dependencies:" + printf ' - %s\n' "${BLOCKED_PACKAGES[@]}" + fi print_warning "Some packages failed (see details above)" - exit $BUILD_RESULT + exit 1 fi + +print_success "Build completed successfully!" diff --git a/build/build.sh b/build/build.sh index 9be2716..bcf02a4 100755 --- a/build/build.sh +++ b/build/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Build script - builds packages based on package metadata +# 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. @@ -11,6 +11,10 @@ ARCH=${ARCH:-x86_64} 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} @@ -48,6 +52,14 @@ if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then 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 @@ -59,7 +71,7 @@ if [[ "$DRY_RUN" != true ]]; then # 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 + sudo pacman -Syu --noconfirm || exit 1 # Configure Omarchy repositories for dependency resolution echo "==> Configuring Omarchy repositories for dependency resolution..." @@ -72,22 +84,39 @@ if [[ "$DRY_RUN" != true ]]; then echo " -> omarchy-build (priority 1): $BUILD_OUTPUT_DIR" # Initialize empty build database if it doesn't exist - cd "$BUILD_OUTPUT_DIR" + 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 - ln -sf omarchy-build.db.tar.zst omarchy-build.db + 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. - 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 + 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) if [[ -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" ]] || [[ -f "$FINAL_OUTPUT_DIR/omarchy.db" ]]; then @@ -96,7 +125,7 @@ if [[ "$DRY_RUN" != true ]]; then fi # Sync pacman database - sudo pacman -Sy + sudo pacman -Sy || exit 1 fi echo "==> Package Builder" @@ -109,8 +138,6 @@ 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 @@ -262,19 +289,34 @@ install_deferred_build_dependencies() { # Build a package build_package() { local pkg="$1" - local pkgdir=$(find_package_dir "$pkg") + 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 - rm -rf "$pkg" - cp -r "$pkgdir" "$pkg" + 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" || { - FAILED_PACKAGES="$FAILED_PACKAGES $pkg" return 1 } @@ -283,7 +325,6 @@ build_package() { if [[ -z "$pkgbuild_version" ]]; then echo " Failed to read PKGBUILD version" - FAILED_PACKAGES="$FAILED_PACKAGES $pkg" return 1 fi @@ -322,7 +363,6 @@ build_package() { # is installed in one verified transaction downstream. Only the # build-time dependencies are installed, then makepkg skips the check. install_deferred_build_dependencies "$pkg" || { - FAILED_PACKAGES="$FAILED_PACKAGES $pkg" return 1 } makepkg_flags=(-cf --noconfirm --nodeps) @@ -340,11 +380,9 @@ build_package() { if [[ ${#package_files[@]} -eq 0 ]]; then echo " Makepkg produced no package files for $pkg" - FAILED_PACKAGES="$FAILED_PACKAGES $pkg" return 1 fi - local dependency_pkg_file="" local -a new_pkgs=() local pkg_path pkg_file for pkg_path in "${package_files[@]}"; do @@ -358,55 +396,27 @@ build_package() { fi echo " Expected package file was not produced: $pkg_file" - FAILED_PACKAGES="$FAILED_PACKAGES $pkg" return 1 fi - cp "$pkg_file" "$BUILD_OUTPUT_DIR/" + cp "$pkg_file" "$BUILD_OUTPUT_DIR/" || return 1 new_pkgs+=("$pkg_file") - - if [[ "$(bsdtar -xOf "$pkg_file" .PKGINFO 2>/dev/null | sed -n 's/^pkgname = //p')" == "$pkg" ]]; then - dependency_pkg_file="$BUILD_OUTPUT_DIR/$pkg_file" - fi done - cd "$BUILD_OUTPUT_DIR" + 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 - ln -sf omarchy-build.db.tar.zst omarchy-build.db - sudo pacman -Sy >/dev/null 2>&1 - fi - - cd /src/$pkg - - # A lower-priority official repository may contain an older package with - # the same name. Install the exact artifact we just built before building - # its consumers, so pacman cannot select that older provider instead. - if [[ "${INSTALL_PACKAGES[$pkg]:-}" == "1" ]]; then - if [[ -z "$dependency_pkg_file" ]]; then - echo " Could not find the built $pkg package to install as a dependency" - FAILED_PACKAGES="$FAILED_PACKAGES $pkg" - return 1 - fi - - echo " Installing freshly built $pkg for dependent packages..." - if ! sudo /usr/local/bin/pacman-for-makepkg -U --needed --noconfirm "$dependency_pkg_file"; then - echo " Failed to install freshly built dependency $pkg" - FAILED_PACKAGES="$FAILED_PACKAGES $pkg" - return 1 - fi + 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 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 } @@ -421,11 +431,17 @@ get_package_deps() { return fi - # Extract depends and makedepends, filter for packages in our pkgbuilds/ + # 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 - echo "${depends[@]} ${makedepends[@]}" - ) | tr ' ' '\n' | while read -r dep; do + 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 @@ -522,15 +538,17 @@ collect_packages() { # Main execution if [[ "$DRY_RUN" != true ]]; then - cd "$SRC_DIR" + cd "$SRC_DIR" || exit 1 + build_package "$BUILD_PACKAGE" + exit $? fi -TOTAL_COUNT=0 - 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 @@ -581,7 +599,7 @@ 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 "==> ${#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 @@ -604,6 +622,7 @@ else ((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") @@ -618,7 +637,6 @@ else 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]}" @@ -640,63 +658,16 @@ else exit 1 fi - echo "==> Build order: ${ORDERED_PACKAGES[@]}" + echo "==> Build order: ${ORDERED_PACKAGES[*]}" +fi - 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 +# 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 "========================================" -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!" +echo "==> Plan complete. Packages that would build: ${ORDERED_PACKAGES[*]}" diff --git a/tests/build-isolation.Dockerfile b/tests/build-isolation.Dockerfile new file mode 100644 index 0000000..ddfd0a2 --- /dev/null +++ b/tests/build-isolation.Dockerfile @@ -0,0 +1,11 @@ +# Only the build runner is under test; source-free fixtures need no Omarchy +# bootstrap, signing key, mirror, or production repository. +FROM archlinux:base-devel +RUN pacman -Syu --noconfirm git jq sudo && \ + useradd -m -u 1000 builder && \ + echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder && \ + printf '#!/bin/bash\nexec /usr/bin/pacman --ask 4 "$@"\n' > /usr/local/bin/pacman-for-makepkg && \ + chmod +x /usr/local/bin/pacman-for-makepkg && \ + mkdir /src && chown builder:builder /src +USER builder +WORKDIR /src diff --git a/tests/build-isolation.sh b/tests/build-isolation.sh new file mode 100755 index 0000000..27c9df5 --- /dev/null +++ b/tests/build-isolation.sh @@ -0,0 +1,152 @@ +#!/bin/bash +# Integration regression: uses real containers, makepkg, repo-add and pacman. +# Requires the normal builder image (or TEST_BUILDER_IMAGE) for this platform. +set -euo pipefail + +BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") +source "$BUILD_ROOT/helpers/message-helpers.sh" +source "$BUILD_ROOT/helpers/docker-helpers.sh" +check_engine +TEST_ARCH=$(docker_native_arch) +TEST_BUILDER_IMAGE=${TEST_BUILDER_IMAGE:-omarchy-pkg-builder:latest-$TEST_ARCH-edge} +TEST_ROOT=$(mktemp -d) +trap 'rm -rf "$TEST_ROOT"' EXIT +mkdir -p "$TEST_ROOT/bin" "$TEST_ROOT/pkgbuilds" "$TEST_ROOT/build-output/edge/$TEST_ARCH" +cp "$BUILD_ROOT/bin/build" "$TEST_ROOT/bin/" +cp -r "$BUILD_ROOT/build" "$BUILD_ROOT/helpers" "$TEST_ROOT/" +# No external verification keys are needed for source-free fixtures. +: > "$TEST_ROOT/build/gpg-keys.txt" + +# The runner expects the normal image tag. Use an engine wrapper to select +# the supplied test image without overwriting an operator's builder image. +TEST_ENGINE=$(command -v "$CONTAINER_ENGINE") +mkdir "$TEST_ROOT/engine" +cat > "$TEST_ROOT/engine/$CONTAINER_ENGINE" <<'ENGINE' +#!/bin/bash +args=("$@") +for index in "${!args[@]}"; do + if [[ "${args[$index]}" == omarchy-pkg-builder:latest-* ]]; then + args[$index]="$TEST_BUILDER_IMAGE" + fi +done +exec "$TEST_ENGINE" "${args[@]}" +ENGINE +chmod +x "$TEST_ROOT/engine/$CONTAINER_ENGINE" +export TEST_ENGINE TEST_BUILDER_IMAGE +export PATH="$TEST_ROOT/engine:$PATH" +export OMARCHY_SKIP_BUILDER_IMAGE=1 +unset OMARCHY_REPO_ROOT OMARCHY_RC_PINS OMARCHY_DEFER_RUNTIME_DEPS OMARCHY_KEEP_BUILD_WORKSPACE + +fixture() { + local name=$1 metadata=$2 + mkdir -p "$TEST_ROOT/pkgbuilds/$name/.omarchy" + printf '{"source":"local"}\n' > "$TEST_ROOT/pkgbuilds/$name/.omarchy/package.json" + cat > "$TEST_ROOT/pkgbuilds/$name/PKGBUILD" < "$TEST_ROOT/pairs.log" 2>&1 || { + cat "$TEST_ROOT/pairs.log" + exit 1 +} +for package in flea omarchy omarchy-dev omarchy-settings omarchy-settings-dev; do + compgen -G "$TEST_ROOT/build-output/edge/$TEST_ARCH/$package-1-1-*.pkg.tar.zst" >/dev/null +done +printf 'PASS: release/dev pairs and Flea build together in isolated roots\n' + +# Test-only and architecture-specific dependencies must precede consumers. +# Failure must block both direct and transitive consumers, while an +# unrelated package still builds in a clean container. +fixture broken '' +cat >> "$TEST_ROOT/pkgbuilds/broken/PKGBUILD" <<'EOF' +prepare() { touch /tmp/omarchy-build-contamination; return 1; } +EOF +fixture consumer "checkdepends_${TEST_ARCH}=('broken')" +fixture transitive "makedepends=('consumer')" +fixture independent '' +if run_build transitive consumer broken independent > "$TEST_ROOT/failure.log" 2>&1; then + cat "$TEST_ROOT/failure.log" + echo 'FAIL: a failed prerequisite did not fail the run' >&2 + exit 1 +fi +grep -q 'consumer blocked by unsuccessful dependency: broken' "$TEST_ROOT/failure.log" +grep -q 'transitive blocked by unsuccessful dependency: consumer' "$TEST_ROOT/failure.log" +compgen -G "$TEST_ROOT/build-output/edge/$TEST_ARCH/independent-1-1-*.pkg.tar.zst" >/dev/null +if compgen -G "$TEST_ROOT/build-output/edge/$TEST_ARCH/consumer-*.pkg.tar.zst" >/dev/null; then + echo 'FAIL: consumer of a failed prerequisite was built' >&2 + exit 1 +fi +printf 'PASS: failed prerequisite blocks consumers; independent build remains clean\n' + +# A later job can resolve dependencies from artifacts kept by the caller. +# Make consumer depend on the already staged fixture without selecting it. +sed -i "s/^checkdepends_.*/depends=('independent')/" "$TEST_ROOT/pkgbuilds/consumer/PKGBUILD" +OMARCHY_KEEP_BUILD_WORKSPACE=1 run_build consumer > "$TEST_ROOT/seeded.log" 2>&1 || { + cat "$TEST_ROOT/seeded.log" + exit 1 +} +compgen -G "$TEST_ROOT/build-output/edge/$TEST_ARCH/consumer-1-1-*.pkg.tar.zst" >/dev/null +printf 'PASS: kept workspace supplies dependencies to a later isolated build\n' + +# A rebuild may keep its filename while changing its bytes. Pacman's shared +# cache must not hide the replacement artifact from the next consumer. +cat >> "$TEST_ROOT/pkgbuilds/independent/PKGBUILD" <<'EOF' +package() { + install -Dm644 /dev/null "$pkgdir/usr/share/build-isolation/independent" + echo replacement > "$pkgdir/usr/share/build-isolation/independent" +} +EOF +cat >> "$TEST_ROOT/pkgbuilds/consumer/PKGBUILD" <<'EOF' +check() { [[ $(cat /usr/share/build-isolation/independent) == replacement ]]; } +EOF +OMARCHY_KEEP_BUILD_WORKSPACE=1 run_build independent consumer > "$TEST_ROOT/rebuilt.log" 2>&1 || { + cat "$TEST_ROOT/rebuilt.log" + exit 1 +} +printf 'PASS: rebuilt artifacts supersede cached packages with the same filename\n' + +# CI's deferred pair mode still accepts the complete request although each +# half now builds in its own container. An unavailable runtime dependency +# proves it was deferred; build/test dependencies still use pacman normally. +sed -i "s/^depends=.*/depends=('omarchy-settings-dev' 'unavailable-runtime-fixture')/" "$TEST_ROOT/pkgbuilds/omarchy-dev/PKGBUILD" +OMARCHY_DEFER_RUNTIME_DEPS=true run_build omarchy-dev omarchy-settings-dev > "$TEST_ROOT/deferred.log" 2>&1 || { + cat "$TEST_ROOT/deferred.log" + exit 1 +} +compgen -G "$TEST_ROOT/build-output/edge/$TEST_ARCH/omarchy-dev-1-1-*.pkg.tar.zst" >/dev/null +printf 'PASS: deferred pair mode works across isolated containers\n' + +fixture excluded '' +printf '{"source":"local","channels":["stable"]}\n' > "$TEST_ROOT/pkgbuilds/excluded/.omarchy/package.json" +run_build excluded > "$TEST_ROOT/empty.log" 2>&1 || { cat "$TEST_ROOT/empty.log"; exit 1; } +grep -q 'Total packages: 0' "$TEST_ROOT/empty.log" +if grep -q 'in a fresh container' "$TEST_ROOT/empty.log"; then + echo 'FAIL: an excluded package started a build container' >&2 + exit 1 +fi +printf 'PASS: empty plans finish without starting a package build\n'