check-release-artifact-features.sh

In scripts/ · At e15865e

e15865eHistory

Preview

Executable · 7 KiB
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT OR Apache-2.0
#
# Checks the release mkit compiler-artifact log and stripped binary.
# The mkit check also allowlists the packages the CLI may compile:
# scripts/release/mkit-packages.golden, the union over every release target
# of `cargo tree -p mkit-cli` (normal + build dependencies, which matches
# the release build's compiler artifacts exactly). A package outside it
# fails the check. If the new package is intended, run `--update-golden`
# (it needs no toolchain for the other targets) and commit the diff with
# the change that brought the package in.

set -euo pipefail

here="$(cd "$(dirname "$0")" && pwd)"
root="$(cd "$here/.." && pwd)"
golden="$here/release/mkit-packages.golden"

case "${1:-}" in
  --update-golden)
    # Every release target, straight from release.yml's build matrix.
    targets="$(sed -n 's/^ *- target: *//p' "$root/.github/workflows/release.yml")"
    if [ -z "$targets" ]; then
      echo "check-release-artifact-features: no targets found in release.yml" >&2
      exit 1
    fi
    tmp="$(mktemp)"
    trap 'rm -f "$tmp"' EXIT
    {
      echo "# Packages the release \`mkit\` build may compile: the union over"
      echo "# release.yml's targets of \`cargo tree -p mkit-cli -e normal,build\`."
      echo "# Checked by scripts/check-release-artifact-features.sh; regenerate with"
      echo "# \`scripts/check-release-artifact-features.sh --update-golden\` and review"
      echo "# the diff: every new name is a new dependency of the shipped CLI."
      for t in $targets; do
        (cd "$root/rust" && cargo tree --locked -p mkit-cli --target "$t" \
          -e normal,build --prefix none -f '{p}') | awk '{print $1}'
      done | LC_ALL=C sort -u
    } > "$tmp"
    mv "$tmp" "$golden"
    trap - EXIT
    echo "check-release-artifact-features: wrote $golden ($(grep -vc '^#' "$golden") packages; targets: $(echo "$targets" | tr '\n' ' '))"
    exit 0
    ;;
esac

