#!/usr/bin/env just --justfile
#
# Language-specific recipes for the Rust workspace under rs/. Invoked from
# the repo root as `just rs <recipe>` via the `mod rs` import.
#
# The Rust crates form a single workspace whose root Cargo.toml lives at
# the repository root, so these recipes run from the repo root rather than
# from rs/ (cargo resolves paths relative to the workspace root either way).

set working-directory := '..'

default:
    just check

# Compile, lint, format-check, doc-check, and verify dependency hygiene.
check *args:
    cargo check --all-targets {{ args }}
    cargo clippy --all-targets {{ args }} -- -D warnings
    cargo fmt --all --check
    RUSTDOCFLAGS="-D warnings" cargo doc --no-deps {{ args }}
    cargo shear
    cargo sort --workspace --check --no-format

# Auto-fix clippy/format/shear/sort.
fix *args:
    cargo clippy --fix --allow-staged --allow-dirty --all-targets {{ args }}
    cargo fmt --all
    cargo shear --fix
    cargo sort --workspace --no-format

# Prints `ALL` when the diff hits something every crate depends on, and nothing
# when no crate is affected.
#
# FILES is an exported parameter, not `{{ FILES }}`: just interpolates the
# latter into the recipe source, where a filename like `$(...)` would run as a
# command. Git-derived paths are untrusted input, so they stay data.

# Print `--package` flags for the crates a diff touches, plus their dependents.
[private]
_select $FILES:
    #!/usr/bin/env bash
    set -euo pipefail

    # Workspace-wide inputs affect every crate, so there is nothing to scope to.
    if [[ -z "$FILES" ]] || grep -qE '^(Cargo\.(toml|lock)|rust-toolchain\.toml)$' <<< "$FILES"; then
    	echo ALL
    	exit 0
    fi

    grep -q '^rs/' <<< "$FILES" || exit 0

    # `--no-deps` keeps this to workspace members and off the network.
    metadata=$(cargo metadata --format-version 1 --no-deps)

    # Every crate directory is named after the crate it holds, which is what
    # makes a changed path a seed below. Verify it rather than trust it: under-
    # selecting silently checks nothing, so fall back to checking everything.
    if jq -e '.packages[] | select((.manifest_path | split("/")[-2]) != .name)' <<< "$metadata" > /dev/null; then
    	echo "rs: a crate directory no longer matches its crate name; selecting everything." >&2
    	echo ALL
    	exit 0
    fi

    seeds=$(sed -n 's|^rs/\([^/]*\)/.*|\1|p' <<< "$FILES" | sort -u)

    edges=$(jq -r '
    	.workspace_default_members as $members
    	| .packages[]
    	| select(.id | IN($members[]))
    	| .name as $name
    	| .dependencies[]
    	| "\($name) \(.name)"
    ' <<< "$metadata")

    # A crate has to be rebuilt when anything it depends on changed, so walk the
    # edges backwards until the selection stops growing. Non-default members
    # (libmoq, moq-ffi, moq-gst, moq-wasm) have no edges here and drop out,
    # matching what an unscoped `cargo check` would have compiled.
    awk -v seeds="$seeds" '
    	BEGIN { split(seeds, s, "\n"); for (i in s) want[s[i]] = 1 }
    	{ pkg[NR] = $1; dep[NR] = $2 }
    	END {
    		do {
    			grew = 0
    			for (i = 1; i <= NR; i++)
    				if (want[dep[i]] && !want[pkg[i]]) { want[pkg[i]] = 1; grew = 1 }
    		} while (grew)
    		for (i = 1; i <= NR; i++) if (want[pkg[i]]) print pkg[i]
    	}
    ' <<< "$edges" | sort -u | sed 's/^/--package /' | tr '\n' ' '

# Takes a newline-separated list of changed files; skips when no crate is
# affected. The `just rs ...` calls below go through the root justfile's
# `mod rs` because these recipes run from the repo root, where a bare `check`
# would resolve to the root recipe of that name.

# Like `check`, but only the crates a diff touches plus their dependents.
check-changed $FILES:
    #!/usr/bin/env bash
    set -euo pipefail
    packages=$(just rs _select "$FILES")
    case "$packages" in
    	"")    echo "rs: no crates affected; skipping." ;;
    	ALL)   just rs check ;;
    	*)     echo "rs: checking ${packages//--package /}"; just rs check $packages ;;
    esac

    # moq-wasm is a non-default member, so `_select` never reaches it and the
    # branch above would check nothing at all for a moq-wasm-only diff.
    if grep -q '^rs/moq-wasm/' <<< "$FILES"; then
    	just rs wasm
    fi

# Same as `fix`, but scoped the same way as `check-changed`.
fix-changed $FILES:
    #!/usr/bin/env bash
    set -euo pipefail
    packages=$(just rs _select "$FILES")
    case "$packages" in
    	"")    echo "rs: no crates affected; skipping." ;;
    	ALL)   just rs fix ;;
    	*)     echo "rs: fixing ${packages//--package /}"; just rs fix $packages ;;
    esac

    # Mirrors `check-changed`: without this a moq-wasm-only diff is never
    # formatted, and `ci`'s `cargo fmt --all --check` fails on it later.
    if grep -q '^rs/moq-wasm/' <<< "$FILES"; then
    	just rs wasm-fix
    fi

