1#!/usr/bin/env bash
See ./README.md for what this does and why.
3set -euo pipefail
research/subsecond-patch-build.md §5: re-entering nix develop on every
buck2/curl call this script makes is most of the cycle time when it's not
already inside one. Skip it when it is (nix sets IN_NIX_SHELL) and say so
once; run this script from a nix develop shell to get the fast path.
--- buck2 helpers -----------------------------------------------------
Where a target's -Csave-temps=true objects land is read from buck2's OWN
rustc-invocation record (<crate>-link-diag.args's --out-dir=), not
guessed from buck2's internal per-action directory codes ("XIPL" for a
rust_binary, "LPPL" for a rust_library, seen 2026-09-20) - those are not
documented API and differ by target kind. See tools/hotpatch/CLAUDE.md.
39extras_objects() { # $1 = main output path -> one .rcgu.o path per line, or fails 40 local main_out="$1" link_args extras 41 link_args="$(find "$(dirname "$main_out")" -iname '*-link-diag.args' -print -quit)" 42 [[ -n "$link_args" ]] || return 1 43 extras="$(grep -o -- '--out-dir=[^ ]*' "$link_args" | head -1 | cut -d= -f2-)" 44 shopt -s nullglob 45 local found=("$extras"/*.rcgu.o) 46 shopt -u nullglob 47 ((${#found[@]} > 0)) || return 1 48 printf '%s\n' "${found[@]}" 49} 50 51buck_rule() { # $1 = buck2 label -> that target's own BUCK stanza, for grepping 52 local dir="${1#//}" 53 dir="${dir%%:*}" 54 local name="${1##*:}" 55 sed -n "/name = \"${name}\"/,/^)/p" "$dir/BUCK" 56}
A target's own first-party deps under //apps/ or //packages/, PLUS any
same-package :name dep - broadened 2026-09-20 from //packages/-only, so
a rust_binary that splits its patchable logic into a sibling rust_library
in its OWN apps/<name>/ directory (apps/native:app, next to
apps/native:native, referred to from BUCK as plain ":app") is discovered
the same way a packages/ dependency already was.
64first_party_deps_of() { # $1 = buck2 label -> its own //apps/..., //packages/... or :name deps 65 local label="$1" dir rule 66 dir="${label#//}" 67 dir="${dir%%:*}" 68 rule="$(buck_rule "$label")" 69 grep -o '"//\(apps\|packages\)/[a-zA-Z0-9_/-]*:[a-zA-Z0-9_-]*"' <<<"$rule" | tr -d '"' 70 grep -o '":[a-zA-Z0-9_-]*"' <<<"$rule" | tr -d '":' | sed "s|^|//$dir:|" 71}
Every first-party dep of $1, TRANSITIVELY, each printed once - needed since
apps/native:native -> apps/native:app -> packages/panels is now two hops,
not one: a one-level walk found app but never recursed into panels, so a
panels edit built fresh objects that were never fed to hotpatch at all
(the patch silently fell back to the base binary's OLD panels code via the
undefined-symbol stub - wrong, and no error anywhere; found 2026-09-20
proving this end to end, exactly the kind of silent failure
research/subsecond-patch-build.md §8 warns about).
85all_first_party_deps_of() { # $1 = buck2 label -> every first-party dep, transitively, deduped 86 local seen=" $1 " queue=("$1") i=0 current dep 87 while ((i < ${#queue[@]})); do 88 current="${queue[$i]}" 89 i=$((i + 1)) 90 while IFS= read -r dep; do 91 [[ -n "$dep" ]] || continue 92 [[ "$seen" == *" $dep "* ]] && continue 93 seen+="$dep " 94 queue+=("$dep") 95 echo "$dep" 96 done < <(first_party_deps_of "$current") 97 done 98}
100stale_extras_of() { # $1 = buck2 label -> its own extras, found ON DISK, with NO buck2 build 101 local label="$1" dir name cfg main_dir 102 dir="${label#//}" 103 dir="${dir%%:*}" 104 name="${label##*:}" 105 for cfg in buck-out/v2/art/root/*/; do 106 main_dir="${cfg}${dir}/__${name}__" 107 [[ -d "$main_dir" ]] || continue 108 extras_objects "$main_dir/." && return 0 109 done 110 return 1 111} 112 113delegates_to_hot_patch_dep() { # $1 = buck2 label -> true if any first-party dep of it (transitively) has_save_temps 114 local dep 115 while IFS= read -r dep; do 116 [[ -n "$dep" ]] || continue 117 has_save_temps "$dep" && return 0 118 done < <(all_first_party_deps_of "$1") 119 return 1 120}
--- where pid/aslr_reference/the base image come from -----------------
apps/native has an MCP server inside the window (packages/mcp); everything else (apps/hotdemo) only has a log line to scrape and a file to drop, per research/subsecond-patch-build.md §4/§6.3. Resolved FIRST and from the running process alone, with no buck2 build: --base is /proc/$PID/exe, the exact bytes the process is executing, never a freshly-linked copy from disk. See CLAUDE.md - this is both cheaper (no relink of the executable, ever, in this script) and more correct (a fresh relink's symbol addresses are not guaranteed to still match what the OS actually has mapped for a process that has not been restarted).
139mcp_raw() { # $1 = JSON-RPC request body -> the raw (SSE) HTTP response body 140 local extra=() 141 [[ -n "$MCP_SESSION" ]] && extra+=(-H "Mcp-Session-Id: $MCP_SESSION") 142 curl -sS -D "$MCP_HEADERS" -X POST "$MCP_URL" \ 143 -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \ 144 "${extra[@]}" -d "$1" 145} 146 147mcp_connect() { 148 mcp_raw '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"patch.sh","version":"0"}}}' >/dev/null 149 MCP_SESSION="$(grep -i '^mcp-session-id:' "$MCP_HEADERS" | tr -d '\r' | awk '{print $2}')" 150 [[ -n "$MCP_SESSION" ]] || { echo "patch.sh: no Mcp-Session-Id back from $MCP_URL - is the window running?" >&2; exit 1; } 151 mcp_raw '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null 152} 153 154mcp_tool() { # $1 = tool name, $2 = arguments JSON -> the tool's text content, or exits loudly 155 MCP_ID=$((MCP_ID + 1)) 156 local body result 157 body="$(mcp_raw "$(jq -nc --arg name "$1" --argjson args "$2" --argjson id "$MCP_ID" \ 158 '{jsonrpc:"2.0",id:$id,method:"tools/call",params:{name:$name,arguments:$args}}')")" 159 result="$(grep -m1 '^data: {' <<<"$body" | sed 's/^data: //' | jq '.result')" 160 if [[ "$(jq -r '.isError // false' <<<"$result")" == "true" ]]; then 161 echo "patch.sh: $1 failed: $(jq -r '.content[0].text' <<<"$result")" >&2 162 exit 1 163 fi 164 jq -r '.content[0].text' <<<"$result" 165} 166 167mcp_close() { 168 curl -sS -o /dev/null -X DELETE "$MCP_URL" -H "Mcp-Session-Id: $MCP_SESSION" || true 169} 170 171if [[ "$TARGET" == "//apps/native:native" ]]; then 172 echo "patch.sh: asking the window for pid/aslr_reference over MCP" >&2 173 mcp_connect 174 info="$(mcp_tool patch_info '{}')" 175 PID="$(jq -r '.pid' <<<"$info")" 176 ASLR="$(jq -r '.aslr_reference' <<<"$info")" 177else 178 LOG="run/${CRATE}.log" 179 [[ -f "$LOG" ]] || { echo "patch.sh: $LOG does not exist - is $CRATE running and logging there?" >&2; exit 1; } 180 PID="$(grep -o 'pid=[0-9]*' "$LOG" | tail -1 | cut -d= -f2)" 181 ASLR="$(grep -o 'aslr_reference=0x[0-9a-f]*' "$LOG" | tail -1 | cut -d= -f2)" 182fi 183if [[ -z "${PID:-}" || -z "${ASLR:-}" ]]; then 184 echo "patch.sh: could not get $CRATE's pid/aslr_reference" >&2 185 exit 1 186fi 187BASE_OUT="/proc/$PID/exe" 188[[ -r "$BASE_OUT" ]] || { echo "patch.sh: cannot read $BASE_OUT - is pid $PID still running as this user?" >&2; exit 1; }
--- the tip's own objects, and folding in any hot-patch-flagged first- party dependency -------------------------------------------------------
Three shapes, told apart mechanically rather than by target name:
- A tip with save-temps and no first-party dependency that ALSO has save-temps (apps/hotdemo) IS the whole patchable crate: rebuild it and take its own fresh objects.
- A tip with save-temps that DELEGATES to such a dependency
(apps/native:native -> apps/native:app, since 2026-09-20) still needs
an object of its OWN, because subsecond's
apply_patchlooks up the "main" sentinel symbol by name inside whatever gets linked into the patch, andmainonly exists in the executable's own crate - but REBUILDING it would force exactly the relink this split exists to avoid, whenever the dependency it delegates to has changed. So its object is read from disk, from whatever buck2 last actually built it (initial launch, or arestart), with no buck2 build here at all. - A tip with no save-temps of its own contributes nothing directly; every object comes from its dependencies.
210OBJECTS=() 211if has_save_temps "$TARGET" && ! delegates_to_hot_patch_dep "$TARGET"; then 212 echo "patch.sh: building $TARGET" >&2 213 TIP_OUT="$(build_output "$TARGET")" 214 [[ -n "$TIP_OUT" ]] || { echo "patch.sh: rebuild of $TARGET produced no output path - see $BUILD_LOG" >&2; exit 1; } 215 mapfile -t OBJECTS < <(extras_objects "$TIP_OUT") \ 216 || { echo "patch.sh: $TARGET claims -Csave-temps=true but produced no .rcgu.o files - see $BUILD_LOG" >&2; exit 1; } 217elif has_save_temps "$TARGET"; then 218 echo "patch.sh: $TARGET delegates its patch points to a dependency - reusing its own last-built object as the 'main' sentinel, without rebuilding it" >&2 219 mapfile -t OBJECTS < <(stale_extras_of "$TARGET") \ 220 || { echo "patch.sh: no on-disk objects for $TARGET yet - build or run it at least once before patching" >&2; exit 1; } 221else 222 echo "patch.sh: $TARGET has no patch points of its own - patching its first-party dependencies only, without rebuilding it" >&2 223fi
225while IFS= read -r dep; do 226 [[ -n "$dep" ]] || continue 227 # Checked BEFORE building, not after: a dep that isn't hot-patch-flagged 228 # (packages/mcp) is skipped without ever invoking buck2 on it. Building 229 # it anyway to find that out costs real time - mcp depends on panels, so 230 # a panels edit invalidates mcp's own (proc-macro-heavy) compile too, 231 # for a dependency whose result was always going to be thrown away 232 # (measured 2026-09-20: ~10s wasted on packages/mcp per panels edit, 233 # before this check moved earlier). 234 if ! has_save_temps "$dep"; then 235 echo "patch.sh: $dep is not hot-patch-flagged - skipping it, unbuilt" >&2 236 continue 237 fi 238 dep_out="$(build_output "${dep}[static_pic]")" 239 if [[ -z "$dep_out" ]]; then 240 echo "patch.sh: $dep did not build - skipping it (see $BUILD_LOG)" >&2 241 continue 242 fi 243 if dep_objects="$(extras_objects "$dep_out")"; then 244 n="$(wc -l <<<"$dep_objects")" 245 echo "patch.sh: folding in $n object(s) from $dep" >&2 246 while IFS= read -r o; do OBJECTS+=("$o"); done <<<"$dep_objects" 247 else 248 echo "patch.sh: $dep claims -Csave-temps=true but produced no .rcgu.o - see $BUILD_LOG" >&2 249 fi 250done < <(all_first_party_deps_of "$TARGET") 251 252((${#OBJECTS[@]} > 0)) || { echo "patch.sh: no hot-patch-flagged objects found for $TARGET or its dependencies" >&2; exit 1; } 253 254echo "patch.sh: building tools/hotpatch" >&2 255HOTPATCH_OUT="$(build_output //tools/hotpatch:hotpatch)" 256 257echo "patch.sh: patching pid=$PID aslr_reference=$ASLR with ${#OBJECTS[@]} object(s)" >&2 258 259run "$HOTPATCH_OUT" \ 260 --base "$BASE_OUT" \ 261 --objects "${OBJECTS[@]}" \ 262 --aslr-reference "$ASLR" \ 263 --out-dir "$PATCH_DIR" 264 265LATEST_JSON="$(ls -t "$PATCH_DIR"/patch-*.json | head -1)" 266 267if [[ "$TARGET" == "//apps/native:native" ]]; then 268 JSON_PATH="$(realpath "$LATEST_JSON")" 269 mcp_tool dev_mode '{"on":true}' >/dev/null 270 echo "patch.sh: $(mcp_tool hot_patch "$(jq -nc --arg p "$JSON_PATH" '{jump_table:$p}')")" >&2 271 mcp_tool dev_mode '{"on":false}' >/dev/null 272 mcp_close 273else 274 cp "$LATEST_JSON" "run/${CRATE}.patch.json" 275 echo "patch.sh: dropped $(basename "$LATEST_JSON") at run/${CRATE}.patch.json" >&2 276fi