# justfile — CEF builds for zshot / cef-rs.
#
# Reads .env (see .env.example). Driven from this Mac:
#   - Linux x64 and arm64 build in a throwaway Ubuntu container on the remote
#     Docker daemon DOCKER_HOST (ssh://<x64 host>), against a persistent
#     checkout under LINUX_BUILD_ROOT on that host. arm64 cross-compiles via
#     Chromium's sysroot.
#   - macOS arm64 builds in a fresh Tart VM cloned per build, against a
#     persistent data disk (MAC_DATA_DISK) holding the checkout.
# Either way the orchestrator pushes CEF_REF from CEF_SRC to a bare mirror on
# the build host, so build hosts need no credentials for the CEF fork.
#
# Chromium links the released FFmpeg named by FFMPEG_VERSION (fetched and
# cosign-verified by scripts/fetch-ffmpeg.sh), never its bundled copy.
#
# Release cycle (README.md):
#   just cef-status; just cef-update              take upstream CEF's fixes
#   just linux; just linux-collect; just mac      build
#   just smoke; just ceftests ...                 test the archives
#   just publish-source <v>; just publish <v>     to staging/
#   just bindings <v>; just zshot-test ...        cef-rs against staging
#   just promote <v>                              staging/ -> production
#
# Required tools: brew install just docker rsync cosign awscli cirruslabs/cli/tart

set dotenv-load

# List recipes
default:
    @just --list

# Upstream commits and Chromium releases we lack, our patches, newer branches
cef-status:
    scripts/cef-sync.sh status

# Merges, never rebases (published builds name commits on the branch); doesn't push.
# Merge upstream/<CEF_BRANCH> into zshot/<CEF_BRANCH> in CEF_SRC
cef-update:
    scripts/cef-sync.sh update

# For a new Chromium milestone; then set CEF_BRANCH/CEF_REF in .env.
# Start zshot/<new> from upstream/<new> with our patches cherry-picked
cef-branch new:
    scripts/cef-sync.sh branch {{new}}

# Fetch and verify the FFmpeg FFMPEG_VERSION names, for every target
ffmpeg-fetch:
    #!/usr/bin/env bash
    set -euo pipefail
    for t in linux-x86_64 linux-aarch64 darwin-arm64; do scripts/fetch-ffmpeg.sh "$t" >/dev/null; done

