# The default entrypoint to working on this project.
# Run `just --list` to see available targets organized by group.
#
# See also `Makefile` and `xtask.rs`. Commands which end in `-local`
# skip containerization or virtualization (and typically just proxy `make`).
#
# By default the layering is:
# Github Actions -> Justfile -> podman -> make -> rustc
#                            -> podman -> package manager
#                            -> cargo xtask
# --------------------------------------------------------------------

# Configuration variables (override via environment or command line)
# Example: BOOTC_base=quay.io/fedora/fedora-bootc:42 just build

# Output image name
base_img := "localhost/bootc"
# Synthetic upgrade image for testing
upgrade_img := base_img + "-upgrade"
# Base image with tmt dependencies added, used as the boot source for upgrade tests
upgrade_source_img := base_img + "-upgrade-source"

# Build variant: ostree (default) or composefs
variant := env("BOOTC_variant", "ostree")
bootloader := env("BOOTC_bootloader", "grub")
# Only used for composefs tests
filesystem := env("BOOTC_filesystem", "ext4")
# Only used for composefs tests
boot_type := env("BOOTC_boot_type", "bls")
# Only used for composefs tests
seal_state := env("BOOTC_seal_state", "unsealed")
# Base container image to build from
base := env("BOOTC_base", "quay.io/centos-bootc/centos-bootc:stream10")
# Buildroot base image
buildroot_base := env("BOOTC_buildroot_base", "quay.io/centos/centos:stream10")
# Optional: path to extra source (e.g. composefs-rs) for local development
# DEPRECATED: Use [patch] sections in Cargo.toml instead, which are auto-detected
extra_src := env("BOOTC_extra_src", "")
# Set to "1" to disable auto-detection of local Rust dependencies
no_auto_local_deps := env("BOOTC_no_auto_local_deps", "")

# Internal variables
nocache := env("BOOTC_nocache", "")
_nocache_arg := if nocache != "" { "--no-cache" } else { "" }
testimage_label := "bootc.testimage=1"
lbi_images := "quay.io/curl/curl:latest quay.io/curl/curl-base:latest registry.access.redhat.com/ubi9/podman:latest"
fedora-coreos := "quay.io/fedora/fedora-coreos:testing-devel"
generic_buildargs := ""
_extra_src_args := if extra_src != "" { "-v " + extra_src + ":/run/extra-src:ro --security-opt=label=disable" } else { "" }
base_buildargs := generic_buildargs + " " + _extra_src_args \
                  + " --build-arg=base=" + base \
                  + " --build-arg=variant=" + variant \
                  + " --build-arg=bootloader=" + bootloader \
                  + " --build-arg=boot_type=" + boot_type \
                  + " --build-arg=seal_state=" + seal_state \
                  + " --build-arg=filesystem=" + filesystem # required for bootc container ukify to allow missing fsverity
buildargs := base_buildargs \
             + " --cap-add=all --security-opt=label=type:container_runtime_t --device /dev/fuse" \
             + " --secret=id=secureboot_key,src=target/test-secureboot/db.key --secret=id=secureboot_cert,src=target/test-secureboot/db.crt"

# ============================================================================
# Core workflows - the main targets most developers will use
# ============================================================================

# Build container image from current sources (default target)
[group('core')]
build: package _keygen && _pull-lbi-images
    #!/bin/bash
    set -xeuo pipefail
    test -d target/packages
    pkg_path=$(realpath target/packages)
    eval $(just _git-build-vars)
    podman build {{_nocache_arg}} --build-arg=image_version=${VERSION} --build-context "packages=${pkg_path}" -t {{base_img}} {{buildargs}} .

# Show available build variants and current configuration
[group('core')]
list-variants:
    #!/bin/bash
    cat <<'EOF'
    Build Variants (set via BOOTC_variant= or variant=)
    ====================================================

    ostree (default)
        Standard bootc image using ostree backend.
        This is the traditional, production-ready configuration.

    composefs (bootloader, filesystem, boot_type, seal_state)
        Build Composefs image with:
        - The specified bootloader (grub/systemd)
        - The specified filesystem (ext4,btrfs,xfs)
        - The specified boot type (BLS/UKI)
        - The specified seal state (sealed/unsealed) determining whether we sign the UKI and
          use secure boot or not

    Use `just build-sealed` as shortcut to build a sealed composefs image with systemd-boot as the bootloader

    Current Configuration
    =====================
    EOF
    echo "    BOOTC_variant={{variant}}"
    echo "    BOOTC_base={{base}}"
    echo "    BOOTC_extra_src={{extra_src}}"
    echo ""