# Runs the `#[cfg(target_os = "windows")]` code past the compiler: moq-video's
# Media Foundation capture/encode/decode and its D3D11 frames, which the Linux
# gate skips entirely. Cross-compiling can't stand in, because openh264-sys2
# builds vendored C++ that needs an MSVC toolchain and openh264 is a
# non-optional dependency.
#
# Nothing in CI runs this. Windows runners cost too much for a per-PR gate, so
# the code compiles for the first time in a tag-triggered release build unless
# someone runs this by hand on a Windows host.
#
# Default features rather than `--all-features`: jemalloc and the quiche
# backend don't build on MSVC, while moq-video's Linux-only nvenc/nvdec defaults
# are already no-ops off Linux. moq-gst is excluded because it links
# GStreamer via pkg-config, which the Windows runner doesn't have.
#
# `moq-cli/play` is named explicitly because it's off by default and is what
# turns on moq-video's wgpu renderer and moq-audio's cpal output, neither of
# which any default-feature build compiles.

# Compile the whole workspace on a Windows host. Must run ON Windows.
windows *args:
    cargo check --workspace --exclude moq-gst --all-targets --features moq-cli/play {{ args }}

# Runs the `#[cfg(target_os = "macos")]` code past the compiler: moq-video's
# VideoToolbox encode/decode and its ScreenCaptureKit / AVFoundation capture,
# plus moq-audio's ScreenCaptureKit system audio and TCC permission pre-check.
#
# Nothing in CI runs this either; Mac runners cost too much for a per-PR gate.
# The moq-video half has a release-time backstop, since libmoq's `libmoq-v*` tag
# build compiles it on Apple Silicon. The moq-audio half has none: its
# capture backend is behind an off-by-default feature no release build enables,
# so this recipe is the only thing that ever compiles it.
#
# Scoped to those two crates instead of the workspace, because they hold all
# the Apple-gated code the Linux gate misses. moq-ffi (and through it the
# Swift/Kotlin/Go wrappers) already compiles on macOS in swift.yml.
# `--all-features` is required, not just tidy: moq-audio's capture backend sits
# behind an off-by-default feature, so a plain check would skip the very code
# this recipe exists for. moq-video's Linux-only nvenc/vaapi/nvdec/pipewire
# deps are no-ops here.

# Compile the Apple-only code paths. Must run ON macOS.
macos *args:
    cargo check -p moq-video -p moq-audio --all-targets --all-features {{ args }}

# Same idea as `windows`/`macos`, for the browser: moq-wasm's whole crate root is
# `#![cfg(target_arch = "wasm32")]`, so a host-target `cargo check --workspace`
# compiles an empty crate and every error in it stays invisible. Only a wasm32
# build sees the code.
#
# Unlike those two this needs no special host, so `ci` runs it on every Rust PR.
# The wasm32 target and the `getrandom`/`web-sys` cfg flags come from the Nix dev
# shell and `.cargo/config.toml`. Not to be confused with the root `just wasm`,
# which builds the shippable `@moq/wasm` package (wasm-bindgen, release profile);
# this is the compile gate.

# Compile and lint the browser/WASM bindings, which the host-target check skips.
wasm *args:
    cargo clippy -p moq-wasm --target wasm32-unknown-unknown --all-targets {{ args }} -- -D warnings

# The `fix` counterpart to `wasm`. `cargo fmt` is scoped rather than `--all`
# because this runs alongside `fix`, which already formats everything else.

# Auto-fix the browser/WASM bindings.
wasm-fix *args:
    cargo clippy --fix --allow-staged --allow-dirty -p moq-wasm --target wasm32-unknown-unknown --all-targets {{ args }}
    cargo fmt -p moq-wasm

# Full Rust CI: check + feature edge cases + tests + build. Takes a
# newline-separated list of changed files; skips if FILES is non-empty
# and none match the Rust scope. Run `just rs ci` (no FILES) to
# force-run everything. `cargo publish --dry-run` lives in release-rs.yml.
#
# `cargo deny` runs here (not in `check`) so the inner-loop stays fast