# Arches build in order from one checkout. Follow with `just linux-logs`,
# then `just linux-collect`.
# Start the Linux build (detached) on the remote Docker host
linux arches="x64 arm64":
    #!/usr/bin/env bash
    set -euo pipefail
    source scripts/lib.sh
    : "${CEF_SRC:?}" "${CEF_REF:?}" "${CEF_BRANCH:?}" "${CEF_MASTER_REF:?}"
    : "${LINUX_BUILD_ROOT:?}" "${LINUX_MIN_AVAIL_GB:?}"
    host=$(linux_ssh_host)

    sha=$(git -C "$CEF_SRC" rev-parse "$CEF_REF")
    chromium_tag=$(git -C "$CEF_SRC" show "$sha:CHROMIUM_BUILD_COMPATIBILITY.txt" \
        | grep -oE 'refs/tags/[0-9.]+' | sed 's|refs/tags/||')
    echo "==> CEF $CEF_REF @ $sha (Chromium $chromium_tag) on $host: {{arches}}"

    # Something scaled back up mid-build would only fail hours in, at the link.
    # MemAvailable doesn't count the ZFS ARC, which gives memory back under
    # pressure down to c_min, so add the reclaimable part. The ARC releases
    # slowly, though, so cap zfs_arc_max on the host for builds (see README).
    read -r avail arc arc_max < <(ssh "$host" bash -s <<'EOF'
    awk '/MemAvailable/ {a=$2*1024} END {printf "%d ", a/2^30}' /proc/meminfo
    f=/proc/spl/kstat/zfs/arcstats
    if [ -r "$f" ]; then
        awk '$1=="size" {s=$3} $1=="c_min" {m=$3} $1=="c_max" {x=$3}
             END {r=s-m; if (r<0) r=0; printf "%d %d\n", r/2^30, x/2^30}' "$f"
    else
        echo "0 0"
    fi
    EOF
    )
    total=$((avail + arc))
    echo "==> ${total} GB available (${avail} free + ${arc} reclaimable ARC; ARC max ${arc_max} GB)"
    if (( total < LINUX_MIN_AVAIL_GB )); then
        echo "need $LINUX_MIN_AVAIL_GB GB on $host" >&2
        exit 1
    fi

    ssh "$host" "mkdir -p '$LINUX_BUILD_ROOT' && { [ -d '$LINUX_BUILD_ROOT/cef.git' ] || git init -q --bare '$LINUX_BUILD_ROOT/cef.git'; }"
    # CEF's version string needs both refs (tools/cef_version.py): HEAD must
    # be on a branch named for the Chromium build, or it gets the
    # "-<branch>.N" dev format, and the patch number counts commits since
    # origin/master.
    git -C "$CEF_SRC" push --force "$host:$LINUX_BUILD_ROOT/cef.git" "$sha:refs/heads/$CEF_BRANCH" "$CEF_MASTER_REF:refs/heads/master"

    # FFmpeg for Chromium to link, extracted to <root>/ffmpeg/<arch> with a
    # description for the build record.
    for arch in {{arches}}; do
        target=$(ffmpeg_target linux "$arch")
        tarball=$(scripts/fetch-ffmpeg.sh "$target")
        dir="$LINUX_BUILD_ROOT/ffmpeg/$arch"
        ssh "$host" "rm -rf '$dir' && mkdir -p '$dir' && tar -xzf - -C '$dir'" < "$tarball"
        ffmpeg_json "$tarball" "$target" | ssh "$host" "cat > '$dir/.zshot-ffmpeg.json'"
    done

    docker build \
        --platform linux/amd64 \
        --build-arg CHROMIUM_TAG="$chromium_tag" \
        -t cef-build-linux \
        -f scripts/Dockerfile.linux \
        scripts

    read -r uid gid < <(ssh "$host" 'echo $(id -u) $(id -g)')
    docker rm -f cef-build-linux >/dev/null 2>&1 || true
    # Any build that held the lock was in that container, which is gone now.
    ssh "$host" "rmdir '$LINUX_BUILD_ROOT/.build.lock' 2>/dev/null || true"
    docker run -d \
        --name cef-build-linux \
        --platform linux/amd64 \
        --user "$uid:$gid" \
        -e HOME=/build/home \
        -e BUILD_ROOT=/build \
        -e CEF_BRANCH="$CEF_BRANCH" \
        -e CEF_CHECKOUT="$sha" \
        -e EXTERNAL_FFMPEG=1 \
        -e CEF_BUILD_REV="$(cef_build_rev)" \
        -e CEF_BUILD_DIRTY="$(cef_build_dirty)" \
        -v "$LINUX_BUILD_ROOT:/build" \
        cef-build-linux \
        bash /opt/cef-build/build-cef.sh linux {{arches}}
    echo "==> Started. Follow with: just linux-logs"

# Follow the running (or last) Linux build's output
linux-logs:
    docker logs -f cef-build-linux

# Stop the running Linux build
linux-stop:
    docker stop cef-build-linux

# Copy finished Linux archives, build records and test bundles into ./dist
linux-collect:
    #!/usr/bin/env bash
    set -euo pipefail
    source scripts/lib.sh
    mkdir -p dist
    rsync -av "$(linux_ssh_host):$LINUX_BUILD_ROOT/dist/" ./dist/

# Create the persistent sparse data disk for the macOS build VM (once)
mac-disk:
    #!/usr/bin/env bash
    set -euo pipefail
    : "${MAC_DATA_DISK:?}" "${MAC_DATA_DISK_GB:?}"
    if [ -e "$MAC_DATA_DISK" ]; then
        echo "$MAC_DATA_DISK already exists"; exit 0
    fi
    mkdir -p "$(dirname "$MAC_DATA_DISK")"
    # Sparse: takes no space until written. Formatted inside the VM on first use.
    dd if=/dev/zero of="$MAC_DATA_DISK" bs=1 count=0 seek="${MAC_DATA_DISK_GB}g"
    ls -lhs "$MAC_DATA_DISK"