# Build a sealed composefs image (alias for variant=composefs-sealeduki-sdboot)
[group('core')]
build-sealed:
    @just --justfile {{justfile()}} variant=composefs bootloader=systemd boot_type=uki seal_state=sealed build

# Run tmt integration tests in VMs (e.g. `just test-tmt readonly`)
[group('core')]
test-tmt *ARGS: build
    @just _build-upgrade-image
    @just test-tmt-nobuild {{ARGS}}

# Run containerized unit and integration tests
[group('core')]
test-container: build build-units
    podman run --rm --read-only localhost/bootc-units /usr/bin/bootc-units
    podman run --rm --env=BOOTC_variant={{variant}} --env=BOOTC_base={{base}} --env=BOOTC_boot_type={{boot_type}} --mount=type=image,source={{base_img}},target=/run/target {{base_img}} bootc-integration-tests container

[group('core')]
test-composefs bootloader filesystem boot_type seal_state *ARGS:
    @if [ "{{seal_state}}" = "sealed" ] && [ "{{filesystem}}" = "xfs" ]; then \
        echo "Invalid combination: sealed requires filesystem that supports fs-verity (ext4, btrfs)"; \
        exit 1; \
    fi

    @if [ "{{seal_state}}" = "sealed" ] && [ "{{boot_type}}" != "uki" ]; then \
        echo "Invalid combination: sealed requires boot_type=uki"; \
        exit 1; \
    fi

    just variant=composefs \
        bootloader={{bootloader}} \
        filesystem={{filesystem}} \
        boot_type={{boot_type}} \
        seal_state={{seal_state}} \
            test-tmt --composefs-backend \
                --bootloader={{bootloader}} \
                --filesystem={{filesystem}} \
                --seal-state={{seal_state}} \
                --boot-type={{boot_type}} \
                {{ARGS}} \
                $(if [ "{{boot_type}}" = "uki" ]; then echo "readonly image-upgrade-reboot"; else echo "integration"; fi)

# Run upgrade test: boot VM from published base image (with tmt deps added),
# upgrade to locally-built image, reboot, then run readonly tests to verify.
# The --upgrade-image flag triggers --bind-storage-ro in bcvk, making the
# locally-built image available inside the VM via containers-storage transport.
[group('core')]
test-upgrade *ARGS: build _build-upgrade-source-image
    #!/bin/bash
    set -xeuo pipefail
    composefs_args=()
    if [[ "{{variant}}" = composefs ]]; then
        composefs_args=(--composefs-backend \
            --bootloader={{bootloader}} \
            --filesystem={{filesystem}} \
            --seal-state={{seal_state}} \
            --boot-type={{boot_type}} \
            --karg=enforcing=0)
    fi
    cargo xtask run-tmt --env=BOOTC_variant={{variant}} \
        --env=BOOTC_test_upgrade_image={{base_img}} \
        --upgrade-image={{base_img}} \
        "${composefs_args[@]}" \
        {{upgrade_source_img}} {{ARGS}} readonly

# Run cargo fmt and clippy checks in container
[group('core')]
validate:
    podman build {{base_buildargs}} --target validate .

# Test container export via Anaconda liveimg install in a QEMU VM
[group('testing')]
test-container-export: build
    #!/bin/bash
    set -xeuo pipefail
    iso=target/anaconda-test/boot.iso
    if [ ! -f "$iso" ]; then
        # Determine the ISO download URL from the base image's os-release
        eval $(podman run --rm {{base_img}} bash -c '. /etc/os-release && echo "ID=$ID VERSION_ID=$VERSION_ID"')
        case "${ID}-${VERSION_ID}" in
            centos-10)
                url="https://mirror.stream.centos.org/10-stream/BaseOS/x86_64/iso/CentOS-Stream-10-latest-x86_64-boot.iso" ;;
            fedora-*)
                url="https://download.fedoraproject.org/pub/fedora/linux/releases/${VERSION_ID}/Everything/x86_64/iso/Fedora-Everything-netinst-x86_64-${VERSION_ID}-1.1.iso" ;;
            *)
                echo "Unsupported OS: ${ID}-${VERSION_ID}" >&2; exit 1 ;;
        esac
        mkdir -p target/anaconda-test
        curl -L --retry 3 --progress-bar -o "$iso" "$url"
    fi
    cargo run -p tests-integration -- anaconda-test --iso "$iso" {{base_img}}