if [ $# -ne 2 ]; then
  echo "usage: $0 <build.jsonl> <binary> | --update-golden" >&2
  exit 2
fi
jsonl="$1"
binary="$2"
for f in "$jsonl" "$binary" "$golden"; do
  if [ ! -f "$f" ]; then
    echo "check-release-artifact-features: not found: $f" >&2
    exit 1
  fi
done

fail=0

python3 - "$jsonl" "$binary" "$golden" <<'PY' || fail=1
import json
import os
import sys

path, binary, golden_path = sys.argv[1:4]


def pkg_name(pid):
    # Package id spec (cargo >= 1.77): `<source-url>#<name>@<version>`, or
    # `<source-url>#<version>` when the name is the URL's last segment.
    # Older cargo: `<name> <version> (<source>)`.
    if "#" not in pid:
        return pid.split(" ", 1)[0]
    url, frag = pid.rsplit("#", 1)
    if "@" in frag:
        return frag.split("@", 1)[0]
    return url.split("?", 1)[0].rstrip("/").rsplit("/", 1)[-1]


features = {}
executables = {}
with open(path, encoding="utf-8") as f:
    for line in f:
        line = line.strip()
        if not line.startswith("{"):
            continue
        msg = json.loads(line)
        if msg.get("reason") != "compiler-artifact":
            continue
        name = pkg_name(msg["package_id"])
        features.setdefault(name, set()).update(msg.get("features", []))
        target = msg.get("target", {})
        if "bin" in target.get("kind", []) and msg.get("executable"):
            executables[target.get("name")] = msg["executable"]

errors = []


def err(text):
    errors.append(text)


if not features:
    err("no compiler-artifact messages: was the build run with --message-format=json*?")

for name, feats in sorted(features.items()):
    for seam in ("__test-faults", "test-faults", "stubs"):
        if seam in feats:
            err(f"{name} was compiled with the `{seam}` test seam")

# The binary being scanned must be the one this log built.
bin_name = "mkit"
exe = executables.get(bin_name)
if exe is None:
    err(f"the build did not produce the `{bin_name}` binary")
elif os.path.realpath(exe) != os.path.realpath(binary):
    err(f"the log built `{bin_name}` at {exe}, but the scanned binary is {binary}")

for banned in ("mkit-server-native", "axum", "rusqlite", "libsqlite3-sys"):
    if banned in features:
        err(f"server-only package `{banned}` is in the mkit build")
# Server-side features of shared HTTP crates.
for name, banned in (
    ("hyper", {"server"}),
    ("hyper-util", {"server", "server-auto", "server-graceful"}),
    ("connectrpc", {"server", "axum"}),
):
    bad = features.get(name, set()) & banned
    if bad:
        err(f"{name} was compiled with server features {sorted(bad)}")
# tower-http is reqwest's redirect layer in the CLI: exactly these
# features, never the server's layers (cors, limit, trace, ...).
tower_http_allowed = {"follow-redirect", "futures-util", "tower"}
extra = features.get("tower-http", set()) - tower_http_allowed
if extra:
    err(f"tower-http was compiled with {sorted(extra)}; mkit allows only {sorted(tower_http_allowed)}")
# mkit-cli's own `mkit-server` dependency (WP-M0-13): ssh + fs only.
extra = features.get("mkit-server", set()) - {"ssh", "fs"}
if extra:
    err(f"mkit-server was compiled with {sorted(extra)} (mkit-cli declares only ssh, fs)")
# Package allowlist.
with open(golden_path, encoding="utf-8") as f:
    golden = {l.strip() for l in f if l.strip() and not l.startswith("#")}
new = sorted(set(features) - golden)
if new:
    err(f"{len(new)} package(s) not in {os.path.relpath(golden_path)}: {', '.join(new)}. "
        "If this dependency of the mkit CLI is intended, run "
        "`scripts/check-release-artifact-features.sh --update-golden` and commit "
        "the golden diff with the change that brought it in.")
print(f"check-release-artifact-features: mkit: {len(features)} packages compiled "
      f"(golden: {len(golden)}); "
      f"hyper={sorted(features.get('hyper', []))} "
      f"hyper-util={sorted(features.get('hyper-util', []))} "
      f"connectrpc={sorted(features.get('connectrpc', []))} "
      f"tower-http={sorted(features.get('tower-http', []))} "
      f"mkit-server={sorted(features['mkit-server']) if 'mkit-server' in features else 'absent'}")

for e in errors:
    print(f"check-release-artifact-features: ERROR: {e}", file=sys.stderr)
sys.exit(1 if errors else 0)
PY

# The binary itself: the stripped release binary has no symbol table, so
# scan its bytes (a superset of `strings`). `sqlite3_` names come from the
# bundled SQLite; `x-mkit-test-` headers exist only under `test-faults`.
scan() {
  if LC_ALL=C grep -a -q -- "$1" "$binary"; then
    echo "check-release-artifact-features: ERROR: $(basename "$binary") contains \`$1\` ($2)" >&2
    fail=1
  fi
}
scan 'sqlite3_' 'SQLite linked into mkit'
scan 'x-mkit-test-' 'test-faults seam compiled in'
scan '/__stub/' 'MPP stub control plane compiled in'
scan 'TEST_OUTBOX_BACKLOG_ROWS' 'test-only Worker backlog var compiled in'
scan 'TEST_TICKET_TTL_MS' 'test-only Worker ticket var compiled in'

if [ "$fail" -ne 0 ]; then
  echo "check-release-artifact-features: FAILED (cli): $(basename "$binary")" >&2
  exit 1
fi
echo "check-release-artifact-features: OK (cli): $(basename "$binary")"

Files