Set up CraftBukkit-style patch workflow for xai-org/grok-build

Upstream is a periodic one-way export of xAI's monorepo (linear
"Synced from monorepo" commits, SOURCE_REV pinning the internal SHA)
and explicitly refuses external contributions. So local changes can
never be upstreamed, and upstream re-drops the whole tree on every
sync -- structurally the same problem CraftBukkit has with Mojang.

Adopt the Spigot/BuildTools model: patches/ is the source of truth,
work/ is a disposable build artifact regenerated from upstream.rev
plus patches/.

  scripts/apply-patches.sh    ~ applyPatches.sh
  scripts/rebuild-patches.sh  ~ rebuildPatches.sh
  scripts/update-upstream.sh  forward-ports patches onto a new sync
  scripts/setup.sh            toolchain: rust 1.94.0, dotslash/protoc
  upstream.rev                ~ BuildTools versions/*.json pin

format-patch uses --zero-commit so rebases do not rewrite the From
line of every patch, and apply's git clean preserves work/target so
replaying patches does not cost a cold Rust rebuild.

Ships two [EXAMPLE PATCH] commits demonstrating a source edit and a
new-file addition; both are safe to delete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iceBear67
2026-08-14 04:50:07 +00:00
co-authored by Claude Opus 5
commit 618c8e31ee
12 changed files with 841 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Rebuild work/ from scratch: pristine upstream@upstream.rev + every patch in
# patches/, each replayed as its own commit.
#
# This is the CraftBukkit/Spigot `applyPatches.sh` step. work/ is a DERIVED
# artifact -- anything in it that is not a commit is discarded.
#
# usage: apply-patches.sh [-f|--force]
# -f discard uncommitted changes in work/ without asking
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
FORCE="${FORCE:-0}"
case "${1:-}" in
-f|--force) FORCE=1 ;;
"") ;;
*) die "unknown argument: $1" ;;
esac
ensure_upstream
REV="$(read_rev)"
if [[ ! -d "$WORK_DIR/.git" ]]; then
info "Creating work/ from upstream/"
git clone --origin upstream --no-checkout "$UPSTREAM_DIR" "$WORK_DIR"
else
# Refuse to nuke work the developer has not saved anywhere. patches/ is the
# source of truth, so anything only present in work/ is about to be lost.
if [[ "$FORCE" != "1" ]]; then
assert_no_am_in_progress
if [[ -n "$(git -C "$WORK_DIR" status --porcelain)" ]]; then
die "work/ has uncommitted changes; they would be destroyed.
Commit them (cd work && git add -A && git commit) then 'make rebuild',
or re-run with: make apply FORCE=1"
fi
if git -C "$WORK_DIR" rev-parse -q --verify "refs/tags/$BASE_TAG" >/dev/null; then
have="$(git -C "$WORK_DIR" rev-list --count "$BASE_TAG..HEAD" 2>/dev/null || echo 0)"
want="$(count_patches)"
if [[ "$have" != "$want" ]]; then
die "work/ has $have commit(s) above $BASE_TAG but patches/ has $want patch file(s).
You probably forgot to run 'make rebuild' after committing.
To discard the work/ commits and replay patches/ as-is: make apply FORCE=1"
fi
fi
fi
step "Fetching into work/"
git -C "$WORK_DIR" remote set-url upstream "$UPSTREAM_DIR"
git -C "$WORK_DIR" fetch --tags --prune upstream
fi
cd "$WORK_DIR"
git cat-file -e "${REV}^{commit}" 2>/dev/null \
|| die "pinned revision $REV not found in upstream. Run 'make update' to re-pin."
# Abandon any half-finished am from a previous run before resetting.
gitdir="$(git rev-parse --git-dir)"
[[ -d "$gitdir/rebase-apply" ]] && git am --abort >/dev/null 2>&1 || true
info "Resetting work/ to upstream $(git rev-parse --short "$REV")"
git checkout -q -B "$WORK_BRANCH" "$REV"
git reset -q --hard "$REV"
# -x removes ignored files too, which is what we want for a pristine tree --
# EXCEPT target/, which holds the Rust incremental cache. Wiping it would turn
# every apply into a 30-minute cold rebuild.
git clean -qfdx -e /target
# The boundary between "upstream" and "ours". rebuild-patches.sh formats
# everything after this tag.
git tag -f "$BASE_TAG" HEAD >/dev/null
n="$(count_patches)"
if [[ "$n" -eq 0 ]]; then
info "No patches to apply -- work/ is pristine upstream."
exit 0
fi
info "Applying $n patch(es)"
if ! git am --3way --keep-cr --whitespace=nowarn "$PATCHES_DIR"/*.patch; then
failed="$(basename "$(cat "$(git rev-parse --git-dir)/rebase-apply/original-commit" 2>/dev/null || true)" 2>/dev/null || true)"
cat >&2 <<EOF
${C_RED}${C_BOLD}==> patch application failed${C_RESET}
This is normal after an upstream sync that touched the same code.
Resolve it the same way you would a rebase:
cd work
git status # see the conflicting files
\$EDITOR <conflicted files> # fix the conflict markers
git add -A
git am --continue # or: git am --skip / git am --abort
When 'git am' finishes cleanly, write the result back to patches/:
make rebuild
EOF
exit 1
fi
applied="$(git rev-list --count "$BASE_TAG..HEAD")"
info "work/ is ready: upstream $(git rev-parse --short "$BASE_TAG") + $applied patch(es)"
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Compile the patched tree. Any extra arguments are passed straight to cargo,
# e.g. ./scripts/build.sh --release
#
# Set CARGO_CMD to run something other than `build` (check, test, clippy, run).
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
setup_path
assert_work_ready
require_cmd cargo "Run 'make setup' first."
CARGO_CMD="${CARGO_CMD:-build}"
PKG="${PKG:-$BIN_PKG}"
cd "$WORK_DIR"
# bin/protoc is a DotSlash wrapper; it needs dotslash on PATH to self-resolve.
# If that is unavailable, hand the build script a system protoc instead so it
# does not silently skip codegen.
if ! ./bin/protoc --version >/dev/null 2>&1; then
if command -v protoc >/dev/null 2>&1 && [[ -z "${PROTOC:-}" ]]; then
export PROTOC="$(command -v protoc)"
warn "bin/protoc unusable (dotslash missing?) -- using PROTOC=$PROTOC"
else
die "no working protoc. Run 'make setup'."
fi
fi
info "cargo $CARGO_CMD -p $PKG ${*:-}"
exec cargo "$CARGO_CMD" -p "$PKG" "$@"
Executable
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# Shared helpers for the patch workflow. Sourced by every script in this dir.
#
# Layout (see README.md):
# upstream/ pristine clone of the vendor repo -- never edited by hand
# work/ upstream@REV + patches/ applied -- where you actually edit
# patches/ the source of truth -- tracked in git
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
UPSTREAM_URL="${UPSTREAM_URL:-https://github.com/xai-org/grok-build.git}"
UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-main}"
UPSTREAM_DIR="$ROOT/upstream"
WORK_DIR="$ROOT/work"
PATCHES_DIR="$ROOT/patches"
REV_FILE="$ROOT/upstream.rev"
# Branch created inside work/, and the tag marking the pristine upstream commit
# that patches are generated against. `base` is the boundary: everything after
# it is ours.
WORK_BRANCH="fork"
BASE_TAG="base"
# The package that produces the shipping binary.
BIN_PKG="xai-grok-pager-bin"
BIN_NAME="xai-grok-pager"
if [[ -t 1 ]]; then
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_RED=$'\033[31m'
C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_BLUE=$'\033[34m'
else
C_RESET=""; C_BOLD=""; C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE=""
fi
info() { printf '%s==>%s %s\n' "$C_BLUE$C_BOLD" "$C_RESET" "$*"; }
step() { printf '%s ->%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
warn() { printf '%s==> warning:%s %s\n' "$C_YELLOW$C_BOLD" "$C_RESET" "$*" >&2; }
die() { printf '%s==> error:%s %s\n' "$C_RED$C_BOLD" "$C_RESET" "$*" >&2; exit 1; }
# Rust lives in CARGO_HOME/bin, which is not on PATH by default in
# non-interactive shells. Every script that shells out to cargo needs this.
setup_path() {
export CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}"
export RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}"
case ":$PATH:" in
*":$CARGO_HOME/bin:"*) ;;
*) export PATH="$CARGO_HOME/bin:$PATH" ;;
esac
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 \
|| die "'$1' not found on PATH.${2:+ $2}"
}
# The pinned upstream commit. Comments and blank lines are allowed in the file
# so the pin can carry a note about which sync it corresponds to.
read_rev() {
[[ -f "$REV_FILE" ]] || die "missing $REV_FILE -- run 'make update' to pin an upstream revision"
local rev
rev="$(grep -vE '^\s*(#|$)' "$REV_FILE" | head -n1 | tr -d '[:space:]')"
[[ -n "$rev" ]] || die "$REV_FILE contains no revision"
printf '%s' "$rev"
}
write_rev() {
local rev="$1"
{
echo "# Pinned upstream revision of $UPSTREAM_URL"
echo "# Bump with: make update (never edit by hand unless you know why)"
echo "$rev"
} > "$REV_FILE"
}
# Clone on first use, otherwise fetch. Deliberately NOT a shallow clone: forward
# -porting patches across upstream syncs needs real history for merge bases.
ensure_upstream() {
if [[ ! -d "$UPSTREAM_DIR/.git" ]]; then
info "Cloning upstream $UPSTREAM_URL -> upstream/"
git clone --branch "$UPSTREAM_BRANCH" "$UPSTREAM_URL" "$UPSTREAM_DIR"
else
step "Fetching upstream"
git -C "$UPSTREAM_DIR" fetch --tags --prune origin \
"+refs/heads/$UPSTREAM_BRANCH:refs/remotes/origin/$UPSTREAM_BRANCH"
fi
}
# Number of .patch files currently in patches/ (0 when the dir is empty).
count_patches() {
local n=0
shopt -s nullglob
local f
for f in "$PATCHES_DIR"/*.patch; do n=$((n + 1)); done
shopt -u nullglob
printf '%s' "$n"
}
# Guard against running rebuild/build while a patch application is half-done.
assert_no_am_in_progress() {
local gitdir
gitdir="$(git -C "$WORK_DIR" rev-parse --git-dir)"
if [[ -d "$WORK_DIR/$gitdir/rebase-apply" ]]; then
die "a 'git am' is still in progress in work/.
Resolve it first:
cd work && git status
# fix conflicts, then: git add -A && git am --continue
# or abandon it with: git am --abort"
fi
}
assert_work_ready() {
[[ -d "$WORK_DIR/.git" ]] || die "work/ does not exist yet -- run 'make apply' first"
assert_no_am_in_progress
git -C "$WORK_DIR" rev-parse -q --verify "refs/tags/$BASE_TAG" >/dev/null \
|| die "work/ has no '$BASE_TAG' tag -- it was not created by apply-patches.sh. Run 'make apply'."
}
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Export every commit in work/ above the `base` tag back into patches/.
#
# This is the CraftBukkit/Spigot `rebuildPatches.sh` step and the only way
# changes leave work/. Run it after every commit you make in work/.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
assert_work_ready
cd "$WORK_DIR"
if [[ -n "$(git status --porcelain)" ]]; then
warn "work/ has uncommitted changes -- they will NOT be exported."
warn "Commit them first: cd work && git add -A && git commit"
fi
n="$(git rev-list --count "$BASE_TAG..HEAD")"
info "Exporting $n commit(s) above $BASE_TAG -> patches/"
mkdir -p "$PATCHES_DIR"
# Clear first: dropping or reordering commits in work/ must not leave orphaned
# patch files behind, and format-patch only ever writes, never deletes.
rm -f "$PATCHES_DIR"/*.patch
if [[ "$n" -eq 0 ]]; then
info "No commits above $BASE_TAG -- patches/ is now empty."
exit 0
fi
# --zero-commit the "From <sha>" line would otherwise change on every single
# rebase, producing a diff in every patch file for no reason
# --full-index full blob hashes give `git am --3way` something to fall back
# on when context has drifted
# --no-signature drops the trailing "-- \n2.47.3" git version banner
# --no-stat / -N match Spigot's output shape (no diffstat, no "[PATCH n/m]")
git format-patch \
--quiet \
--no-stat \
-N \
--zero-commit \
--full-index \
--no-signature \
--output-directory "$PATCHES_DIR" \
"$BASE_TAG"
info "patches/ now holds $(count_patches) patch file(s):"
for f in "$PATCHES_DIR"/*.patch; do
printf ' %s\n' "$(basename "$f")"
done
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# One-time host setup: Rust toolchain + the protoc that proto codegen needs.
# Idempotent -- safe to re-run.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
setup_path
info "Checking Rust toolchain"
if ! command -v rustup >/dev/null 2>&1; then
step "rustup not found -- installing"
curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs \
| sh -s -- -y --no-modify-path --profile default
setup_path
else
step "rustup $(rustup --version 2>/dev/null | head -n1)"
fi
# rust-toolchain.toml pins the exact version (and extra targets). Running rustup
# inside the tree makes it materialise that toolchain rather than the default.
TOOLCHAIN_SRC=""
for d in "$WORK_DIR" "$UPSTREAM_DIR"; do
[[ -f "$d/rust-toolchain.toml" ]] && { TOOLCHAIN_SRC="$d"; break; }
done
if [[ -n "$TOOLCHAIN_SRC" ]]; then
pinned="$(grep -E '^\s*channel' "$TOOLCHAIN_SRC/rust-toolchain.toml" | head -n1 | cut -d'"' -f2)"
step "Materialising pinned toolchain ${pinned:-from rust-toolchain.toml}"
(cd "$TOOLCHAIN_SRC" && rustup show >/dev/null)
step "$(cd "$TOOLCHAIN_SRC" && rustc --version)"
else
warn "no checkout yet -- pinned toolchain will be installed on first 'make apply'"
fi
# protoc: xai-grok-tools-api's build script compiles proto/grok-tools.proto.
# Upstream resolves it as $PROTOC -> ./bin/protoc (a DotSlash wrapper that
# downloads protoc 29.3) -> protoc on PATH. Satisfy path 2, fall back to 3.
info "Checking protoc"
if command -v dotslash >/dev/null 2>&1; then
step "dotslash $(dotslash --version 2>/dev/null | head -n1)"
else
step "dotslash not found -- installing (enables the hermetic bin/protoc)"
cargo install dotslash || warn "cargo install dotslash failed; will fall back to a system protoc"
setup_path
fi
probe_protoc() {
local d="$1"
[[ -x "$d/bin/protoc" ]] || return 1
(cd "$d" && ./bin/protoc --version) 2>/dev/null
}
resolved=""
for d in "$WORK_DIR" "$UPSTREAM_DIR"; do
if out="$(probe_protoc "$d")"; then
step "hermetic bin/protoc works: $out"
resolved=1
break
fi
done
if [[ -z "$resolved" ]]; then
if command -v protoc >/dev/null 2>&1; then
step "falling back to system protoc: $(protoc --version)"
elif [[ -d "$WORK_DIR" || -d "$UPSTREAM_DIR" ]]; then
step "no working protoc -- installing protobuf-compiler"
sudo apt-get update -qq && sudo apt-get install -y -qq protobuf-compiler \
|| die "could not provide a protoc. Install DotSlash (https://dotslash-cli.com) or protobuf-compiler manually."
step "system protoc: $(protoc --version)"
else
warn "no checkout yet -- protoc will be verified on first 'make apply'"
fi
fi
info "Setup complete."
cat <<EOF
Add Rust to your interactive shell if you have not already:
fish: source $CARGO_HOME/env.fish
bash: source $CARGO_HOME/env
Next:
make apply # build work/ from upstream + patches
make build # compile the $BIN_NAME binary
EOF
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Follow a new upstream sync: re-pin upstream.rev, replay patches/ on top of it,
# and write the forward-ported result back out.
#
# usage: update-upstream.sh [--dry-run] [--to <rev>]
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
DRY_RUN=0
TARGET=""
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--dry-run) DRY_RUN=1; shift ;;
--to) TARGET="${2:?--to needs a revision}"; shift 2 ;;
*) die "unknown argument: $1" ;;
esac
done
ensure_upstream
OLD="$(read_rev)"
if [[ -n "$TARGET" ]]; then
NEW="$(git -C "$UPSTREAM_DIR" rev-parse --verify "${TARGET}^{commit}")" \
|| die "revision '$TARGET' not found in upstream"
else
NEW="$(git -C "$UPSTREAM_DIR" rev-parse --verify "origin/$UPSTREAM_BRANCH")"
fi
if [[ "$OLD" == "$NEW" ]]; then
info "Already pinned to the newest upstream commit ($(git -C "$UPSTREAM_DIR" rev-parse --short "$NEW")). Nothing to do."
exit 0
fi
count="$(git -C "$UPSTREAM_DIR" rev-list --count "$OLD..$NEW" 2>/dev/null || echo '?')"
info "Upstream moved: $(git -C "$UPSTREAM_DIR" rev-parse --short "$OLD") -> $(git -C "$UPSTREAM_DIR" rev-parse --short "$NEW") ($count new commit(s))"
git -C "$UPSTREAM_DIR" log --oneline --no-decorate "$OLD..$NEW" | sed 's/^/ /'
# Each upstream commit is a squashed monorepo drop; SOURCE_REV names the
# internal commit it came from, which is the only human-meaningful version here.
old_src="$(git -C "$UPSTREAM_DIR" show "$OLD:SOURCE_REV" 2>/dev/null | tr -d '[:space:]' || true)"
new_src="$(git -C "$UPSTREAM_DIR" show "$NEW:SOURCE_REV" 2>/dev/null | tr -d '[:space:]' || true)"
[[ -n "$old_src$new_src" ]] && info "SOURCE_REV: ${old_src:-?} -> ${new_src:-?}"
changed="$(git -C "$UPSTREAM_DIR" diff --name-only "$OLD" "$NEW" | wc -l | tr -d ' ')"
info "$changed file(s) changed upstream"
if [[ "$DRY_RUN" == "1" ]]; then
info "--dry-run: upstream.rev left at $OLD"
exit 0
fi
info "Pinning upstream.rev -> $NEW"
write_rev "$NEW"
info "Replaying patches on the new base"
if ! "$ROOT/scripts/apply-patches.sh" --force; then
cat >&2 <<EOF
${C_YELLOW}${C_BOLD}==> upstream.rev has been bumped, but patches did not apply cleanly.${C_RESET}
Finish the 'git am' in work/ (instructions above), then run:
make rebuild
To abandon this update entirely and go back:
cd work && git am --abort
printf '%s\\n' "$OLD" > upstream.rev # or: git checkout upstream.rev
make apply
EOF
exit 1
fi
info "Patches replayed cleanly -- regenerating patches/ against the new base"
"$ROOT/scripts/rebuild-patches.sh"
cat <<EOF
${C_GREEN}${C_BOLD}==> Update complete.${C_RESET}
Review and commit the result:
git diff --stat upstream.rev patches/
make build
git add upstream.rev patches/ && git commit -m 'Update upstream to $(git -C "$UPSTREAM_DIR" rev-parse --short "$NEW")'
EOF