# ============================================================================
# Testing variants and utilities
# ============================================================================

# Run tmt tests without rebuilding (for fast iteration)
[group('testing')]
test-tmt-nobuild *ARGS:
    cargo xtask run-tmt --env=BOOTC_variant={{variant}} --upgrade-image={{upgrade_img}} {{base_img}} {{ARGS}}

# Run tmt tests on Fedora CoreOS
[group('testing')]
test-tmt-on-coreos *ARGS:
    cargo xtask run-tmt --env=BOOTC_variant={{variant}} --env=BOOTC_target={{base_img}}-coreos:latest {{fedora-coreos}} {{ARGS}}

# Run external container tests against localhost/bootc
[group('testing')]
run-container-external-tests:
   ./tests/container/run {{base_img}}

# Remove all test VMs created by tmt tests
[group('testing')]
tmt-vm-cleanup:
    bcvk libvirt rm --stop --force --label bootc.test=1

# Build test image for Fedora CoreOS testing
[group('testing')]
build-testimage-coreos PATH: _keygen
    #!/bin/bash
    set -xeuo pipefail
    pkg_path=$(realpath "{{PATH}}")
    podman build --build-context "packages=${pkg_path}" \
        --build-arg SKIP_CONFIGS=1 \
        -t {{base_img}}-coreos {{buildargs}} .

# Build test image for install tests (used by CI)
[group('testing')]
build-install-test-image: build
    cd hack && podman build {{base_buildargs}} -t {{base_img}}-install -f Containerfile.drop-lbis

# ============================================================================
# Documentation
# ============================================================================

# Serve docs locally (prints URL)
[group('docs')]
mdbook-serve: build-mdbook
    #!/bin/bash
    set -xeuo pipefail
    podman run --init --replace -d --name bootc-mdbook --rm --publish 127.0.0.1::8000 localhost/bootc-mdbook
    echo http://$(podman port bootc-mdbook 8000/tcp)

# Build the documentation (mdbook)
[group('docs')]
build-mdbook:
    #!/bin/bash
    set -xeuo pipefail
    secret_arg=""
    if test -n "${GH_TOKEN:-}"; then
        secret_arg="--secret=id=GH_TOKEN,env=GH_TOKEN"
    fi
    podman build {{generic_buildargs}} ${secret_arg} -t localhost/bootc-mdbook -f docs/Dockerfile.mdbook .

# Build docs and extract to DIR
[group('docs')]
build-mdbook-to DIR: build-mdbook
    #!/bin/bash
    set -xeuo pipefail
    container_id=$(podman create localhost/bootc-mdbook)
    podman cp ${container_id}:/src/docs/book {{DIR}}
    podman rm -f ${container_id}

# ============================================================================
# Debugging and validation
# ============================================================================

# Validate composefs digests match between build and install views
[group('debugging')]
validate-composefs-digest:
    cargo xtask validate-composefs-digest {{base_img}}

# Verify reproducible builds (runs package twice, compares output)
[group('debugging')]
check-buildsys:
    cargo run -p xtask check-buildsys

# Get container image pullspec for a given OS (e.g. `pullspec-for-os base fedora-42`)
[group('debugging')]
pullspec-for-os TYPE NAME:
    @jq -r --arg v "{{NAME}}" '."{{TYPE}}"[$v]' < hack/os-image-map.json

# ============================================================================
# Maintenance
# ============================================================================

# Update generated files (man pages, JSON schemas)
[group('maintenance')]
update-generated:
    cargo run -p xtask update-generated

# Remove all locally-built test container images
[group('maintenance')]
clean-local-images:
    podman images --filter "label={{testimage_label}}"
    podman images --filter "label={{testimage_label}}" --format "{{{{.ID}}" | xargs -r podman rmi -f
    podman image prune -f
    podman rmi {{fedora-coreos}} -f