# Runs in the foreground for hours (use tmux); Ctrl-C stops and deletes the
# VM, but the checkout on the data disk survives for the next run.
# Build macOS in a fresh Tart VM against the persistent data disk
mac arch="arm64":
    #!/usr/bin/env bash
    set -euo pipefail
    source scripts/lib.sh
    source scripts/mac-vm.sh
    : "${CEF_SRC:?}" "${CEF_REF:?}" "${CEF_BRANCH:?}" "${CEF_MASTER_REF:?}"
    : "${MAC_VM:?}" "${MAC_VM_BASE:?}" "${MAC_DATA_DISK:?}" "${MAC_DATA_DISK_GB:?}"
    [ -e "$MAC_DATA_DISK" ] || { echo "no data disk at $MAC_DATA_DISK; run 'just mac-disk'" >&2; exit 1; }

    sha=$(git -C "$CEF_SRC" rev-parse "$CEF_REF")
    root=/Volumes/CEFWork   # the APFS image on the data disk (mac-mount-data-disk.sh)
    target=$(ffmpeg_target mac "{{arch}}")
    tarball=$(scripts/fetch-ffmpeg.sh "$target")

    echo "==> CEF $CEF_REF @ $sha in fresh Tart VM '$MAC_VM' ({{arch}})"
    vm_up "$MAC_VM" "$MAC_VM_BASE" "$MAC_DATA_DISK"

    vm_push scripts cef-build-scripts
    vm_ssh DATA_DISK_GB="$MAC_DATA_DISK_GB" bash cef-build-scripts/mac-mount-data-disk.sh

    # A lock left on the data disk is from a build whose VM was torn down
    # (Ctrl-C); the VM was deleted above, so nothing can still hold it.
    vm_ssh "rmdir $root/.build.lock 2>/dev/null || true; [ -d $root/cef.git ] || git init -q --bare $root/cef.git"
    GIT_SSH_COMMAND="ssh ${vm_ssh_opts[*]}" \
        git -C "$CEF_SRC" push --force "admin@$vm_ip:$root/cef.git" "$sha:refs/heads/$CEF_BRANCH" "$CEF_MASTER_REF:refs/heads/master"

    # FFmpeg for Chromium to link, extracted to <root>/ffmpeg/<arch>.
    dir="$root/ffmpeg/{{arch}}"
    vm_ssh "rm -rf $dir && mkdir -p $dir && tar -xzf - -C $dir" < "$tarball"
    ffmpeg_json "$tarball" "$target" | vm_ssh "cat > $dir/.zshot-ffmpeg.json"

    vm_ssh "BUILD_ROOT=$root CEF_BRANCH=$CEF_BRANCH CEF_CHECKOUT=$sha EXTERNAL_FFMPEG=1 \
            CEF_BUILD_REV=$(cef_build_rev) CEF_BUILD_DIRTY=$(cef_build_dirty) \
            bash cef-build-scripts/build-cef.sh mac {{arch}}"

    echo "==> Pulling artifacts"
    mkdir -p dist
    vm_ssh "tar -C $root/dist -cf - ." | tar -C dist -xvf -