# and devs aren't blocked by a fresh upstream advisory mid-edit.
ci $FILES="":
    #!/usr/bin/env bash
    set -euo pipefail
    if [[ -n "$FILES" ]] && ! grep -qE '^(rs/|Cargo\.toml$|Cargo\.lock$)' <<< "$FILES"; then
    	echo "rs: no Rust changes; skipping."
    	exit 0
    fi
    # Hygiene (non-compiling) + cargo-deny (hits the network, so it couldn't run
    # in the old Nix sandbox; runs fine here).
    cargo fmt --all --check
    cargo sort --workspace --check --no-format
    cargo shear
    cargo deny check --show-stats

    # Compiles. The all-features clippy / doc / test run as plain cargo (flake.nix
    # `checks` is unwired). Each full-workspace compile below is expensive, so we
    # run the minimum set that still covers every feature edge:
    #   - clippy --all-targets --all-features is the gate and a superset of a plain
    #     `cargo check --all-targets` (default features), so that redundant check
    #     pass is intentionally omitted -- one fewer full workspace compile.
    #   - --no-default-features stays: it's the only pass that exercises code behind
    #     `#[cfg(not(feature = ...))]`, which the all-features run never compiles.
    # (Local devs still get the full cargo loop via `just rs check`.)
    cargo check --workspace --no-default-features
    cargo clippy --workspace --all-targets --all-features -- -D warnings
    RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
    # `--workspace` above still compiles moq-wasm for the host, where its crate
    # root cfgs away to nothing, so this wasm32 pass is what actually checks it.
    just rs wasm
    # nextest compiles + runs the test binaries with real parallelism (faster than
    # `cargo test`'s serial harness). It does not build or run
    # doctests, so the `/// ```` examples are no longer compile-checked in CI -- an
    # accepted trade for speed (they still render in `cargo doc` output).
    # `--profile ci` picks up the longer hang timeout in .config/nextest.toml;
    # without it a runner under load could trip the local one.
    cargo nextest run --profile ci --workspace --all-targets --all-features

# Run the test suite.
#
# nextest, not `cargo test`, so a wedged test is killed instead of pinning a core
# until someone notices (`.config/nextest.toml` sets the timeout). It also
# matches what `ci` runs. The trade is doctests, which nextest does not execute;
# `just rs doctest` covers those.
test *args:
    cargo nextest run --all-targets {{ args }}

# Compile and run the `/// ```` examples, which nextest skips.
doctest *args:
    cargo test --doc {{ args }}

# Permutation-test the concurrent handoffs with loom.
#
# `--cfg loom` swaps kio's Mutex/atomics for loom's instrumented ones, so it
# rebuilds the world and can't share artifacts with a normal `cargo test`, so
# it stays separate from `check`/`ci`.
#
# `--release` because a model check runs the body once per interleaving, so the
# optimizer pays for itself many times over: 411s -> 51s across the two suites.
# Our assertions are plain `assert!`, not `debug_assert!`, so nothing is
# compiled out (verified by re-running the mutation check in release).
#
# The search is deliberately unbounded (no `preemption_bound`), so it covers
# every interleaving rather than just the short ones.
#
# kio's own unit tests are `cfg(not(loom))`: they'd construct loom primitives
# outside `loom::model`, which panics.
loom *args:
    RUSTFLAGS="--cfg loom" cargo test --release -p kio --lib loom:: {{ args }}
    RUSTFLAGS="--cfg loom" cargo test --release -p moq-net --test loom {{ args }}

build:
    cargo build

# Remove the Rust target directory.
clean:
    cargo clean

# Check semver compatibility against crates.io (default-members only).
semver:
    cargo semver-checks check-release

# Update versions and changelogs via release-plz.
bump:
    release-plz update

# Create release PRs and publish crates via release-plz.
release:
    release-plz release-pr --git-token "$GITHUB_TOKEN"
    release-plz release --git-token "$GITHUB_TOKEN"

update:
    cargo update
    cargo upgrade --incompatible

# Build a .deb or .rpm for one of the Rust binaries locally. nfpm comes
# from the flake's dev shell (`nix develop`). For .rpm produced this way,
# the linkage matches the host's glibc; CI uses an AlmaLinux 9 container
# to produce broadly compatible artifacts instead.
#
# Examples:
#   just rs package moq-relay deb

# just rs package moq-cli rpm
package crate packager:
    #!/usr/bin/env bash
    set -euo pipefail
    case "{{ crate }}" in
    	moq-relay)      bin=moq-relay  ;;
    	moq-cli)        bin=moq        ;;
        moq-token-cli)  bin=moq-token  ;;
    	*) echo "Unknown crate: {{ crate }} (use moq-relay, moq-cli, or moq-token-cli)" >&2; exit 1 ;;
    esac
    case "{{ packager }}" in
    	deb)
    		if command -v dpkg >/dev/null 2>&1; then
    			arch=$(dpkg --print-architecture)
    		else
    			case "$(uname -m)" in
    				x86_64)        arch=amd64 ;;
    				aarch64|arm64) arch=arm64 ;;
    				*) echo "Cannot infer deb arch from host $(uname -m)" >&2; exit 1 ;;
    			esac
    		fi
    		;;
    	rpm) arch=$(uname -m) ;;
    	*) echo "Unknown packager: {{ packager }} (use deb or rpm)" >&2; exit 1 ;;
    esac
    version=$(grep -m1 '^version' rs/{{ crate }}/Cargo.toml | sed 's/.*"\(.*\)".*/\1/')
    cargo build --release -p {{ crate }}
    mkdir -p dist
    VERSION="$version" ARCH="$arch" BINARY_PATH="target/release/$bin" \
    	nfpm pkg --packager {{ packager }} \
    		--config packaging/{{ crate }}/nfpm.yaml \
    		--target dist/
    ls -1 dist/