# Build packages (RPM) into target/packages/
[group('maintenance')]
package:
    #!/bin/bash
    set -xeuo pipefail
    packages=target/packages
    if test -n "${BOOTC_SKIP_PACKAGE:-}"; then
        if test '!' -d "${packages}"; then
            echo "BOOTC_SKIP_PACKAGE is set, but missing ${packages}" 1>&2; exit 1
        fi
        exit 0
    fi
    eval $(just _git-build-vars)
    echo "Building RPM with version: ${VERSION}"
    # Auto-detect local Rust path dependencies (e.g., from [patch] sections)
    local_deps_args=""
    if [[ -z "{{no_auto_local_deps}}" ]]; then
        local_deps_args=$(cargo xtask local-rust-deps)
    fi
    podman build {{base_buildargs}} --build-arg=SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} --build-arg=pkgversion=${VERSION} -t localhost/bootc-pkg --target=build $local_deps_args .
    mkdir -p "${packages}"
    rm -vf "${packages}"/*.rpm
    podman run --rm localhost/bootc-pkg tar -C /out/ -cf - . | tar -C "${packages}"/ -xvf -
    chmod a+rx target "${packages}"
    chmod a+r "${packages}"/*.rpm

# Build unit tests into a container image
[group('maintenance')]
build-units:
    #!/bin/bash
    set -xeuo pipefail
    eval $(just _git-build-vars)
    podman build {{base_buildargs}} --build-arg=SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} --build-arg=pkgversion=${VERSION} --target units -t localhost/bootc-units .

# ============================================================================
# Internal helpers (prefixed with _)
# ============================================================================

_pull-lbi-images:
    podman pull -q --retry 5 --retry-delay 5s {{lbi_images}}

_git-build-vars:
    #!/bin/bash
    set -euo pipefail
    SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
    if VERSION=$(git describe --tags --exact-match 2>/dev/null); then
        VERSION="${VERSION#v}"
        VERSION="${VERSION//-/.}"
    else
        COMMIT=$(git rev-parse HEAD | cut -c1-10)
        COMMIT_TS=$(git show -s --format=%ct)
        TIMESTAMP=$(date -u -d @${COMMIT_TS} +%Y%m%d%H%M)
        VERSION="${TIMESTAMP}.g${COMMIT}"
    fi
    echo "SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}"
    echo "VERSION=${VERSION}"

_keygen:
    ./hack/generate-secureboot-keys

_build-upgrade-image:
    #!/bin/bash
    set -xeuo pipefail
    # Secrets are always available (test-tmt depends on build which runs _keygen).
    # Extra capabilities are only needed for UKI builds (composefs + fuse).
    extra_args=()
    if [ "{{boot_type}}" = "uki" ]; then
        extra_args+=(--cap-add=all --security-opt=label=type:container_runtime_t --device /dev/fuse)
    fi
    podman build \
        --build-arg "boot_type={{boot_type}}" \
        --build-arg "seal_state={{seal_state}}" \
        --build-arg "filesystem={{filesystem}}" \
        --secret=id=secureboot_key,src=target/test-secureboot/db.key \
        --secret=id=secureboot_cert,src=target/test-secureboot/db.crt \
        "${extra_args[@]}" \
        -t {{upgrade_img}} \
        -f tmt/tests/Dockerfile.upgrade \
        .

# Build the upgrade source image: base image + tmt dependencies (rsync, nu, cloud-init)
_build-upgrade-source-image:
    podman build --build-arg=base={{base}} --build-arg=variant={{variant}} -t {{upgrade_source_img}} -f tmt/tests/Dockerfile.upgrade-source .

# Copy an image from user podman storage to root's podman storage
# This allows building as regular user then running privileged tests
[group('testing')]
copy-to-rootful $image:
    #!/bin/bash
    set -euxo pipefail

    # If already running as root, nothing to do
    if [[ "${UID}" -eq "0" ]]; then
        echo "Already root, no need to copy image"
        exit 0
    fi

    # Check if the image exists in user storage
    if ! podman image exists "${image}"; then
        echo "Image ${image} not found in user podman storage" >&2
        exit 1
    fi

    # Get the image ID from user storage
    USER_IMG_ID=$(podman images --filter reference="${image}" --format '{{{{.ID}}')

    # Check if the same image ID exists in root storage
    ROOT_IMG_ID=$(sudo podman images --filter reference="${image}" --format '{{{{.ID}}' 2>/dev/null || true)

    if [[ "${USER_IMG_ID}" == "${ROOT_IMG_ID}" ]] && [[ -n "${ROOT_IMG_ID}" ]]; then
        echo "Image ${image} already exists in root storage with same ID"
        exit 0
    fi

    # Copy the image from user to root storage
    # Use podman save/load via pipe (works on systems without machinectl)
    podman save "${image}" | sudo podman load
    echo "Copied ${image} to root podman storage"

# Copy all LBI (bound) images to root's podman storage
[group('testing')]
copy-lbi-to-rootful:
    #!/bin/bash
    set -euxo pipefail
    for img in {{lbi_images}}; do
        just copy-to-rootful "$img"
    done