# Default: every platform's archive of the newest version in dist/. Arguments
# are archives or versions (156.0.8+g3fec89c+chromium-156.0.8078.17).
# Static checks (layout, FFmpeg linkage, rpaths, glibc) on dist archives
smoke *archives:
    #!/usr/bin/env bash
    set -euo pipefail
    args=({{archives}})
    if [ ${#args[@]} -eq 0 ]; then
        latest=$(ls -t dist/cef_binary_*_minimal.tar.bz2 2>/dev/null | head -1)
        [ -n "$latest" ] || { echo "no archives in dist/" >&2; exit 1; }
        v=${latest#dist/cef_binary_}; args=("${v%_*_minimal.tar.bz2}")
    fi
    archives=()
    for a in "${args[@]}"; do
        case "$a" in
          *.tar.bz2) archives+=("$a") ;;
          *) archives+=(dist/cef_binary_"$a"_*_minimal.tar.bz2) ;;
        esac
    done
    python3 -I scripts/smoke.py "${archives[@]}"

# Results land in logs/ceftests-<platform>-<arch>-<time>/. Expected failures:
# tests/ceftests-filter/<platform>.txt (+ <platform>-<arch>.txt). Default
# version: the newest in dist/.
# CEFTESTS_ARGS adds ceftests flags; CEFTESTS_ONLY=<gtest filter> runs only those.
# Run ceftests against a dist archive: linux x64|arm64, or mac arm64
ceftests platform arch version="":
    #!/usr/bin/env bash
    set -euo pipefail
    source scripts/lib.sh
    case "{{platform}}/{{arch}}" in
      linux/x64) plat=linux64 docker_platform=linux/amd64 ;;
      linux/arm64) plat=linuxarm64 docker_platform=linux/arm64 ;;
      mac/arm64) plat=macosarm64 ;;
      *) echo "unknown platform/arch {{platform}}/{{arch}}" >&2; exit 64 ;;
    esac
    version="{{version}}"
    if [ -z "$version" ]; then
        latest=$(ls -t dist/cef_binary_*_"${plat}"_minimal.tar.bz2 2>/dev/null | head -1)
        [ -n "$latest" ] || { echo "no $plat archive in dist/" >&2; exit 1; }
        version=${latest#dist/cef_binary_}; version=${version%_"${plat}"_minimal.tar.bz2}
    fi
    archive="dist/cef_binary_${version}_${plat}_minimal.tar.bz2"
    bundle="dist/tests/ceftests_${version}_${plat}.tar.gz"
    for f in "$archive" "$archive.build.json" "$bundle"; do
        [ -f "$f" ] || { echo "missing $f (just linux-collect?)" >&2; exit 1; }
    done
    # Test with the FFmpeg the archive was built against.
    ffmpeg_version=$(python3 -I -c 'import json,sys; print(json.load(open(sys.argv[1]))["ffmpeg"]["version"])' "$archive.build.json")
    [ "$ffmpeg_version" = "$(tr -d '[:space:]' < FFMPEG_VERSION)" ] \
        || { echo "$archive was built against FFmpeg $ffmpeg_version, but FFMPEG_VERSION is $(cat FFMPEG_VERSION)" >&2; exit 1; }
    ffmpeg=$(scripts/fetch-ffmpeg.sh "$(ffmpeg_target {{platform}} {{arch}})")
    # The platform's expected failures plus the arch's, if any.
    filter="$(mktemp -d)/ceftests-filter.txt"
    cat "tests/ceftests-filter/{{platform}}.txt" > "$filter"
    [ ! -f "tests/ceftests-filter/{{platform}}-{{arch}}.txt" ] || cat "tests/ceftests-filter/{{platform}}-{{arch}}.txt" >> "$filter"
    out="$PWD/logs/ceftests-{{platform}}-{{arch}}-$(date -u +%Y%m%dT%H%M%SZ)"
    mkdir -p "$out"
    echo "==> ceftests $version on {{platform}}/{{arch}} -> $out"

    stage=$(mktemp -d)
    cid=""
    cleanup() { [ -z "$cid" ] || docker rm -f "$cid" >/dev/null 2>&1 || true; rm -rf "$stage"; }
    trap cleanup EXIT
    cp "$archive" "$bundle" "$ffmpeg" "$filter" scripts/run-ceftests.sh "$stage/"
    args=("$(basename "$archive")" "$(basename "$bundle")" "$(basename "$ffmpeg")" "$(basename "$filter")" out)

    status=0
    if [ "{{platform}}" = linux ]; then
        # x64 on the remote x86 daemon; arm64 natively on the local Mac's
        # (qemu can't run Chromium).
        [ "{{arch}}" = arm64 ] && unset DOCKER_HOST
        echo "==> docker: ${DOCKER_HOST:-local}"
        docker build --platform "$docker_platform" -t "cef-build-test-{{arch}}" -f scripts/Dockerfile.test scripts
        cid=$(docker create --platform "$docker_platform" --shm-size=2g -w /work \
            -e CEFTESTS_ARGS="${CEFTESTS_ARGS:-}" -e CEFTESTS_ONLY="${CEFTESTS_ONLY:-}" \
            "cef-build-test-{{arch}}" bash /work/run-ceftests.sh "${args[@]}")
        docker cp "$stage/." "$cid:/work/"
        docker start -a "$cid" || status=$?
        docker cp "$cid:/work/out/." "$out/" || true
    else
        source scripts/mac-vm.sh
        : "${MAC_TEST_VM_BASE:?}"
        trap 'vm_down; cleanup' EXIT
        vm_up cef-tester "$MAC_TEST_VM_BASE"
        trap 'vm_down; cleanup' EXIT
        vm_push "$stage" work
        vm_ssh "cd work && CEFTESTS_ARGS='${CEFTESTS_ARGS:-}' CEFTESTS_ONLY='${CEFTESTS_ONLY:-}' bash run-ceftests.sh ${args[*]@Q}" || status=$?
        vm_ssh "tar -C work/out -cf - ." | tar -C "$out" -xf - || true
    fi
    python3 -I scripts/gtest-summary.py "$out/ceftests.xml" || true
    exit "$status"

# Uploads to the bucket root (sources are immutable): cef-source-<full>.tar.gz
# (the CEF commit, these scripts, the build records) and, once per Chromium
# version, a mirror of Google's chromium-<tag>.tar.xz (~4.5 GB). Required
# before `just publish` (LGPL: libcef statically links Blink).
# Publish the corresponding source for a dist release (e.g. 156.0.8)
publish-source version *flags:
    python3 -I scripts/publish.py source {{version}} {{flags}}

# Runs the LGPL/release preflight (COMPLIANCE.md), then uploads the archives
# and their signatures and merges them into staging/index.json. Point cef-rs
# at it with CEF_DOWNLOAD_URL=$CEF_PUBLIC_URL/staging.
# Publish a dist release (e.g. 156.0.8) to staging/
publish version:
    python3 -I scripts/publish.py publish {{version}}

# `publish` runs this too. Keys: symbols/<platform>/<build id>/<file>, plus
# symbols/index/cef-<version>/<platform>.json (cef-rs scripts/release/SYMBOLS.md).
# Upload a release's debug symbols to the private B2_SYMBOLS_BUCKET
publish-symbols version:
    python3 -I scripts/publish.py symbols {{version}}

# Platform: linux-amd64, linux-arm64 or macos-arm64. Lands in ~/.cache/zshot-symbols/.
# Download a published CEF build's debug symbols
symbols-fetch version platform:
    python3 -I scripts/fetch-symbols.py {{version}} {{platform}}

# Promote a staged release to the bucket root, where cef-rs downloads by default
promote version:
    python3 -I scripts/publish.py promote {{version}}

# cef-rs's `just cef-bindings` against the staged release: a cef/<version>
# branch in CEF_RS_SRC with the CEF version bumped and the bindings for our
# three targets regenerated, uncommitted (cef-rs must have no uncommitted
# changes to tracked files). Linux bindings generate on DOCKER_HOST.
# Regenerate cef-rs bindings for a staged release (e.g. 156.0.8)
bindings version:
    just -f "${CEF_RS_SRC:?}/justfile" -d "$CEF_RS_SRC" cef-bindings {{version}} "${CEF_PUBLIC_URL:?}/staging"

# Runs cef-rs's suite in CEF_RS_SRC (normally the cef/<version> branch from
# `just bindings`) against staging: linux in the CI image (x64 on DOCKER_HOST,
# arm64 locally), mac natively after rebuilding the app bundle so it carries
# the new framework. Extra args go to test.sh.
# Run the zshot test suite against staging: linux x64|arm64, or mac arm64
zshot-test platform arch *args:
    #!/usr/bin/env bash
    set -euo pipefail
    : "${CEF_RS_SRC:?}" "${CEF_PUBLIC_URL:?}"
    export CEF_DOWNLOAD_URL="$CEF_PUBLIC_URL/staging"
    case "{{platform}}/{{arch}}" in
      linux/x64)   just -f "$CEF_RS_SRC/justfile" -d "$CEF_RS_SRC" test-docker amd64 {{args}} ;;
      linux/arm64) just -f "$CEF_RS_SRC/justfile" -d "$CEF_RS_SRC" test-docker arm64 {{args}} ;;
      # With cef-rs's tool versions, not this directory's: mise picks node
      # per directory, and the replay tests need node >= 20.
      mac/arm64)
        cd "$CEF_RS_SRC"
        run=(); ! command -v mise >/dev/null || run=(mise -C "$CEF_RS_SRC" exec --)
        ${run[@]+"${run[@]}"} ./build.sh
        ${run[@]+"${run[@]}"} ./test.sh {{args}} ;;
      *) echo "unknown platform/arch {{platform}}/{{arch}}" >&2; exit 64 ;;
    esac
